Click here to Skip to main content
14,238,068 members

Scaffolding Entity Framework Core 2 with CatFactory

Rate this:
4.85 (37 votes)
Please Sign up or sign in to vote.
4.85 (37 votes)
25 May 2019CPOL
Scaffolding Entity Framework Core 2 with CatFactory

Introduction

What is CatFactory?

CatFactory is a scaffolding engine for .NET Core built with C#.

How does it Works?

The concept behind CatFactory is to import an existing database from SQL Server instance and then to scaffold a target technology.

We can also replace the database from SQL Server instance with an in-memory database.

The flow to import an existing database is:

  1. Create Database Factory
  2. Import Database
  3. Create instance of Project (Entity Framework Core, Dapper, etc)
  4. Build Features (One feature per schema)
  5. Scaffold objects, these methods read all objects from database and create instances for code builders

Currently, the following technologies are supported:

This package is the core for child packages, additional packages have created with this naming convention: CatFactory.PackageName.

  • CatFactory.SqlServer
  • CatFactory.NetCore
  • CatFactory.EntityFrameworkCore
  • CatFactory.AspNetCore
  • CatFactory.Dapper

Concepts Behind CatFactory

Database Type Map

One of things I don't like to get equivalent between SQL data type for CLR is use magic strings, after of review the more "fancy" way to resolve a type equivalence is to have a class that allows to know the equivalence between SQL data type and CLR type.

Using this table as reference, now CatFactory has a class with name DatabaseTypeMap. Database class contains a property with all mappings with name Mappings, so this property is filled by Import feature for SQL Server package.

Class Definition:

namespace CatFactory.Mapping
{
    public class DatabaseTypeMap
    {
        public string DatabaseType { get; set; }
        
        public bool AllowsLengthInDeclaration { get; set; }
        
        public bool AllowsPrecInDeclaration { get; set; }
        
        public bool AllowsScaleInDeclaration { get; set; }
        
        public string ClrFullNameType { get; set; }
        
        public bool HasClrFullNameType { get; }
        
        public string ClrAliasType { get; set; }
        
        public bool HasClrAliasType { get; }
        
        public bool AllowClrNullable { get; set; }
        
        public DbType DbTypeEnum { get; set; }
        
        public bool IsUserDefined { get; set; }
        
        public string ParentDatabaseType { get; set; }
        
        public string Collation { get; set; }
    }
}

Code Sample:

// Get mappings
var mappings = database.DatabaseTypeMaps;

// Resolve CLR type
var mapsForString = mappings.Where(item => item.ClrType == typeof(string)).ToList();

// Resolve SQL Server type
var mapForVarchar = mappings.FirstOrDefault(item => item.DatabaseType == "varchar");

Project Selection

A project selection is a limit to apply settings for objects match with pattern.

GlobalSelection is the default selection for project, contains a default instance of settings.

Patterns:

Pattern Scope
Sales.Order Applies for specific object with name Sales.Order
Sales.* Applies for all objects inside of Sales schema
*.Order Applies for all objects with name Order with no matter schema
*.* Applies for all objects, this is the global selection

Code Sample:

// Apply settings for Project
project.GlobalSelection(settings =>
{
    settings.ForceOverwrite = true;
    settings.AuditEntity = new AuditEntity("CreationUser", "CreationDateTime", "LastUpdateUser", "LastUpdateDateTime");
    settings.ConcurrencyToken = "Timestamp";
});

// Apply settings for specific object
project.Select("Sales.Order", settings =>
{
    settings.EntitiesWithDataContracts = true;
});

Event Handlers to Scaffold

In order to provide a more flexible way in scaffolding, there are two delegates in CatFactory, one to perform an action before of scaffolding and another one to handle and action after of scaffolding.

Code Sample:

// Add event handlers to before and after of scaffold

project.ScaffoldingDefinition += (source, args) =>
{
    // Add code to perform operations with code builder instance before to create code file
};

project.ScaffoldedDefinition += (source, args) =>
{
    // Add code to perform operations after of create code file
};

Workshop

One of things I don't like about dapper is to have all definitions for queries in strings or string builders... I prefer to have an object that builds queries and in that way reduce code lines quantity but I don't know if that concept breaks Dapper philosophy, I really weant to know about that because scaffold high quality code is fundamental in CatFactory, so I don't want to add an implementation that breaks the main concept behind an ORM...

Anyway I have invested some time to research about how can I solve query building for repositories and I have no found any object that allows to build a query from object's definition, there are frameworks that provide CRUD functions but something like LINQ there isn't, as I know of course if I wrong about this point please let me know in comments.

So I'll provide a draft for query building and take your time to let me know your feedback and then please answer 2 questions:

  1. This implementation breaks Dapper concept?
  2. What do you think about to have metadata for entities in Dapper?

Query Builder Draft

Select all

var query = QueryBuilder
    .Select<Shipper>();

// Output:
// select [ShipperID], [CompanyName], [Phone] from [dbo].[Shipper]

Select by key

var query = QueryBuilder
    .Select<Shipper>()
    .Where("ShipperID", QueryOperator.Equals, 1);

// Output:
// select [ShipperID], [CompanyName], [Phone] from [dbo].[Shipper] where [ShipperID] = 1

Insert

var query = QueryBuilder
    .Insert<Shipper>(identity: "ShipperID");

// Output:
// insert into [dbo].[Shipper] ([CompanyName], [Phone]) values (@companyName, @phone)
// select @shipperID = @@identity

Update

var query = QueryBuilder
    .Update<Shipper>(key: new string[] { "ShipperID" });

// Output:
// update [dbo].[Shipper] set [CompanyName] = @companyName, [Phone] = @phone where [ShipperID] = @shipperID

Delete

var query = QueryBuilder
    .Delete<Shipper>(key: new string[] { "ShipperID" });

// Output:
// delete from [dbo].[Shipper] where [ShipperID] = @shipperID

Select by

// Search by
var query = QueryBuilder
    .Select<Shipper>()
    .Where("CompanyName", QueryOperator.Like, "%a%")
    .And("Phone", QueryOperator.Like, "%a%");

// Output:
// select [ShipperID], [CompanyName], [Phone] from [Shipper] where [CompanyName] like '%a%' and [Phone] like '%a%'

