Generating Code for EF Core with CatFactory

0
33

Introduction

EF Core is the version of Entity Framework for DotNet Core, to develop large applications it’s very hard to write code for a lot of objects: entities, mappings, repositories and contracts, CatFactory provides an easy way to generate repetitive code and focus on complex logic.

Background

Code generation it’s a common technique developers use to reduce time in code writing, I know the most programmers build a code generator in their professional lifes.

EF 6.x had a wizard for code generation, that tool generates DbContext and POCOs but there isn’t code for Fluent API, Repositories and other things like those; with .NET Core there is a command line tool for code generation but we have the same scenario, there is generation only for DbContext and Entities; with CatFactory we’re looking for a simple way to generate code with enterprise patterns, please don’t forget this is an alpha version of CatFactory, don’t pretend to have in this date a full version of code generation engine.

Why don’t use code CodeDOM? CodeDOM it’s a complex code generation engine, I don’t saying CodeDOM sucks or something like that, but at this moment we’re focus on generate code in the more simple way, maybe in the incoming versions we’ll add some integration with CodeDOM.

Skills Prerequisites

  • OOP
  • AOP
  • ORM
  • C#
  • Design Patterns

Software Prerequisites

  • Visual Studio 2015 with Update 3 or VS Code
  • Access to valid SQL Server instance

CatFactory History

In 2005 year, I was on my college days and I worked on my final project that included a lot of tables, for those days C# didn’t have automatic properties also I worked on store procedures that included a lot of columns, I thought if there was a way to generate all that code because it was repetitive and I wasted time in wrote a lot of code.

For 2006 beggining I’ve worked for a company and I worked in a prototype to generate code but I didn’t have experience and I was a junior developer, so I developed a version in WebForms that didn’t allow to save the structure ha,ha,ha that project it was my first project in C# because I came from VB world but I bought a book about Web Services in DotNet and that book used C# code, that was new for me but it got me a very important idea, learn C# and I wrote all first code generation form in C#.

Later, there was a prototype of Entity for SQL, the grandfather of entity framework and I develop a simple ORM because I had table class and other classes such as Column, so after of reviewed Entity for SQL I decided to add the logic to read database and provide a simple way to read the database also of code generation.

For 2008 I built the first ORM based on my code generation engine, in that time it was called F4N1, I worked on an ORM must endure different databases engines such as SQL Server, Sybase and Oracle; so I generated a lot of classes with that engine, for that time the automated unit tests did not exist, I had a webform page that generated that code ha,ha,ha I know it was ugly and crappy but in that time that was my knowledge allowed me

For 2011 I worked on a demo for a person that worked in his company and that person used another tool for code generation, so my code generation engine wasn’t use for his work.