Shipper is an entity for this example, I have found the following issues with this solution:

  • There isn't information for schema (e.g. dbo, Production, Purchasing, Sales)
  • There isn't a way to know if one table with name "Order Details" is mapped to entity with Name OrderDetail

The above points can be solve if there is any information for table and entity (C# class), something like metadata, we can have an interface with name IEntity like this:

public interface IEntity
{
	Table ToTable();
}

Then create a class with name Shipper and implement interface:

public class Shipper : IEntity
{
 public int? ShipperID { get; set; }
 
 public string CompanyName { get; set; }
 
 public string Phone { get; set; }
 
 public Table ToTable()
  => new Table
  {
   Schema = "dbo",
   Name = "Shipper",
   Identity = new Identity("ShipperID", 1, 1),
   PrimaryKey = new PrimaryKey("ShipperID")
   Columns = new List<Column>
   {
    new Column
    {
     Name = "ShipperID",
     Type = "int"
    },
    new Column
    {
     Name = "CompanyName",
     Type = "varchar",
     Lenght = 50
    },
    new Column
    {
     Name = "Phone",
     Type = "varchar",
     Length = 25
    }
   }
  };
 }
}

In that way we can have all "metadata" for all entities and get that definitions to build queries in dynamic way, so we can reduce code lines in our repositories.

The definition for Table, Columns, Identity and PrimaryKey already exists in CatFactory, so we can reuse those definitions for this purpose :)

Please let me know what do you think about this implementation, make sense?

According to feedback from developers and to provide a better experience for users, I'm working on some improvements to get a more clean way to work with CatFactory:

Working with database

// Import from existing database
var database = SqlServerDatabaseFactory.Import("YourConnectionStringHere");

// Read all tables
foreach (var table in database.Tables)
{
    // Check primary key on table's definition
    if (table.PrimaryKey == null)
    {
        continue;
    }
    
    // Check identity on table's definition
    if (table.Identity != null)
    {
        var identityName = table.Identity.Name;
    }
    
    // Read all columns
    foreach (var column in table.Columns)
    {
        // Get equivalent CLR type from column type
        var clrType = database.ResolveType(column).GetClrType();
    }
}

Packages

  • CatFactory
  • CatFactory.SqlServer
  • CatFactory.NetCore
  • CatFactory.EntityFrameworkCore
  • CatFactory.AspNetCore
  • CatFactory.Dapper
  • CatFactory.TypeScript

You can check the download statistics for CatFactory packages in NuGet Gallery.

Background

Generate code is a common task in software developer, the most of developers write a "code generator" in their lives.

Using Entity Framework 6.x, I worked with EF wizard and it's a great tool even with limitations like:

  • Not scaffolding for Fluent API
  • Not scaffolding for Repositories
  • Not scaffolding for Unit of Work
  • Custom scaffolding is so complex or in some cases impossible

With Entity Framework Core I worked with command line to scaffold from existing database, EF Core team provided a great tool with command line but there are still the same limitations above.

So, CatFactory pretends to solve those limitations and provide a simple way to scaffold Entity Framework Core.

StringBuilder it was used to scaffold a class or interface in older versions of CatFactory but some years ago there was a change about how to scaffold a definition (class or interface), CatFactory allows to define the structure for class or interface in a simple and clear way, then use an instance of CodeBuilder to scaffold in C#.

Lets start with scaffold a class in C#:

var definition = new CSharpClassDefinition
{
    Namespace = "OnlineStore.DomainDrivenDesign",
    AccessModifier = AccessModifier.Public,
    Name = "StockItem",
    Properties =
    {
        new PropertyDefinition(AccessModifier.Public, "string", "GivenName")
        {
            IsAutomatic = true
        },
        new PropertyDefinition(AccessModifier.Public, "string", "MiddleName")
        {
            IsAutomatic = true
        },
        new PropertyDefinition(AccessModifier.Public, "string", "Surname")
        {
            IsAutomatic = true
        },
        new PropertyDefinition(AccessModifier.Public, "string", "FullName")
        {
            IsReadOnly = true,
            GetBody =
            {
                new CodeLine(" return GivenName + (string.IsNullOrEmpty(MiddleName) ? \"\" : \" \" + MiddleName) + \" \" + Surname)")
            }
        }
    }
};

CSharpCodeBuilder.CreateFiles("C:\\Temp", string.Empty, true, definition);

This is the output code:

namespace OnlineStore.DomainDrivenDesign
{
	public class StockItem
	{
		public string GivenName { get; set; }

		public string MiddleName { get; set; }

		public string Surname { get; set; }

		public string FullName
			=> GivenName + (string.IsNullOrEmpty(MiddleName) ? "" : " " + MiddleName) + " " + Surname;

	}
}

To create an object definition like class or interface, these types can be use:

  • EventDefinition
  • FieldDefinition
  • ClassConstructorDefinition
  • FinalizerDefinition
  • IndexerDefinition
  • PropertyDefinition
  • MethodDefinition

Types like ClassConstructorDefinition, FinalizerDefinition, IndexerDefinition, PropertyDefinition and MethodDefinition can have code blocks, these blocks are arrays of ILine.

ILine interface allows to represent a code line inside of code block, there are different types for lines:

  1. CodeLine
  2. CommentLine
  3. EmptyLine
  4. PreprocessorDirectiveLine
  5. ReturnLine
  6. TodoLine

Lets create a class with methods:

var classDefinition = new CSharpClassDefinition
{
 Namespace = "OnlineStore.BusinessLayer",
 AccessModifier = AccessModifier.Public,
 Name = "WarehouseService",
 Fields =
 {
  new FieldDefinition("OnlineStoreDbContext", "DbContext")
  {
   IsReadOnly = true
  }
 },
 Constructors =
 {
  new ClassConstructorDefinition
  {
   AccessModifier = AccessModifier.Public,
   Parameters =
   {
    new ParameterDefinition("OnlineStoreDbContext", "dbContext")
   },
   Lines =
   {
    new CodeLine("DbContext = dbContext;")
   }
  }
 },
 Methods =
 {
  new MethodDefinition
  {
   AccessModifier = AccessModifier.Public,
   Type = "IListResponse<StockItem>",
   Name = "GetStockItems",
   Lines =
   {
    new TodoLine(" Add filters"),
    new CodeLine("return DbContext.StockItems.ToList();")
   }
  }
 }
};

CSharpCodeBuilder.CreateFiles("C:\\Temp", string.Empty, true, definition);

This is the output code:

namespace OnlineStore.BusinessLayer
{
 public class WarehouseService
 {
  private readonly OnlineStoreDbContext DbContext;

  public WarehouseService(OnlineStoreDbContext dbContext)
  {
   DbContext = dbContext;
  }

  public IListResponse<StockItem> GetStockItems()
  {
   // todo:  Add filters
   return DbContext.StockItems.ToList();
  }
 }
}

Now lets refact an interface from class:

var interfaceDefinition = classDefinition.RefactInterface();

CSharpCodeBuilder.CreateFiles(@"C:\Temp", string.Empty, true, interfaceDefinition);

This is the output code:

public interface IWarehouseService
{
	IListResponse<StockItem> GetStockItems();
}

I know some developers can reject this design alleging there is a lot of code to scaffold a simple class with 4 properties but keep in mind CatFactory's way looks like a "clear" transcription of definitions.

CatFactory.NetCore uses the model from CatFactory to allow scaffold C# code, so the question is: What is CatFactory.Dapper package?

Is a package that allows to scaffold Dapper using scaffolding engine provided by CatFactory.

Prerequisites

Skills

  • OOP
  • AOP
  • ORM
  • C#
  • Design Patterns (Repository and Unit of Work)

Software

  • .NET Core
  • Visual Studio 2017 or VS Code
  • Access to existing SQL Server instance

Using the code

Please follow these steps to scaffold Entity Framework Core with CatFactory:

Step 01 - Create sample database

Take a look for sample database to understand each component in architecture. In this database there are 4 schemas: Dbo, HumanResources, Warehouse and Sales.

Each schema represents a division on store company, keep this in mind because all code is designed following this aspect; at this moment this code only implements features for Production and Sales schemas.

All tables have a primary key with one column and have columns for creation, last update and concurrency token.

Tables

Schema Name
dbo ChangeLog
dbo ChangeLogExclusion
dbo Country
dbo CountryCurrency
dbo Currency
dbo EventLog
HumanResources Employee
HumanResources EmployeeAddress
HumanResources EmployeeEmail
Sales Customer
Sales OrderDetail
Sales OrderHeader
Sales OrderStatus
Sales PaymentMethod
Sales Shipper
Warehouse Location
Warehouse Product
Warehouse ProductCategory
Warehouse ProductInventory

You can found the scripts for database in this link: Online Store Database Scripts on GitHub.

Please remember: This is a sample database, only for demonstration of concepts.

Step 02 - Create Project

Create a console application for .NET 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 to scaffold, for example: OnLineStore.CatFactory.EntityFrameworkCore.

Add the following packages for your project:

Name Version Description
CatFactory.SqlServer 1.0.0-beta-sun-build36 Provides import feature for SQL Server databases
CatFactory.EntityFrameworkCore 1.0.0-beta-sun-build34 Provides scaffold for Entity Framework Core

Save all changes and build the project.

Step 03 - Add Code to Scaffold

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

// Create database factory
var databaseFactory = new SqlServerDatabaseFactory
{
    DatabaseImportSettings = new DatabaseImportSettings
    {
        ConnectionString = "server=(local);database=OnlineStore;integrated security=yes;",
        ImportScalarFunctions = true,
        ImportTableFunctions = true,
        Exclusions =
        {
            "dbo.sysdiagrams",
            "dbo.fn_diagramobjects"
        }
    }
};

// Import database
var database = databaseFactory.Import();

// Create instance of Entity Framework Core project
var project = new EntityFrameworkCoreProject
{
    Name = "OnlineStore.Core",
    Database = database,
    OutputDirectory = @"C:\Projects\OnlineStore.Core"
};

// Apply settings for Entity Framework Core project
project.GlobalSelection(settings =>
{
    settings.ForceOverwrite = true;
    settings.ConcurrencyToken = "Timestamp";
    settings.AuditEntity = new AuditEntity
    {
        CreationUserColumnName = "CreationUser",
        CreationDateTimeColumnName = "CreationDateTime",
        LastUpdateUserColumnName = "LastUpdateUser",
        LastUpdateDateTimeColumnName = "LastUpdateDateTime"
    };
});

project.Selection("Sales.OrderHeader", settings => settings.EntitiesWithDataContracts = true);

// Build features for project, group all entities by schema into a feature
project.BuildFeatures();

// Add event handlers to before and after of scaffold

project.ScaffoldingDefinition += (source, args) =>
{
    // Add code to perform operations with code builder instance before to create code file
};

project.ScaffoldedDefinition += (source, args) =>
{
    // Add code to perform operations after of create code file
};

// Scaffolding =^^=
project
    .ScaffoldEntityLayer()
    .ScaffoldDataLayer();
Extension methods for EntityFrameworkCoreProject instance
Name Description
ScaffoldEntityLayer Scaffold code for entities from tables and views
ScaffoldDataLayer Scaffold code for data access: DbContext, Mapping, Contracts, Data Contracts, Repositories and extensions

Step 04 - Create Console Project

The following code shows how to use the scaffolded code, if We want to use the code with ASP.NET Web API project, We'll need to set up dependency injection and anothers things, please check in related links for more information.

Now We can go to output directory and create a console project for .NET Core, select code snippet according to your case:

Get All:

// Create options for DbContext instance
var options = new DbContextOptionsBuilder<OnLineStoreDbContext>()
    .UseSqlServer("server=(local);database=OnLineStore;integrated security=yes;")
    .Options;

// Create DbContext instance
var dbContext = new OnLineStoreDbContext(options);

using (var repository = new SalesRepository(dbContext))
{
    // Get "in memory" query
    var query = repository.GetOrderHeaders();
    
    // Get paging info
    var totalItems = await query.CountAsync();
    var pageSize = 25;
    var pageNumber = 1;
    var pageCount = totalItems / pageSize;
    
    // Retrieve list from database
    var list = await query.Paging(pageSize, pageNumber).ToListAsync();
}