For 2012 I worked for a company needed to rebuilt all system with new technologies (ASP.NET MVC and Entity Framework) so I invested time about MVC and EF learning but as usual, there isn’t time for that ha,ha,ha and again my code generation it wasn’t considered for that upgrade =(

For 2014, I thought to make a nuget package to my code generation but in those days I didn’t have the focus to accomplish that feature and always I used my code generation as a private tool, in some cases I shared my tool with some coworkers to generate code and reduce the time for code writing.

For 2016, I decided to create a nuget package and integrates with EF Core, using all experience from 10 years ago 😀

Please remember that from the beginning I was continuing improve the way of code generation, my first code was a crap but with the timeline I’ve improved the design and naming for objects.

Why I named CatFactory? It was I had a cat, her name was Mindy and that cat had manny kittens (sons), so the basic idea it was the code generation engine generates the code as fast Mindy provided kittens ha,ha,ha

Using the code

Please follow these steps to generate code for EF Core with CatFactory:

Step 01 – Create sample database

In order to generate code, run the following script on your SQL Server instance:

use master
go

create database Store
go

use Store
go

create schema HumanResources
go

create schema Production
go

create schema Sales
go

create table [EventLog]
(
 [EventLogID] int not null identity(1, 1),
 [EventType] int not null,
 [Key] varchar(255) not null,
 [Message] varchar(max) not null,
 [EntryDate] datetime not null
)

create table [ChangeLog]
(
 [ChangeLogID] int not null identity(1, 1),
 [ClassName] varchar(255) not null,
 [PropertyName] varchar(255) not null,
 [Key] varchar(255) not null,
 [OriginalValue] varchar(max) null,
 [CurrentValue] varchar(max) null,
 [UserName] varchar(25) not null,
 [ChangeDate] datetime not null
)

create table [HumanResources].[Employee]
(
 [EmployeeID] int not null identity(1, 1),
 [FirstName] varchar(25) not null,
 [MiddleName] varchar(25) null,
 [LastName] varchar(25) not null,
 [BirthDate] datetime not null,
 [CreationUser] varchar(25) not null,
 [CreationDateTime] datetime not null,
 [LastUpdateUser] varchar(25) null,
 [LastUpdateDateTime] datetime null,
 [Timestamp] rowversion null
)

create table [Production].[ProductCategory]
(
 [ProductCategoryID] int not null identity(1, 1),
 [ProductCategoryName] varchar(100) not null,
 [CreationUser] varchar(25) not null,
 [CreationDateTime] datetime not null,
 [LastUpdateUser] varchar(25) null,
 [LastUpdateDateTime] datetime null,
 [Timestamp] rowversion null
)

create table [Production].[Product]
(
 [ProductID] int not null identity(1, 1),
 [ProductName] varchar(100) not null,
 [ProductCategoryID] int not null,
 [UnitPrice] decimal(8, 4) not null,
 [Description] varchar(255) null,
 [Discontinued] bit not null,
 [CreationUser] varchar(25) not null,
 [CreationDateTime] datetime not null,
 [LastUpdateUser] varchar(25) null,
 [LastUpdateDateTime] datetime null,
 [Timestamp] rowversion null
)

create table [Production].[ProductInventory]
(
 [ProductInventoryID] int not null identity(1, 1),
 [ProductID] int not null,
 [Quantity] int not null,
 [Stocks] int not null,
 [CreationUser] varchar(25) not null,
 [CreationDateTime] datetime not null,
 [LastUpdateUser] varchar(25) null,
 [LastUpdateDateTime] datetime null,
 [Timestamp] rowversion null
)

create table [Sales].[Customer]
(
 [CustomerID] int not null identity(1, 1),
 [CompanyName] varchar(100) null,
 [ContactName] varchar(100) null,
 [CreationUser] varchar(25) not null,
 [CreationDateTime] datetime not null,
 [LastUpdateUser] varchar(25) null,
 [LastUpdateDateTime] datetime null,
 [Timestamp] rowversion null
)

create table [Sales].[Shipper]
(
 [ShipperID] int not null identity(1, 1),
 [CompanyName] varchar(100) null,
 [ContactName] varchar(100) null,
 [CreationUser] varchar(25) not null,
 [CreationDateTime] datetime not null,
 [LastUpdateUser] varchar(25) null,
 [LastUpdateDateTime] datetime null,
 [Timestamp] rowversion null
)

create table [Sales].[OrderStatus]
(
 [OrderStatusID] smallint not null,
 [Description] varchar(100) not null,
 [CreationUser] varchar(25) not null,
 [CreationDateTime] datetime not null,
 [LastUpdateUser] varchar(25) null,
 [LastUpdateDateTime] datetime null,
 [Timestamp] rowversion null
)

create table [Sales].[Order]
(
 [OrderID] int not null identity(1, 1),
 [OrderStatusID] smallint not null,
 [OrderDate] datetime not null,
 [CustomerID] int not null,
 [EmployeeID] int not null,
 [ShipperID] int not null,
 [Total] decimal(12, 4) not null,
 [Comments] varchar(255) null,
 [CreationUser] varchar(25) not null,
 [CreationDateTime] datetime not null,
 [LastUpdateUser] varchar(25) null,
 [LastUpdateDateTime] datetime null,
 [Timestamp] rowversion null
)

create table [Sales].[OrderDetail]
(
 [OrderDetailID] int not null identity(1, 1),
 [OrderID] int not null,
 [ProductID] int not null,
 [ProductName] varchar(255) not null,
 [UnitPrice] decimal(8, 4) not null,
 [Quantity] int not null,
 [Total] decimal(8, 4) not null,
 [CreationUser] varchar(25) not null,
 [CreationDateTime] datetime not null,
 [LastUpdateUser] varchar(25) null,
 [LastUpdateDateTime] datetime null,
 [Timestamp] rowversion null
)
go

alter table [EventLog]
 add constraint EventLog_PK primary key (EventLogID)
go

alter table [ChangeLog]
 add constraint ChangeLog_PK primary key (ChangeLogID)
go

alter table [HumanResources].[Employee]
 add constraint HumanResources_Employee_PK primary key (EmployeeID)
go

alter table [Production].[ProductCategory]
 add constraint Production_ProductCategory_PK primary key (ProductCategoryID)
go

alter table [Production].[Product]
 add constraint Production_Product_PK primary key (ProductID)
go

alter table [Production].[Product]
 add constraint Production_Product_ProductName unique (ProductName)
go

alter table [Production].[ProductInventory]
 add constraint Production_ProductInventory_PK primary key (ProductInventoryID)
go

alter table [Sales].[Customer]
 add constraint Sales_Customer_PK primary key (CustomerID)
go

alter table [Sales].[Shipper]
 add constraint Sales_Shipper_PK primary key (ShipperID)
go

alter table [Sales].[OrderStatus]
 add constraint Sales_OrderStatus_PK primary key (OrderStatusID)
go

alter table [Sales].[Order]
 add constraint Sales_Order_PK primary key (OrderID)
go

alter table [Sales].[OrderDetail]
 add constraint Sales_OrderDetail_PK primary key (OrderDetailID)
go

alter table [Sales].[OrderDetail]
 add constraint Sales_OrderDetail_U unique (OrderID, ProductID)
go

alter table [Production].[Product]
 add constraint Production_Product_ProductCategory foreign key (ProductCategoryID)
 references [Production].[ProductCategory]
go

alter table [Production].[ProductInventory]
 add constraint Production_ProductInventory_Product foreign key (ProductID)
 references [Production].[Product]
go

alter table [Sales].[Order]
 add constraint Sales_Order_OrderStatus foreign key (OrderStatusID)
 references [Sales].[OrderStatus]
go

alter table [Sales].[Order]
 add constraint Sales_Order_Customer foreign key (CustomerID)
 references [Sales].[Customer]
go

alter table [Sales].[Order]
 add constraint Sales_Order_Employee foreign key (EmployeeID)
 references [HumanResources].[Employee]
go

alter table [Sales].[Order]
 add constraint Sales_Order_Shipper foreign key (ShipperID)
 references [Sales].[Shipper]
go

alter table [Sales].[OrderDetail]
 add constraint Sales_OrderDetail_Order foreign key (OrderID)
 references [Sales].[Order]
go

alter table [Sales].[OrderDetail]
 add constraint Sales_OrderDetail_Product foreign key (ProductID)
 references [Production].[Product]
go

create view OrderSummary
as
 select
 OrderHeader.OrderID,
 OrderHeader.OrderDate,
 Customer.CompanyName as CustomerName,
 Employee.FirstName + ' ' + isnull(Employee.MiddleName, '') + ' ' + Employee.LastName as EmployeeName,
 Shipper.CompanyName as ShipperName
 from
 Sales.[Order] OrderHeader
 inner join Sales.Customer Customer
 on OrderHeader.CustomerID = Customer.CustomerID
 inner join HumanResources.Employee Employee
 on OrderHeader.EmployeeID = Employee.EmployeeID
 inner join Sales.Shipper Shipper
 on OrderHeader.ShipperID = Shipper.ShipperID
go

declare @userName varchar(25)
select @userName = 'seed'

insert into [HumanResources].[Employee]
 values ('John', null, 'Doe', getdate(), @userName, getdate(), null, null, null)

insert into [Production].[ProductCategory]
 values ('PS4 Games', @userName, getdate(), null, null, null)

insert into [Production].[Product]
 values ('King of Fighters XIV', 1, 59.99, 'KOF XIV', 0, @userName, getdate(), null, null, null)
insert into [Production].[Product]
 values ('Street Fighter V', 1, 49.99, 'SF V', 0, @userName, getdate(), null, null, null)
insert into [Production].[Product]
 values ('Guilty Gear', 1, 39.99, 'GG', 0, @userName, getdate(), null, null, null)

insert into [Production].[ProductInventory]
 values (1, 100000, 100000, @userName, getdate(), null, null, null)
insert into [Production].[ProductInventory]
 values (2, 100000, 100000, @userName, getdate(), null, null, null)

insert into [Sales].[Customer]
 values ('Best Buy', 'Colleen Dunn', @userName, getdate(), null, null, null)
insert into [Sales].[Customer]
 values ('Circuit City', 'Bill McCorey', @userName, getdate(), null, null, null)
insert into [Sales].[Customer]
 values ('Game Stop', 'Michael Cooper', @userName, getdate(), null, null, null)

insert into [Sales].[Shipper]
 values ('DHL', 'Ricardo A. Bartra', @userName, getdate(), null, null, null)
insert into [Sales].[Shipper]
 values ('FedEx', 'Rob Carter', @userName, getdate(), null, null, null)
insert into [Sales].[Shipper]
 values ('UPS', 'Juan R. Perez', @userName, getdate(), null, null, null)

insert into [Sales].[OrderStatus]
 values (100, 'Created', @userName, getdate(), null, null, null)
go

Step 02 – Create Project

Create a console application for DotNet Core, in some cases you can add one project to your existing solution but with some name or sufix that indicates it’s a project for code generation, for example: Store.CatFactory.

Step 03 – Add Packages for Project

Visual Studio 2015

Add the following packages in project.json file:

Name Version Description
CatFactory.EfCore 2.0.0-alpha-build00 Provides code generation for EF Core

Save all changes and build the project.

Visual Studio 2017

Add package with Nuget Package Manager for your console project:

Visual Studio 2017 Nuget Package Manager


Save chages and build project.

Do you want to generate another code that not is include in this guide? You can write your own code builder for CatFactory, maybe the next month I write a guide about howto write a code builder for CatFactory.

Step 03 – Add Code for Generation

Once we have the packages installed on our project, we can add code in Main method:


var db = SqlServerDatabaseFactory
 .Import("server=(local);database=Store;integrated security=yes;", "dbo.sysdiagrams");


var project = new EfCoreProject
{
 Name = "Store",
 Database = db,
 OutputDirectory = "C:\\Temp\\Store"
};


project.Settings.AuditEntity = new AuditEntity("CreationUser", "CreationDateTime", "LastUpdateUser", "LastUpdateDateTime");


project.Settings.ConcurrencyToken = "Timestamp";


project.Settings.EntitiesWithDataContracts.Add("Sales.Order");


project.BuildFeatures();


project
 .GenerateEntityLayer()
 .GenerateDataLayer();
Extension Methods for EfCoreProject Instance
Name Description
GenerateEntityLayer Generate code for entities from tables and views definitions
GenerateDataLayer Generates code for data access: AppSettings, DbContext, Mapping, Contracts and Repositories

Additionally, there are more settings for EF Core project instance, we’ll take a look on those settings:

Name Default Value Description
UseAutomaticPropertiesForEntities true Indicates if entities classes will use automatic properties or not, if value is false, the entity will contains private fields
UseBackingFields false Enables the using of fields for change tracking in entities instances materialization
EnableDataBindings false Implements INotifyPropertyChanged property and add properties with fields for class definition
UseDataAnnotations false Indicates if mapping in EF Core it will be with data annotations
UseMefForEntitiesMapping true Add dynamic loadings of mappings for entities using MEF (Export attribute for each mapping)
DeclareDbSetPropertiesInDbContext false Indicates if DbContext class definition must to contains declaration of DbSet
DeclareNavigationPropertiesAsVirtual false Indicates if navigation propeties must to declare as virtual
NavigationPropertyEnumerableNamespace System.Collections.ObjectModel Sets the namespace for navigation properties types
NavigationPropertyEnumerableType Collection Sets the type for collection navigation properties
ConcurrencyToken   Sets the column name that respresents the concurrency token
EntityInterfaceName IEntity Sets the name for entity interface
AuditEntity   Sets the names for audit column: creation and last update (user name and date)
GenerateTestsForRepositories false Indicates if CatFactory must to generate the unit tests for repositories (Not implemented yet)
EntitiesWithDataContracts   Sets the list of entities must to generate as data transfer objects in repositories

If we need to change the namespaces, we can set the values for output namespaces inside of project instance:

Namespace Default Value Description
Entity Layer EntityLayer Gets or sets the namespace for entity layer
Data Layer DataLayer Gets or sets the namespace for data layer
Mapping Mapping Gets or sets the namespace for mapping in data layer
Contracts Contracts Gets or sets the namespace for contracts in data layer
Data Contracts DataContracts Gets or sets the namespace for data contracts in data layer
Repositories Repositories Gets or sets the namespace for repositories in data layer

We’ll review some the generated code for one entity to understand this design:

Code for Order class:

using System;
using System.Collections.ObjectModel;

namespace Store.EntityLayer
{
 public class Order : IAuditEntity
 {
 public Order()
 {
 }

 public Int32? OrderID { get; set; }

 public Int16? OrderStatusID { get; set; }

 public DateTime? OrderDate { get; set; }

 public Int32? CustomerID { get; set; }

 public Int32? EmployeeID { get; set; }

 public Int32? ShipperID { get; set; }

 public Decimal? Total { get; set; }

 public String Comments { get; set; }

 public String CreationUser { get; set; }

 public DateTime? CreationDateTime { get; set; }

 public String LastUpdateUser { get; set; }

 public DateTime? LastUpdateDateTime { get; set; }

 public Byte[] Timestamp { get; set; }

 public Customer CustomerFk { get; set; }

 public Employee EmployeeFk { get; set; }

 public Shipper ShipperFk { get; set; }

 public Collection<OrderDetail> OrderDetails { get; set; }
 }
}

Code for OrderMap class:

using System.Composition;
using Microsoft.EntityFrameworkCore;
using Store.EntityLayer;

namespace Store.DataLayer.Mapping
{
 [Export(typeof(IEntityMap))]
 public class OrderMap : IEntityMap
 {
 public void Map(ModelBuilder modelBuilder)
 {
 var entity = modelBuilder.Entity<Order>();
 
 entity.ToTable("Order", "Sales");
 
 entity.HasKey(p => p.OrderID);
 
 entity.Property(p => p.OrderID).UseSqlServerIdentityColumn();
 
 entity
 .HasOne(p => p.CustomerFk)
 .WithMany(b => b.Orders)
 .HasForeignKey(p => p.CustomerID)
 .HasConstraintName("fk_Order_CustomerID_Customer");
 
 entity
 .HasOne(p => p.EmployeeFk)
 .WithMany(b => b.Orders)
 .HasForeignKey(p => p.EmployeeID)
 .HasConstraintName("fk_Order_EmployeeID_Employee");
 
 entity
 .HasOne(p => p.ShipperFk)
 .WithMany(b => b.Orders)
 .HasForeignKey(p => p.ShipperID)
 .HasConstraintName("fk_Order_ShipperID_Shipper");
 
 entity.Property(p => p.OrderStatusID).HasColumnType("smallint").IsRequired();
 
 entity.Property(p => p.OrderDate).HasColumnType("datetime").IsRequired();
 
 entity.Property(p => p.CustomerID).HasColumnType("int").IsRequired();
 
 entity.Property(p => p.EmployeeID).HasColumnType("int").IsRequired();
 
 entity.Property(p => p.ShipperID).HasColumnType("int").IsRequired();
 
 entity.Property(p => p.Total).HasColumnType("decimal(12, 4)").IsRequired();
 
 entity.Property(p => p.Comments).HasColumnType("varchar(255)");
 
 entity.Property(p => p.CreationUser).HasColumnType("varchar(25)").IsRequired();
 
 entity.Property(p => p.CreationDateTime).HasColumnType("datetime").IsRequired();
 
 entity.Property(p => p.LastUpdateUser).HasColumnType("varchar(25)");
 
 entity.Property(p => p.LastUpdateDateTime).HasColumnType("datetime");
 
 entity
 .Property(p => p.Timestamp)
 .ValueGeneratedOnAddOrUpdate()
 .IsConcurrencyToken();
 }
 }
}

Code for ISalesRepository interface:

using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Store.DataLayer.DataContracts;
using Store.EntityLayer;
using Store.DataLayer.Contracts;

namespace Store.DataLayer.Contracts
{
 public interface ISalesRepository : IRepository
 {
 IQueryable<Customer> GetCustomers(Int32 pageSize = 10, Int32 pageNumber = 0);

 Task<Customer> GetCustomerByCompanyNameAsync(Customer entity);

 Task<Customer> GetCustomerAsync(Customer entity);

 Task<Int32> AddCustomerAsync(Customer entity);

 Task<Int32> UpdateCustomerAsync(Customer changes);

 Task<Int32> RemoveCustomerAsync(Customer entity);

 IQueryable<Shipper> GetShippers(Int32 pageSize = 10, Int32 pageNumber = 0);

 Task<Shipper> GetShipperByCompanyNameAsync(Shipper entity);

 Task<Shipper> GetShipperAsync(Shipper entity);

 Task<Int32> AddShipperAsync(Shipper entity);

 Task<Int32> UpdateShipperAsync(Shipper changes);

 Task<Int32> RemoveShipperAsync(Shipper entity);

 IQueryable<OrderDataContract> GetOrders(Int32 pageSize = 10, Int32 pageNumber = 0, Int32? customerID = null, Int32? employeeID = null, Int32? shipperID = null);

 Task<Order> GetOrderAsync(Order entity);

 Task<Int32> AddOrderAsync(Order entity);

 Task<Int32> UpdateOrderAsync(Order changes);

 Task<Int32> RemoveOrderAsync(Order entity);

 IQueryable<OrderDetail> GetOrderDetails(Int32 pageSize = 10, Int32 pageNumber = 0, Int32? orderID = null, Int32? productID = null);

 Task<OrderDetail> GetOrderDetailAsync(OrderDetail entity);

 Task<Int32> AddOrderDetailAsync(OrderDetail entity);

 Task<Int32> UpdateOrderDetailAsync(OrderDetail changes);

 Task<Int32> RemoveOrderDetailAsync(OrderDetail entity);
 }
}

Code for OrderDataContract class:

using System;

namespace Store.DataLayer.DataContracts
{
 public class OrderDataContract
 {
 public Int32? OrderID { get; set; }

 public Int16? OrderStatusID { get; set; }

 public DateTime? OrderDate { get; set; }

 public Int32? CustomerID { get; set; }

 public Int32? EmployeeID { get; set; }

 public Int32? ShipperID { get; set; }

 public Decimal? Total { get; set; }

 public String Comments { get; set; }

 public String CreationUser { get; set; }

 public DateTime? CreationDateTime { get; set; }

 public String LastUpdateUser { get; set; }

 public DateTime? LastUpdateDateTime { get; set; }

 public Byte[] Timestamp { get; set; }

 public String CustomerCompanyName { get; set; }

 public String CustomerContactName { get; set; }

 public String EmployeeFirstName { get; set; }

 public String EmployeeMiddleName { get; set; }

 public String EmployeeLastName { get; set; }

 public DateTime? EmployeeBirthDate { get; set; }

 public String ShipperCompanyName { get; set; }

 public String ShipperContactName { get; set; }
 }
}

Code for Repository class:

using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Store.EntityLayer;

namespace Store.DataLayer.Contracts
{
 public class Repository
 {
 protected Boolean Disposed;
 protected StoreDbContext DbContext;

 public Repository(StoreDbContext dbContext)
 {
 DbContext = dbContext;
 }

 public void Dispose()
 {
 if (!Disposed)
 {
 if (DbContext != null)
 {
 DbContext.Dispose();
 
 Disposed = true;
 }
 }
 }

 protected virtual void Add<TEntity>(TEntity entity) where TEntity : class
 {
 var cast = entity as IAuditEntity;
 
 if (cast != null)
 {
 if (!cast.CreationDateTime.HasValue)
 {
 cast.CreationDateTime = DateTime.Now;
 }
 }
 
 var entry = DbContext.Entry(entity);
 
 if (entry.State != EntityState.Detached)
 {
 entry.State = EntityState.Added;
 }
 else
 {
 var dbSet = DbContext.Set<TEntity>();
 
 dbSet.Add(entity);
 }
 }

 protected virtual void Update<TEntity>(TEntity entity, DbSet<TEntity> dbSet = null) where TEntity : class
 {
 var cast = entity as IAuditEntity;
 
 if (cast != null)
 {
 if (!cast.LastUpdateDateTime.HasValue)
 {
 cast.LastUpdateDateTime = DateTime.Now;
 }
 }
 
 var entry = DbContext.Entry(entity);
 
 if (entry.State == EntityState.Detached)
 {
 dbSet?.Attach(entity);
 }
 
 entry.State = EntityState.Modified;
 }

 protected virtual void Remove<TEntity>(TEntity entity) where TEntity : class
 {
 var dbSet = DbContext.Set<TEntity>();
 
 var entry = DbContext.Entry(entity);
 
 if (entry.State == EntityState.Deleted)
 {
 dbSet.Attach(entity);
 dbSet.Remove(entity);
 }
 else
 {
 entry.State = EntityState.Deleted;
 }
 }

 public Int32 CommitChanges()
 => DbContext.SaveChanges();

 public Task<Int32> CommitChangesAsync()
 => DbContext.SaveChangesAsync();
 }
}

Code for SalesRepository class:

using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Store.DataLayer.DataContracts;
using Store.EntityLayer;
using Store.DataLayer.Contracts;

namespace Store.DataLayer.Repositories
{
 public class SalesRepository : Repository, ISalesRepository
 {
 public SalesRepository(StoreDbContext dbContext)
 : base(dbContext)
 {
 }

 public IQueryable<Customer> GetCustomers(Int32 pageSize = 10, Int32 pageNumber = 0)
 {
 var query = DbContext.Set<Customer>().AsQueryable();
 
 return Paging<Customer>(pageSize, pageNumber);
 }

 public async Task<Customer> GetCustomerByCompanyNameAsync(Customer entity)
 => await DbContext.Set<Customer>().FirstOrDefaultAsync(item => item.CompanyName == entity.CompanyName);

 public async Task<Customer> GetCustomerAsync(Customer entity)
 => await DbContext.Set<Customer>().FirstOrDefaultAsync(item => item.CustomerID == entity.CustomerID);

 public async Task<Int32> AddCustomerAsync(Customer entity)
 {
 Add(entity);
 
 return await CommitChangesAsync();
 }

 public async Task<Int32> UpdateCustomerAsync(Customer changes)
 {
 Update(changes);
 
 return await CommitChangesAsync();
 }

 public async Task<Int32> RemoveCustomerAsync(Customer entity)
 {
 Remove(entity);
 
 return await CommitChangesAsync();
 }

 public IQueryable<Shipper> GetShippers(Int32 pageSize = 10, Int32 pageNumber = 0)
 {
 var query = DbContext.Set<Shipper>().AsQueryable();
 
 return Paging<Shipper>(pageSize, pageNumber);
 }

 public async Task<Shipper> GetShipperByCompanyNameAsync(Shipper entity)
 => await DbContext.Set<Shipper>().FirstOrDefaultAsync(item => item.CompanyName == entity.CompanyName);

 public async Task<Shipper> GetShipperAsync(Shipper entity)
 => await DbContext.Set<Shipper>().FirstOrDefaultAsync(item => item.ShipperID == entity.ShipperID);

 public async Task<Int32> AddShipperAsync(Shipper entity)
 {
 Add(entity);
 
 return await CommitChangesAsync();
 }

 public async Task<Int32> UpdateShipperAsync(Shipper changes)
 {
 Update(changes);
 
 return await CommitChangesAsync();
 }

 public async Task<Int32> RemoveShipperAsync(Shipper entity)
 {
 Remove(entity);
 
 return await CommitChangesAsync();
 }

 public IQueryable<OrderDataContract> GetOrders(Int32 pageSize = 10, Int32 pageNumber = 0, Int32? customerID = null, Int32? employeeID = null, Int32? shipperID = null)
 {
 var query = from order in DbContext.Set<Order>()
 join customer in DbContext.Set<Customer>() on order.CustomerID equals customer.CustomerID
 join employee in DbContext.Set<Employee>() on order.EmployeeID equals employee.EmployeeID
 join shipper in DbContext.Set<Shipper>() on order.ShipperID equals shipper.ShipperID
 select new OrderDataContract
 {
 OrderID = order.OrderID,
 OrderStatusID = order.OrderStatusID,
 OrderDate = order.OrderDate,
 CustomerID = order.CustomerID,
 EmployeeID = order.EmployeeID,
 ShipperID = order.ShipperID,
 Total = order.Total,
 Comments = order.Comments,
 CreationUser = order.CreationUser,
 CreationDateTime = order.CreationDateTime,
 LastUpdateUser = order.LastUpdateUser,
 LastUpdateDateTime = order.LastUpdateDateTime,
 Timestamp = order.Timestamp,
 CustomerCompanyName = customer.CompanyName,
 CustomerContactName = customer.ContactName,
 EmployeeFirstName = employee.FirstName,
 EmployeeMiddleName = employee.MiddleName,
 EmployeeLastName = employee.LastName,
 EmployeeBirthDate = employee.BirthDate,
 ShipperCompanyName = shipper.CompanyName,
 ShipperContactName = shipper.ContactName
 };
 
 if (customerID.HasValue)
 {
 query = query.Where(item => item.CustomerID == customerID);
 }
 
 if (employeeID.HasValue)
 {
 query = query.Where(item => item.EmployeeID == employeeID);
 }
 
 if (shipperID.HasValue)
 {
 query = query.Where(item => item.ShipperID == shipperID);
 }
 
 return Paging(query, pageSize, pageNumber);
 }

 public async Task<Order> GetOrderAsync(Order entity)
 => await DbContext.Set<Order>().FirstOrDefaultAsync(item => item.OrderID == entity.OrderID);

 public async Task<Int32> AddOrderAsync(Order entity)
 {
 Add(entity);
 
 return await CommitChangesAsync();
 }

 public async Task<Int32> UpdateOrderAsync(Order changes)
 {
 Update(changes);
 
 return await CommitChangesAsync();
 }

 public async Task<Int32> RemoveOrderAsync(Order entity)
 {
 Remove(entity);
 
 return await CommitChangesAsync();
 }

 public IQueryable<OrderDetail> GetOrderDetails(Int32 pageSize = 10, Int32 pageNumber = 0, Int32? orderID = null, Int32? productID = null)
 {
 var query = DbContext.Set<OrderDetail>().AsQueryable();
 
 if (orderID.HasValue)
 {
 query = query.Where(item => item.OrderID == orderID);
 }
 
 if (productID.HasValue)
 {
 query = query.Where(item => item.ProductID == productID);
 }
 
 return Paging(query, pageSize, pageNumber);
 }

 public async Task<OrderDetail> GetOrderDetailAsync(OrderDetail entity)
 => await DbContext.Set<OrderDetail>().FirstOrDefaultAsync(item => item.OrderDetailID == entity.OrderDetailID);

 public async Task<Int32> AddOrderDetailAsync(OrderDetail entity)
 {
 Add(entity);
 
 return await CommitChangesAsync();
 }

 public async Task<Int32> UpdateOrderDetailAsync(OrderDetail changes)
 {
 Update(changes);
 
 return await CommitChangesAsync();
 }

 public async Task<Int32> RemoveOrderDetailAsync(OrderDetail entity)
 {
 Remove(entity);
 
 return await CommitChangesAsync();
 }
 }
}

AuditEntity in settings sets the columns for audit, this version of CatFactory supports creation and last update for audit.

ConcurrencyToken sets the column for concurrency, this value will be use in entity’s mapping.

Don’t forget, the previous settings are about columns, we need to use the name of columns not the properties.

EntitiesWithDataContracts is the list of entities that will be generated with joins in linq, this means CatFactory engine reads all foreign keys and create a data contract to retrieve information, if we take a look on GetOrders method on SalesRepository, we can see a linq query with data contract and not a lambda expression as GetShippers method

EnableDataBindings this flag implements INotifyPropertyChanged in all entities that represent tables

UseMefForEntitiesMapping enables the use of MEF for loading entities mapping, this means all mapping will loading with reverse engineering

Please take a look on all operations in repositories, all are async operations.

Step 04 – Create Console Project

Now we can go to output directory and create a console project for DotNet Core and add the following code to Program class:

using System;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Store.DataLayer;
using Store.DataLayer.Contracts;
using Store.DataLayer.Mapping;
using Store.DataLayer.Repositories;
using Store.EntityLayer;

namespace ConsoleApplication
{
 public class Program
 {
 public static void Main(String[] args)
 {
 var task = new Task(ShowDataAsync);

 task.Start();

 task.Wait();

 Console.ReadLine();
 }

 public static async void ShowDataAsync()
 {
 var appSettings = Options.Create(new AppSettings
 {
 ConnectionString = "server=(local);database=Store;integrated security=yes;"
 });

 var entityMapper = new StoreEntityMapper() as IEntityMapper;

 var dbContext = new StoreDbContext(appSettings, entityMapper);

 using (var repository = new ProductionRepository(dbContext) as IProductionRepository)
 {
 var products = await repository.GetProducts().ToListAsync();

 Console.WriteLine("Products:");

 foreach (var item in products)
 {
 Console.WriteLine(" - {0}", item.ProductName);
 }
 }
 }
 }
}

Don’t forget to add the packages for Microsoft.Composition and Microsoft.EntityFrameworkCore in project.json file:

Name Version
Microsoft.Composition 1.0.30
Microsoft.EntityFrameworkCore 1.1.1
Microsoft.EntityFrameworkCore.SqlServer 1.1.1

Don’t forget that package Microsoft.Composition applies only if we generate mappings for MEF!

Console Output:

Project Store (.NETCoreApp,Version=v1.0) was previously compiled. Skipping compilation.
Products:
 - King of Fighters XIV
 - Street Fighter V
 - Guilty Gear

Obviously we add this simple code to test generated code doesn’t have errors, if we use the generated code in Web API project, we’ll need to add dependency injection and anothers things, please check in links list at the end of this post for more information.

How works all code together?

We create a StoreDbContext instance, that instance use the connection string from AppSettings and inside of OnModelCreating method there is a call of MapEntities method for EntityMapper instance, this is code in that way because it’s more a stylish way to mapping entities instead of add a lot of lines inside of OnModelCreating

Later, for example we create an instance of SalesRepository passing a valid instance of StoreDbContext and then we can access to repository’s operations.

For this architecture implementation we are using the DotNet naming conventions: PascalCase for classes, interfaces and methods; camelCase for parameters.

Namespaces for generated code:

  1. EntityLayer
  2. DataLayer
  3. DataLayer\Contracts
  4. DataLayer\DataContracts
  5. DataLayer\Mapping
  6. DataLayer\Repositories

Inside of EntityLayer we’ll place all entities, in this context entity means a class that represents a table or view from database, sometimes entity is named POCO (Plain Old Common language runtime Object) than means a class with only properties not methods nor other things (events)

Inside of DataLayer we’ll place DbContext and AppSettings because they’re common classes for DataLayer

Inside of DataLayer\Contracts we’ll place all interfaces that represent operations catalog, we’re focusing on schemas and we’ll create one interface per schema and Store contract for default schema (dbo)

Inside of DataLayer\DataContracts we’ll place all object definitions for returned values from Contracts namespace, for now this directory would be empty

Inside of DataLayer\Mapping we’ll place all object definition related to mapping a class for database access

Inside of DataLayer\Repositories we’ll place the implementations for Contracts definitons

Inside of EntityLayer and DataLayer\Mapping we’ll create one directory per schema without include the default schema.

We can review the link about EF Core for enterprise, and we can understand this guide allow to us generate all of that code to reduce time in code writing.

You don’t want to generate contracts and repositories? Simple, remove those code lines and generate only POCOs, Mappings, AppSettings and DbContext.

The features that are not supported yet will add in incoming versions of CatFactory.

Code Improvements

CatFactory Features
Layer Feature Supported
Database SQL Server Database import Yes
Entity layer POCOs generation for tables and views Yes
Entity layer Addition of Navigation properties for entities Yes
Data layer Data annotations for POCOs Yes
Data layer DbContext generation Yes
Data layer DbSet properties in DbContext Yes
Data layer Mappings generation Yes
Data layer Contracts generation Yes
Data layer Add invocations for stored procedures Not yet
Data layer Repositories generation Yes
Data layer Async operations for Repositories Yes
Business layer Business objects generation Not yet
UI layer ViewModels generation Yes
QA Unit tests generation Not yet

I’m using a sample database with name Store, but you need to change the connection string to database of your choice and set another values for project name and output directory, please make sure that output directory exists and you have permissions to write on that directory, please make sure about that point to avoid common errors.

How to Use CatFactory Framework

We need to understand the scope for CatFactory, in few words CatFactory is the core for code generation, if we want to have more packages we can create them with this naming convention: CatFactory.PackageName.

As we can see above the basic flow for existing database on CatFactory is this:

  1. Set connection string
  2. Import database
  3. Create instance of project
  4. Build features
  5. Read all tables and views for database and create instances for code builders

We’ll work on common tasks with CatFactory, these will apply once we have a database imported from database server.

Read all tables in database

foreach (var table in db.Tables)
{ var fullName = table.FullName; var columnsCount = table.Columns.Count;
}

Read all columns in table

foreach (var column in table.Columns)
{ var columnName = column.Name;
}

Validate if table has primary key

if (table.PrimaryKey != null)
{ var key = table.Primary.Key;
}

Validate if table has identity (Auto increment)

if (table.Identity != null)
{ var name = table.Identity.Name;
}

Get all columns without key from table

foreach (var column in table.GetColumnsWithOutKey())
{ var name = column.Name;
}

Get .NET equivalent type from column type

var resolver = new ClrTypeResolver() as ITypeResolver; foreach (var column in table.Columns)
{ var clrType = resolver.Resolve(column.Type);
}

Points of Interest

  • You can develope your own code template for CatFactory, now we focus on EF Core but in future there will be Web API, Unit Tests and other things 🙂

Related Links

Code Improvements

  • Addition of selection for objects, for example enable set a range for database import, select 7 tables from 100 existing tables in database
  • Addition of DTO for single entities
  • Addition of exclusions for columns on insert and update
  • Addition of TDD generation
  • Enable command line for code generation (I don’t know how to implement this but if you know about this please let me know in comments :))

History

  • 12th December, 2016: Initial version
  • 16th December, 2016: Addition of script for database
  • 30th January, 2017: Addition of async operations
  • 12th March, 2017: Addition of audit entity, concurrency token and entities with data contracts
  • 4th June, 2017: Addition of backing fields, data bindings and using of MEF for loading entities mapping

LEAVE A REPLY