Get by Key:

// Create options for DbContext instance
var options = new DbContextOptionsBuilder<OnlineStoreDbContext>()
    .UseSqlServer("server=(local);database=OnlineStore;integrated security=yes;")
    .Options;

// Create DbContext instance
var dbContext = new OnlineStoreDbContext(options);

using (var repository = new SalesRepository(dbContext))
{
    // Get entity by id
    var entity = await repository.GetOrderHeaderAsync(new OrderHeader(1));
}

Get by Unique (If table contains an unique constraint):

// Create options for DbContext instance
var options = new DbContextOptionsBuilder<OnlineStoreDbContext>()
    .UseSqlServer("server=(local);database=OnlineStore;integrated security=yes;")
    .Options;

// Create DbContext instance
var dbContext = new OnlineStoreDbContext(options);

using (var repository = new ProductionRepository(dbContext))
{
    // Get entity by name
    var entity = await repository.GetProductByProductNameAsync(new Product { ProductName = "The King of Fighters XIV" });
}

Add:

// Create options for DbContext instance
var options = new DbContextOptionsBuilder<OnlineStoreDbContext>()
    .UseSqlServer("server=(local);database=OnlineStore;integrated security=yes;")
    .Options;

// Create DbContext instance
var dbContext = new OnlineStoreDbContext(options);

using (var repository = new SalesRepository(dbContext))
{
    // Create instance for entity
    var entity = new OrderHeader();
    
    // Set values for properties
    // e.g. entity.Total = 29.99m;
    
    // Add entity
    repository.OrderHeaders.Add(entity);
    
    // Save changes in database
    await repository.CommitChangesAsync();
}

Update:

// Create options for DbContext instance
var options = new DbContextOptionsBuilder<OnlineStoreDbContext>()
    .UseSqlServer("server=(local);database=OnlineStore;integrated security=yes;")
    .Options;

// Create DbContext instance
var dbContext = new OnlineStoreDbContext(options);

using (var repository = new SalesRepository(dbContext))
{
    // Retrieve entity by id instance for entity
    var entity = await repository.GetOrderAsync(new OrderHeader(1));
    
    // Set values for properties
    // e.g. entity.Total = 29.99m;
    
    dbContext.OrderHeaders.Update(entity);
    
    // Save changes in database
    await repository.CommitChangesAsync();
}

Remove:

// Create options for DbContext instance
var options = new DbContextOptionsBuilder<OnlineStoreDbContext>()
    .UseSqlServer("server=(local);database=OnlineStore;integrated security=yes;")
    .Options;

// Create DbContext instance
var dbContext = new OnlineStoreDbContext(options);

using (var repository = new SalesRepository(dbContext))
{
    // Retrieve entity by id instance for entity
    var entity = await repository.GetOrderHeaderAsync(new OrderHeader(1));
    
    dbContext.OrderHeaders.Remove(entity);
    
    // Save changes in database
    await repository.CommitChangesAsync();
}

The previous code samples do not include error handling, these are samples to show the way to work with scaffold code.

How works all code together?

We create a OnlineStoreDbContext instance, that instance use the connection string from DbContextOptionsBuilder and inside of OnModelCreating method there is the configuration for all mappings, that's 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 OnlineStoreDbContext 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\Configurations
  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 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\Configurations 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.

Code Review

We'll review some the output code for one entity to understand the design:

Code for OrderHeader class:

using System;
using OnLineStore.Core.EntityLayer;
using OnLineStore.Core.EntityLayer.Sales;
using OnLineStore.Core.EntityLayer.HumanResources;
using System.Collections.ObjectModel;

namespace OnLineStore.Core.EntityLayer.Sales
{
 public partial class OrderHeader : IAuditEntity
 {
  public OrderHeader()
  {
  }

  public Order(long? orderHeaderID)
  {
   OrderHeaderID = orderHeaderID;
  }

  public long? OrderHeaderID { get; set; }

  public short? OrderStatusID { get; set; }

  public int? CustomerID { get; set; }

  public int? EmployeeID { get; set; }

  public int? ShipperID { get; set; }

  public DateTime? OrderDate { get; set; }

  public decimal? Total { get; set; }

  public short? CurrencyID { get; set; }

  public Guid? PaymentMethodID { 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 OnLineStore.Core.EntityLayer.Currency CurrencyFk { get; set; }

  public OnLineStore.Core.EntityLayer.Sales.Customer CustomerFk { get; set; }

  public OnLineStore.Core.EntityLayer.HumanResources.Employee EmployeeFk { get; set; }

  public OnLineStore.Core.EntityLayer.Sales.OrderStatus OrderStatusFk { get; set; }

  public OnLineStore.Core.EntityLayer.Sales.PaymentMethod PaymentMethodFk { get; set; }

  public OnLineStore.Core.EntityLayer.Sales.Shipper ShipperFk { get; set; }

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

Code for OnLineStoreDbContext class:

using System;
using Microsoft.EntityFrameworkCore;
using OnLineStore.Core.EntityLayer;
using OnLineStore.Core.DataLayer.Configurations;
using OnLineStore.Core.EntityLayer.HumanResources;
using OnLineStore.Core.EntityLayer.Production;
using OnLineStore.Core.EntityLayer.Sales;
using OnLineStore.Core.DataLayer.Configurations.HumanResources;
using OnLineStore.Core.DataLayer.Configurations.Production;
using OnLineStore.Core.DataLayer.Configurations.Sales;

namespace OnLineStore.Core.DataLayer
{
 public class OnLineStoreDbContext : DbContext
 {
  public StoreDbContext(DbContextOptions<OnLineStoreDbContext> options)
   : base(options)
  {
  }

  public DbSet<ChangeLog> ChangeLogs { get; set; }

  public DbSet<ChangeLogExclusion> ChangeLogExclusions { get; set; }

  public DbSet<Country> Countries { get; set; }

  public DbSet<CountryCurrency> CountryCurrencies { get; set; }

  public DbSet<Currency> Currencies { get; set; }

  public DbSet<EventLog> EventLogs { get; set; }

  public DbSet<Employee> Employees { get; set; }

  public DbSet<EmployeeAddress> EmployeeAddresses { get; set; }

  public DbSet<EmployeeEmail> EmployeeEmails { get; set; }

  public DbSet<Product> Products { get; set; }

  public DbSet<ProductCategory> ProductCategories { get; set; }

  public DbSet<ProductInventory> ProductInventories { get; set; }

  public DbSet<Warehouse> Warehouses { get; set; }

  public DbSet<Customer> Customers { get; set; }

  public DbSet<CustomerAddress> CustomerAddresses { get; set; }

  public DbSet<CustomerEmail> CustomerEmails { get; set; }

  public DbSet<OrderHeader> OrderHeaders { get; set; }

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

  public DbSet<OrderStatus> OrderStatuses { get; set; }

  public DbSet<PaymentMethod> PaymentMethods { get; set; }

  public DbSet<Shipper> Shippers { get; set; }

  public DbSet<EmployeeInfo> EmployeeInfos { get; set; }

  public DbSet<OrderSummary> OrderSummaries { get; set; }

  protected override void OnModelCreating(ModelBuilder modelBuilder)
  {
   // Apply all configurations for tables
   
   modelBuilder
    .ApplyConfiguration(new ChangeLogConfiguration())
    .ApplyConfiguration(new ChangeLogExclusionConfiguration())
    .ApplyConfiguration(new CountryConfiguration())
    .ApplyConfiguration(new CountryCurrencyConfiguration())
    .ApplyConfiguration(new CurrencyConfiguration())
    .ApplyConfiguration(new EventLogConfiguration())
    .ApplyConfiguration(new EmployeeConfiguration())
    .ApplyConfiguration(new EmployeeAddressConfiguration())
    .ApplyConfiguration(new EmployeeEmailConfiguration())
    .ApplyConfiguration(new ProductConfiguration())
    .ApplyConfiguration(new ProductCategoryConfiguration())
    .ApplyConfiguration(new ProductInventoryConfiguration())
    .ApplyConfiguration(new WarehouseConfiguration())
    .ApplyConfiguration(new CustomerConfiguration())
    .ApplyConfiguration(new CustomerAddressConfiguration())
    .ApplyConfiguration(new CustomerEmailConfiguration())
    .ApplyConfiguration(new OrderHeaderConfiguration())
    .ApplyConfiguration(new OrderDetailConfiguration())
    .ApplyConfiguration(new OrderStatusConfiguration())
    .ApplyConfiguration(new PaymentMethodConfiguration())
    .ApplyConfiguration(new ShipperConfiguration())
   ;
   
   // Apply all configurations for views
   
   modelBuilder
    .ApplyConfiguration(new EmployeeInfoConfiguration())
    .ApplyConfiguration(new OrderSummaryConfiguration())
   ;
   
   base.OnModelCreating(modelBuilder);
  }

  [DbFunction(FunctionName = "ufnGetEmployeeFullName", Schema = "HumanResources")]
  public static string HumanResourcesUfnGetEmployeeFullName(int? EmployeeID)
  {
   throw new Exception();
  }

  [DbFunction(FunctionName = "ufnGetStock", Schema = "Production")]
  public static int? ProductionUfnGetStock(int? ProductID)
  {
   throw new Exception();
  }
 }
}

Code for OrderHeaderConfiguration class:

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using OnLineStore.Core.EntityLayer.Sales;

namespace OnLineStore.Core.DataLayer.Configurations.Sales
{
 public class OrderHeaderConfiguration : IEntityTypeConfiguration<Store.Core.EntityLayer.Sales.OrderHeader>
 {
  public void Configure(EntityTypeBuilder<Store.Core.EntityLayer.Sales.OrderHeader> builder)
  {
   // Set configuration for entity
   builder.ToTable("OrderHeader", "Sales");
   
   // Set key for entity
   builder.HasKey(p => p.OrderHeaderID);
   
   // Set identity for entity (auto increment)
   builder.Property(p => p.OrderHeaderID).UseSqlServerIdentityColumn();
   
   // Set configuration for columns
   builder.Property(p => p.OrderHeaderID).HasColumnType("bigint").IsRequired();
   builder.Property(p => p.OrderStatusID).HasColumnType("smallint").IsRequired();
   builder.Property(p => p.CustomerID).HasColumnType("int").IsRequired();
   builder.Property(p => p.EmployeeID).HasColumnType("int");
   builder.Property(p => p.ShipperID).HasColumnType("int");
   builder.Property(p => p.OrderDate).HasColumnType("datetime").IsRequired();
   builder.Property(p => p.Total).HasColumnType("decimal(12, 4)").IsRequired();
   builder.Property(p => p.CurrencyID).HasColumnType("smallint");
   builder.Property(p => p.PaymentMethodID).HasColumnType("uniqueidentifier");
   builder.Property(p => p.Comments).HasColumnType("varchar(max)");
   builder.Property(p => p.CreationUser).HasColumnType("varchar(25)").IsRequired();
   builder.Property(p => p.CreationDateTime).HasColumnType("datetime").IsRequired();
   builder.Property(p => p.LastUpdateUser).HasColumnType("varchar(25)");
   builder.Property(p => p.LastUpdateDateTime).HasColumnType("datetime");
   builder.Property(p => p.Timestamp).HasColumnType("timestamp(8)");
   
   // Set concurrency token for entity
   builder
    .Property(p => p.Timestamp)
    .ValueGeneratedOnAddOrUpdate()
    .IsConcurrencyToken();
   
   // Add configuration for foreign keys
   builder
    .HasOne(p => p.CurrencyFk)
    .WithMany(b => b.OrderList)
    .HasForeignKey(p => p.CurrencyID)
    .HasConstraintName("FK_Sales_Order_Currency");
   
   builder
    .HasOne(p => p.CustomerFk)
    .WithMany(b => b.OrderList)
    .HasForeignKey(p => p.CustomerID)
    .HasConstraintName("FK_Sales_Order_Customer");
   
   builder
    .HasOne(p => p.EmployeeFk)
    .WithMany(b => b.OrderList)
    .HasForeignKey(p => p.EmployeeID)
    .HasConstraintName("FK_Sales_Order_Employee");
   
   builder
    .HasOne(p => p.OrderStatusFk)
    .WithMany(b => b.OrderList)
    .HasForeignKey(p => p.OrderStatusID)
    .HasConstraintName("FK_Sales_Order_OrderStatus");
   
   builder
    .HasOne(p => p.PaymentMethodFk)
    .WithMany(b => b.OrderList)
    .HasForeignKey(p => p.PaymentMethodID)
    .HasConstraintName("FK_Sales_Order_PaymentMethod");
   
   builder
    .HasOne(p => p.ShipperFk)
    .WithMany(b => b.OrderList)
    .HasForeignKey(p => p.ShipperID)
    .HasConstraintName("FK_Sales_Order_Shipper");
   
  }
 }
}

Code for ISalesRepository interface:

using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using OnLineStore.Core.EntityLayer;
using OnLineStore.Core.DataLayer.Contracts;
using OnLineStore.Core.EntityLayer.HumanResources;
using OnLineStore.Core.EntityLayer.Production;
using OnLineStore.Core.EntityLayer.Sales;
using OnLineStore.Core.DataLayer.DataContracts;

namespace OnLineStore.Core.DataLayer.Contracts
{
 public interface ISalesRepository : IRepository
 {
  IQueryable<Customer> GetCustomers();

  Task<Customer> GetCustomerAsync(Customer entity);

  IQueryable<CustomerAddress> GetCustomerAddresses(int? customerID = null);

  Task<CustomerAddress> GetCustomerAddressAsync(CustomerAddress entity);

  IQueryable<CustomerEmail> GetCustomerEmails(int? customerID = null);

  Task<CustomerEmail> GetCustomerEmailAsync(CustomerEmail entity);

  IQueryable<OrderDto> GetOrderHeaders(short? currencyID = null, int? customerID = null, int? employeeID = null, short? orderStatusID = null, Guid? paymentMethodID = null, int? shipperID = null);

  Task<Order> GetOrderHeaderAsync(OrderHeader entity);

  IQueryable<OrderDetail> GetOrderDetails(long? orderID = null, int? productID = null);

  Task<OrderDetail> GetOrderDetailAsync(OrderDetail entity);

  Task<OrderDetail> GetOrderDetailByOrderIDAndProductIDAsync(OrderDetail entity);

  IQueryable<OrderStatus> GetOrderStatuses();

  Task<OrderStatus> GetOrderStatusAsync(OrderStatus entity);

  IQueryable<PaymentMethod> GetPaymentMethods();

  Task<PaymentMethod> GetPaymentMethodAsync(PaymentMethod entity);

  IQueryable<Shipper> GetShippers();

  Task<Shipper> GetShipperAsync(Shipper entity);

  IQueryable<OrderSummary> GetOrderSummaries();
 }
}

Code for OrderDataContract class:

using System;

namespace OnLineStore.Core.DataLayer.DataContracts
{
 public class OrderDto
 {
  public Int64? OrderID { get; set; }

  public Int16? OrderStatusID { get; set; }

  public Int32? CustomerID { get; set; }

  public Int32? EmployeeID { get; set; }

  public Int32? ShipperID { get; set; }

  public DateTime? OrderDate { get; set; }

  public Decimal? Total { get; set; }

  public Int16? CurrencyID { get; set; }

  public Guid? PaymentMethodID { 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 CurrencyCurrencyName { get; set; }

  public String CurrencyCurrencySymbol { 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 OrderStatusDescription { get; set; }

  public String PaymentMethodPaymentMethodName { get; set; }

  public String PaymentMethodPaymentMethodDescription { get; set; }

  public String ShipperCompanyName { get; set; }

  public String ShipperContactName { get; set; }
 }
}

Code for Repository class:

using System;
using System.Threading.Tasks;
using OnLine.Core.EntityLayer;

namespace OnLineStore.Core.DataLayer.Contracts
{
 public class Repository
 {
  protected bool Disposed;
  protected OnLineStoreDbContext DbContext;

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

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

  public virtual void Add<TEntity>(TEntity entity) where TEntity : class
  {
   // Cast entity to IAuditEntity
   if(entity is IAuditEntity cast)
   {
    // Set creation datetime
    cast.CreationDateTime = DateTime.Now;
   }
   
   DbContext.Add(entity);
  }

  public virtual void Update<TEntity>(TEntity entity) where TEntity : class
  {
   // Cast entity to IAuditEntity
   if (entity is IAuditEntity cast)
   {
    // Set update datetime
    cast.LastUpdateDateTime = DateTime.Now;
   }
   
   DbContext.Update(entity);
  }

  public virtual void Remove<TEntity>(TEntity entity) where TEntity : class
  {
   DbContext.Remove(entity);
  }

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

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

Code for SalesRepository class:

using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using OnLineStore.Core.EntityLayer;
using OnLineStore.Core.DataLayer.Contracts;
using OnLineStore.Core.EntityLayer.HumanResources;
using OnLineStore.Core.EntityLayer.Production;
using OnLineStore.Core.EntityLayer.Sales;
using OnLineStore.Core.DataLayer.DataContracts;

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

  public IQueryable<Customer> GetCustomers()
   => DbContext.Customers;

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

  public IQueryable<CustomerAddress> GetCustomerAddresses(int? customerID = null)
  {
   // Get query from DbSet
   var query = DbContext.CustomerAddresses.AsQueryable();
   
   // Filter by: 'CustomerID'
   if (customerID.HasValue)
    query = query.Where(item => item.CustomerID == customerID);
   
   return query;
  }

  public async Task<CustomerAddress> GetCustomerAddressAsync(CustomerAddress entity)
   => await DbContext.CustomerAddresses.FirstOrDefaultAsync(item => item.CustomerAddressID == entity.CustomerAddressID);

  public IQueryable<CustomerEmail> GetCustomerEmails(int? customerID = null)
  {
   // Get query from DbSet
   var query = DbContext.CustomerEmails.AsQueryable();
   
   // Filter by: 'CustomerID'
   if (customerID.HasValue)
    query = query.Where(item => item.CustomerID == customerID);
   
   return query;
  }

  public async Task<CustomerEmail> GetCustomerEmailAsync(CustomerEmail entity)
   => await DbContext.CustomerEmails.FirstOrDefaultAsync(item => item.CustomerEmailID == entity.CustomerEmailID);

  public IQueryable<OrderDto> GetOrderHeaders(short? currencyID = null, int? customerID = null, int? employeeID = null, short? orderStatusID = null, Guid? paymentMethodID = null, int? shipperID = null)
  {
   // Get query from DbSet
   var query = from order in DbContext.Set<Order>()
    join currencyJoin in DbContext.Currencies on order.CurrencyID equals currencyJoin.CurrencyID into currencyTemp
     from currency in currencyTemp.DefaultIfEmpty()
    join customer in DbContext.Customers on order.CustomerID equals customer.CustomerID
    join employeeJoin in DbContext.Employees on order.EmployeeID equals employeeJoin.EmployeeID into employeeTemp
     from employee in employeeTemp.DefaultIfEmpty()
    join orderStatus in DbContext.OrderStatuses on order.OrderStatusID equals orderStatus.OrderStatusID
    join paymentMethodJoin in DbContext.PaymentMethods on order.PaymentMethodID equals paymentMethodJoin.PaymentMethodID into paymentMethodTemp
     from paymentMethod in paymentMethodTemp.DefaultIfEmpty()
    join shipperJoin in DbContext.Shippers on order.ShipperID equals shipperJoin.ShipperID into shipperTemp
     from shipper in shipperTemp.DefaultIfEmpty()
    select new OrderDto
    {
     OrderID = order.OrderID,
     OrderStatusID = order.OrderStatusID,
     CustomerID = order.CustomerID,
     EmployeeID = order.EmployeeID,
     ShipperID = order.ShipperID,
     OrderDate = order.OrderDate,
     Total = order.Total,
     CurrencyID = order.CurrencyID,
     PaymentMethodID = order.PaymentMethodID,
     Comments = order.Comments,
     CreationUser = order.CreationUser,
     CreationDateTime = order.CreationDateTime,
     LastUpdateUser = order.LastUpdateUser,
     LastUpdateDateTime = order.LastUpdateDateTime,
     Timestamp = order.Timestamp,
     CurrencyCurrencyName = currency == null ? string.Empty : currency.CurrencyName,
     CurrencyCurrencySymbol = currency == null ? string.Empty : currency.CurrencySymbol,
     CustomerCompanyName = customer == null ? string.Empty : customer.CompanyName,
     CustomerContactName = customer == null ? string.Empty : customer.ContactName,
     EmployeeFirstName = employee == null ? string.Empty : employee.FirstName,
     EmployeeMiddleName = employee == null ? string.Empty : employee.MiddleName,
     EmployeeLastName = employee == null ? string.Empty : employee.LastName,
     EmployeeBirthDate = employee == null ? default(DateTime?) : employee.BirthDate,
     OrderStatusDescription = orderStatus == null ? string.Empty : orderStatus.Description,
     PaymentMethodPaymentMethodName = paymentMethod == null ? string.Empty : paymentMethod.PaymentMethodName,
     PaymentMethodPaymentMethodDescription = paymentMethod == null ? string.Empty : paymentMethod.PaymentMethodDescription,
     ShipperCompanyName = shipper == null ? string.Empty : shipper.CompanyName,
     ShipperContactName = shipper == null ? string.Empty : shipper.ContactName,
    };
   
   // Filter by: 'CurrencyID'
   if (currencyID.HasValue)
    query = query.Where(item => item.CurrencyID == currencyID);
   
   // Filter by: 'CustomerID'
   if (customerID.HasValue)
    query = query.Where(item => item.CustomerID == customerID);
   
   // Filter by: 'EmployeeID'
   if (employeeID.HasValue)
    query = query.Where(item => item.EmployeeID == employeeID);
   
   // Filter by: 'OrderStatusID'
   if (orderStatusID.HasValue)
    query = query.Where(item => item.OrderStatusID == orderStatusID);
   
   // Filter by: 'PaymentMethodID'
   if (paymentMethodID != null)
    query = query.Where(item => item.PaymentMethodID == paymentMethodID);
   
   // Filter by: 'ShipperID'
   if (shipperID.HasValue)
    query = query.Where(item => item.ShipperID == shipperID);
   
   return query;
  }

  public async Task<OrderHeader> GetOrderAsync(OrderHeader entity)
  {
   return await DbContext.Orders
    .Include(p => p.CurrencyFk)
    .Include(p => p.CustomerFk)
    .Include(p => p.EmployeeFk)
    .Include(p => p.OrderStatusFk)
    .Include(p => p.PaymentMethodFk)
    .Include(p => p.ShipperFk)
    .FirstOrDefaultAsync(item => item.OrderHeaderID == entity.OrderHeaderID);
  }

  public IQueryable<OrderDetail> GetOrderDetails(long? orderID = null, int? productID = null)
  {
   // Get query from DbSet
   var query = DbContext.OrderDetails.AsQueryable();
   
   // Filter by: 'OrderID'
   if (orderID.HasValue)
    query = query.Where(item => item.OrderID == orderID);
   
   // Filter by: 'ProductID'
   if (productID.HasValue)
    query = query.Where(item => item.ProductID == productID);
   
   return query;
  }

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

  public async Task<OrderDetail> GetOrderDetailByOrderIDAndProductIDAsync(OrderDetail entity)
   => await DbContext.OrderDetails.FirstOrDefaultAsync(item => item.OrderID == entity.OrderID && item.ProductID == entity.ProductID);

  public IQueryable<OrderStatus> GetOrderStatuses()
   => DbContext.OrderStatuses;

  public async Task<OrderStatus> GetOrderStatusAsync(OrderStatus entity)
   => await DbContext.OrderStatuses.FirstOrDefaultAsync(item => item.OrderStatusID == entity.OrderStatusID);

  public IQueryable<PaymentMethod> GetPaymentMethods()
   => DbContext.PaymentMethods;

  public async Task<PaymentMethod> GetPaymentMethodAsync(PaymentMethod entity)
   => await DbContext.PaymentMethods.FirstOrDefaultAsync(item => item.PaymentMethodID == entity.PaymentMethodID);

  public IQueryable<Shipper> GetShippers()
   => DbContext.Shippers;

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

  public IQueryable<OrderSummary> GetOrderSummaries()
   => DbContext.OrderSummaries;
 }
}

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 indicates that entities will scaffold 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.

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

Setting Up CatFactory for Entity Framework Core

Additionally, there are more settings for Entity Framework Core project instance, we'll take a look on those settings:

Name Default Value Description
ForceOverwrite false Indicates if code builder must overwrite files if they already exist
SimplifyDataTypes true Indicates if code builder must change from CLR types to native types (e.g. Int32 => int)
UseAutomaticPropertiesForEntities true Indicates if entities classes will use automatic properties or not, if value is false, the entity will contains private fields
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
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)
EntitiesWithDataContracts false Indicates if entities must to scaffold as data transfer objects in repositories
BackingFields   Sets the name of columns will use fields instead of properties
InsertExclusions   Sets the name of columns will ignore for insert action
UpdateExclusions   Sets the name of columns will ignore for update action

Also we can change the namespaces, 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
Configurations Configurations Gets or sets the namespace for configurations 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

Points of Interest

  • CatFactory doesn't have command line for nuget because from my point of view it will be a big trouble to allow set values for all settings because we have a lot of settings for EntityFrameworkCoreProjectSettings, I think at this moment is more simple to create a console project to generate the code and then developer move generated files for existing project and make a code refactor if applies
  • CatFactory doesn't have UI now because at the beginning of this project .NET Core had no an standard UI, but we're working on UI for CatFactory maybe we'll choose Angular =^^=
  • Now we are focused on Entity Framework Core and Dapper but in future there will be Web API, Unit Tests and other things :)
  • CatFactory has a package for Dapper, at this moment there isn't article for that but the way to use is similar for Entity Framework Core; you can install CatFactory.Dapper package from nuget
  • We're working in continuous updates to provide better help for user

Related Links

Code Improvements

  • Add support for table functions
  • Add support for stored procedures
  • Allow to overwrite naming convention for project
  • Add author's information for output files

Bugs?

If you get any exception with CatFactory packages, please use these links:

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
  • 19th September, 2017: Change on article's sections
  • 31th October, 2017: Update to alpha 3 version
  • 21th November, 2017: Addition of Trivia
  • 18th January, 2018: Addition of Workshop
  • 30th Abril, 2018: Update package to beta version
  • 16th May, 2018: Add support for EF Core 2

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

HHerzl
Software Developer
El Salvador El Salvador
CatFactory Creator.

Full Stack Developer with Experience in C#, Entity Framework Core, ASP.NET Core and Angular.

Comments and Discussions

 
QuestionLooks great Pin
David Schiffer31-Oct-18 8:46
professionalDavid Schiffer31-Oct-18 8:46 
AnswerRe: Looks great Pin
HHerzl7-Nov-18 21:08
memberHHerzl7-Nov-18 21:08 
SuggestionImprovement and suggestions Pin
Member 965484630-Nov-17 0:23
memberMember 965484630-Nov-17 0:23 
GeneralRe: Improvement and suggestions Pin
HHerzl18-Dec-17 17:03
memberHHerzl18-Dec-17 17:03 
PraiseRe: Improvement and suggestions Pin
Member 965484620-Dec-17 7:08
memberMember 965484620-Dec-17 7:08 
GeneralRe: Improvement and suggestions Pin
Amelia J. Abram26-May-19 23:29
professionalAmelia J. Abram26-May-19 23:29 
BugPrimary keys Pin
Member 965484629-Nov-17 0:25
memberMember 965484629-Nov-17 0:25 
GeneralRe: Primary keys Pin
HHerzl3-Dec-17 17:39
memberHHerzl3-Dec-17 17:39 
GeneralRe: Primary keys Pin
Member 96548464-Dec-17 7:11
memberMember 96548464-Dec-17 7:11 
GeneralRe: Primary keys Pin
HHerzl5-Dec-17 2:19
memberHHerzl5-Dec-17 2:19 
GeneralRe: Primary keys Pin
HHerzl18-Dec-17 13:26
memberHHerzl18-Dec-17 13:26 
Bugviews Pin
Member 965484628-Nov-17 23:50
memberMember 965484628-Nov-17 23:50 
GeneralRe: views Pin
HHerzl3-Dec-17 17:33
memberHHerzl3-Dec-17 17:33 
GeneralRe: views Pin
Member 96548464-Dec-17 7:13
memberMember 96548464-Dec-17 7:13 
BugAn other issue Pin
Member 965484628-Nov-17 21:45
memberMember 965484628-Nov-17 21:45 
GeneralRe: An other issue Pin
HHerzl5-Dec-17 2:14
memberHHerzl5-Dec-17 2:14 
GeneralRe: An other issue Pin
HHerzl18-Dec-17 13:25
memberHHerzl18-Dec-17 13:25 
QuestionGreat Job :-) Pin
Member 96548465-Nov-17 23:27
memberMember 96548465-Nov-17 23:27 
AnswerRe: Great Job :-) Pin
HHerzl7-Nov-17 16:57
memberHHerzl7-Nov-17 16:57 
AnswerRe: Great Job :-) Pin
HHerzl8-Nov-17 21:47
memberHHerzl8-Nov-17 21:47 
PraiseRe: Great Job :-) Pin
Member 96548468-Nov-17 23:38
memberMember 96548468-Nov-17 23:38 
Fine!
I'll test this.
Does it also work for views?
Many Thanks. Smile | :) Thumbs Up | :thumbsup:
GeneralRe: Great Job :-) Pin
HHerzl20-Nov-17 14:11
memberHHerzl20-Nov-17 14:11 
PraiseRe: Great Job :-) Pin
Member 965484623-Nov-17 23:54
memberMember 965484623-Nov-17 23:54 
GeneralRe: Great Job :-) Pin
Member 965484628-Nov-17 21:48
memberMember 965484628-Nov-17 21:48 
QuestionGreat article & concept, but a little problem Pin
dkurok5-Nov-17 1:58
memberdkurok5-Nov-17 1:58 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Article
Posted 12 Dec 2016

Tagged as

Stats

104.8K views
92 bookmarked