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:
- Create Database Factory
- Import Database
- Create instance of Project (Entity Framework Core, Dapper, etc.)
- Build Features (One feature per schema)
- 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 the 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:
var mappings = database.DatabaseTypeMaps;
var mapsForString = mappings.Where(item => item.GetClrType() == typeof(string)).ToList();
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.OrderHeader | Applies for specific object with name Sales.OrderHeader |
Sales.* | Applies for all objects inside of Sales schema |
*.OrderHeader | Applies for all objects with name OrderHeader with no matter schema |
*.* | Applies for all objects, this is the global selection |
Code Sample:
project.GlobalSelection(settings =>
{
settings.ForceOverwrite = true;
settings.AuditEntity = new AuditEntity
("CreationUser", "CreationDateTime", "LastUpdateUser", "LastUpdateDateTime");
settings.ConcurrencyToken = "Timestamp";
});
project.Selection("Sales.OrderHeader", 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 scaffolding and another one to handle an action after scaffolding.
Code Sample:
project.ScaffoldingDefinition += (source, args) =>
{
};
project.ScaffoldedDefinition += (source, args) =>
{
};
Document Object Model
The most databases now have a document object model, this model provides a simple way to know the structure for database and objects.
In SQL Server there are views like sys.tables, sys.views, ys.columns that contain all information about these objects.
Also there are stored procedures and functions like sp_help and dm_exec_describe_first_result_set_for_object that retrieve information about a database object and columns for the result of execute an object.
In CatFactory, this model is available through extension methods for DbConnection class:
using (var connection = new SqlConnection("server=(local);database=OnlineStore;integrated security=yes;"))
{
connection.Open();
var tables = connection.GetSysTables().ToList();
}
These methods are available for SQL Server Document Object Model:
- GetSysSchemas
- GetSysTypes
- GetSysTables
- GetSysViews
- GetSysColumns
- GetSysSequences
Import Bag
Based on ViewBag concept from ASP MVC, this concept allows to add specific data for different databases providers.
ImportBag is a dynamic property, the CatFactory engine saves specific information in this property, for SQL Server saves extended properties, scalar functions, table functions, stored procedures and sequences.
For Postgre SQL saves sequences, in the future versions it will save another information related to database objects like stored procedures*.
Workshop
One of the 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 want 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 not 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 am wrong about this point, please let me know in the comments below.
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:
- This implementation breaks Dapper concept?
- What do you think about having metadata for entities in Dapper?
Query Builder Draft
Select All
var query = QueryBuilder
.Select<Shipper>();
Select by Key
var query = QueryBuilder
.Select<Shipper>()
.Where("ShipperID", QueryOperator.Equals, 1);
Insert
var query = QueryBuilder
.Insert<Shipper>(identity: "ShipperID");
Update
var query = QueryBuilder
.Update<Shipper>(key: new string[] { "ShipperID" });
Delete
var query = QueryBuilder
.Delete<Shipper>(key: new string[] { "ShipperID" });
Select by
var query = QueryBuilder
.Select<Shipper>()
.Where("CompanyName", QueryOperator.Like, "%a%")
.And("Phone", QueryOperator.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 solved 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 cleaner way to work with CatFactory
.
Working With Database
var database = SqlServerDatabaseFactory.Import("YourConnectionStringHere");
foreach (var table in database.Tables)
{
if (table.PrimaryKey == null)
{
continue;
}
if (table.Identity != null)
{
var identityName = table.Identity.Name;
}
foreach (var column in table.Columns)
{
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, most 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
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#.
Let's 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 used:
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:
CodeLine
CommentLine
EmptyLine
PreprocessorDirectiveLine
ReturnLine
TodoLine
Let's 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()
{
return DbContext.StockItems.ToList();
}
}
}
Now let's 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 Prerequisites
- .NET Core
- Visual Studio 2017 or VS Code
- Access to existing SQL Server instance
Using the Code
Please follow these steps to scaffold Dapper
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.
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 find 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 suffix that indicates it's a project to scaffold, for example: OnLineStore.CatFactory.Dapper
.
Add the following packages for your project:
Name | Version | Description |
CatFactory.SqlServer | 1.0.0-beta-sun-build32 | Provides import feature for SQL Server databases |
CatFactory.Dapper | 1.0.0-beta-sun-build33 | Provides scaffold for Dapper |
Save all changes and build the project.
Step 03 - Add Code To Scaffold
Add this code in Main
method:
var databaseFactory = new SqlServerDatabaseFactory
{
DatabaseImportSettings = new DatabaseImportSettings
{
ConnectionString = "server=(local);database=OnlineStore;integrated security=yes;",
ImportScalarFunctions = true,
ImportTableFunctions = true,
ImportStoredProcedures = true,
Exclusions =
{
"dbo.sysdiagrams",
"dbo.fn_diagramobjects"
}
}
};
var database = databaseFactory.Import();
var project = new DapperProject
{
Name = "OnlineStore.Core",
Database = database,
OutputDirectory = @"C:\Projects\OnlineStore.Core"
};
project.GlobalSelection(settings =>
{
settings.ForceOverwrite = true;
settings.UpdateExclusions = new List<string>
{ "CreationUser", "CreationDateTime", "Timestamp" };
settings.InsertExclusions = new List<string>
{ "LastUpdateUser", "LastUpdateDateTime", "Timestamp" };
});
project.Selection("Sales.OrderHeader",
settings => settings.AddPagingForGetAllOperation = true);
project.BuildFeatures();
project.ScaffoldingDefinition += (source, args) =>
{
};
project.ScaffoldedDefinition += (source, args) =>
{
};
project
.ScaffoldEntityLayer()
.ScaffoldDataLayer();
If you want to know more about import databases with CatFactory.SqlServer
package, take a look on wiki: CatFactory.SqlServer Wiki.
Description for methods:
Extension Methods for DapperProject Instance
Name | Description |
ScaffoldEntityLayer | Scaffold entities from Tables, Views, Scalar Functions and Table Functions |
ScaffoldDataLayer | Scaffold Contracts and Repositories |
Setting Up CatFactory for Dapper
Additionally, there are settings for DapperProject
instance, we'll take a look at those settings:
Name | Default Value | Description |
ForceOverwrite | false | Indicates if code builder must overwrite files if they already exist |
SimplifyDataTypes | false | 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 contain private fields |
EnableDataBindings | false | Implements INotifyPropertyChanged property and add properties with fields for class definition |
UseStringBuilderForQueries | true | Indicates if queries will construct with StringBuilder |
InsertExclusions | | Sets the columns list to exclude in insert method |
UpdateExclusions | | Sets the columns list to exclude in update method |
AddPagingForGetAllOperation | false | Add SQL code for GetAll methods to allow paging |
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 |
Contracts | Contracts | Gets or sets the namespace for contracts in data layer |
Repositories | Repositories | Gets or sets the namespace for repositories in data layer |
Step 04 - Create Console Project
Obviously, we add this simple code to test output code doesn't have errors, if we use the generated code in Web API project, we'll need to add dependency injection and other things, please check in links list at the end of this post for more information.
Now we can go to output directory and create a console project for .NET Core and add code according to your choice.
Get All
using (var connection = new SqlConnection("server=(local);
database=OnlineStore;integrated security=yes;"))
{
var repository = new SalesRepository(connection);
var orders = await repository.GetOrderHeadersAsync();
}
Get by Key
using (var connection = new SqlConnection("server=(local);
database=OnlineStore;integrated security=yes;"))
{
var repository = new SalesRepository(connection);
var entity = await repository.GetOrderHeaderAsync(new Order(1));
}
Get by Unique (If entity has an unique constraint):
using (var connection = new SqlConnection("server=(local);
database=OnlineStore;integrated security=yes;"))
{
var repository = new SalesRepository(connection);
var entity = await repository.GetProductByProductNameAsync
(new Product { ProductName = "The King of Fighters XIV"});
}
Add:
using (var connection = new SqlConnection("server=(local);
database=OnlineStore;integrated security=yes;"))
{
var repository = new SalesRepository(connection);
var entity = new OrderHeader();
await repository.AddOrderHeaderAsync(entity);
}
Update:
using (var connection = new SqlConnection("server=(local);
database=OnlineStore;integrated security=yes;"))
{
var repository = new SalesRepository(connection);
var entity = await repository.GetOrderHeaderAsync(new OrderHeader(1));
await repository.UpdateOrderHeaderAsync(entity);
}
Remove:
using (var connection = new SqlConnection("server=(local);
database=OnlineStore;integrated security=yes;"))
{
var repository = new SalesRepository(connection);
var entity = await repository.GetOrderHeaderAsync(new OrderHeader(1));
await repository.RemoveOrderHeaderAsync(entity);
}
Table Function:
using (var connection = new SqlConnection("server=(local);
database=OnlineStore;integrated security=yes;"))
{
var repository = new SalesRepository(connection);
var customerContacts = await repository.GetUfnGetCustomerContactsAsync(1);
}
The previous code samples do not include error handling, these are samples to show how to work with generated code.
Name | Version | Description |
Dapper | Latest version | Provides helper methods for database operations |
How All Code Works Together?
We need to create the options for connection, then we proceed to create the instance for repository and finally, we invoke the method from repository instance.
For this architecture implementation, we are using the .NET naming conventions: PascalCase for classes, interfaces and methods; camelCase for parameters.
Namespaces for generated code:
EntityLayer
DataLayer
DataLayer\Contracts
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) that 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\Repositories, we'll place the implementations for Contracts
definitions.
We can review the link about Entity Framework Core for Enterprise, and we can understand this guide allows us to generate all of that code to reduce time in code writing.
Code Review
We'll review some of the generated code for one entity to understand this design:
Code for OrderHeader
class:
using System;
namespace OnlineStore.Core.EntityLayer.Sales
{
public class OrderHeader : IEntity
{
public OrderHeader()
{
}
public OrderHeader(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 string CurrencyID { get; set; }
public Guid? PaymentMethodID { get; set; }
public int? DetailsCount { get; set; }
public long? ReferenceOrderID { 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; }
}
}
Code for IHumanResourcesRepository
interface:
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Dapper;
using OnlineStore.Core.EntityLayer;
using OnlineStore.Core.DataLayer.Contracts;
using OnlineStore.Core.EntityLayer.HumanResources;
using OnlineStore.Core.EntityLayer.Sales;
using OnlineStore.Core.EntityLayer.Warehouse;
namespace OnlineStore.Core.DataLayer.Contracts
{
public interface IHumanResourcesRepository : IRepository
{
Task<IEnumerable<Employee>> GetEmployeesAsync();
Task<Employee> GetEmployeeAsync(Employee entity);
Task<Int32> AddEmployeeAsync(Employee entity);
Task<Int32> UpdateEmployeeAsync(Employee entity);
Task<Int32> RemoveEmployeeAsync(Employee entity);
Task<IEnumerable<EmployeeAddress>> GetEmployeeAddressesAsync(Int32? employeeID = null);
Task<EmployeeAddress> GetEmployeeAddressAsync(EmployeeAddress entity);
Task<Int32> AddEmployeeAddressAsync(EmployeeAddress entity);
Task<Int32> UpdateEmployeeAddressAsync(EmployeeAddress entity);
Task<Int32> RemoveEmployeeAddressAsync(EmployeeAddress entity);
Task<IEnumerable<EmployeeEmail>> GetEmployeeEmailsAsync(Int32? employeeID = null);
Task<EmployeeEmail> GetEmployeeEmailAsync(EmployeeEmail entity);
Task<Int32> AddEmployeeEmailAsync(EmployeeEmail entity);
Task<Int32> UpdateEmployeeEmailAsync(EmployeeEmail entity);
Task<Int32> RemoveEmployeeEmailAsync(EmployeeEmail entity);
Task<IEnumerable<EmployeeInfo>> GetEmployeeInfosAsync(Int32? employeeID = null);
}
}
Code for ISalesRepository
interface:
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Dapper;
using OnlineStore.Core.EntityLayer;
using OnlineStore.Core.DataLayer.Contracts;
using OnlineStore.Core.EntityLayer.HumanResources;
using OnlineStore.Core.EntityLayer.Sales;
using OnlineStore.Core.EntityLayer.Warehouse;
namespace OnlineStore.Core.DataLayer.Contracts
{
public interface ISalesRepository : IRepository
{
Task<IEnumerable<Customer>> GetCustomersAsync(int pageSize = 10, int pageNumber = 1);
Task<Customer> GetCustomerAsync(Customer entity);
Task<Int32> AddCustomerAsync(Customer entity);
Task<Int32> UpdateCustomerAsync(Customer entity);
Task<Int32> RemoveCustomerAsync(Customer entity);
Task<IEnumerable<CustomerAddress>> GetCustomerAddressesAsync
(int pageSize = 10, int pageNumber = 1, Int32? customerID = null);
Task<CustomerAddress> GetCustomerAddressAsync(CustomerAddress entity);
Task<Int32> AddCustomerAddressAsync(CustomerAddress entity);
Task<Int32> UpdateCustomerAddressAsync(CustomerAddress entity);
Task<Int32> RemoveCustomerAddressAsync(CustomerAddress entity);
Task<IEnumerable<CustomerEmail>> GetCustomerEmailsAsync
(int pageSize = 10, int pageNumber = 1, Int32? customerID = null);
Task<CustomerEmail> GetCustomerEmailAsync(CustomerEmail entity);
Task<Int32> AddCustomerEmailAsync(CustomerEmail entity);
Task<Int32> UpdateCustomerEmailAsync(CustomerEmail entity);
Task<Int32> RemoveCustomerEmailAsync(CustomerEmail entity);
Task<IEnumerable<OrderDetail>> GetOrderDetailsAsync
(int pageSize = 10, int pageNumber = 1, Int64? orderHeaderID = null,
Int32? productID = null);
Task<OrderDetail> GetOrderDetailAsync(OrderDetail entity);
Task<OrderDetail> GetOrderDetailByOrderHeaderIDAndProductIDAsync(OrderDetail entity);
Task<Int32> AddOrderDetailAsync(OrderDetail entity);
Task<Int32> UpdateOrderDetailAsync(OrderDetail entity);
Task<Int32> RemoveOrderDetailAsync(OrderDetail entity);
Task<IEnumerable<OrderHeader>> GetOrderHeadersAsync
(int pageSize = 10, int pageNumber = 1, String currencyID = null,
Int32? customerID = null, Int32? employeeID = null,
Int16? orderStatusID = null, Guid? paymentMethodID = null, Int32? shipperID = null);
Task<OrderHeader> GetOrderHeaderAsync(OrderHeader entity);
Task<Int32> AddOrderHeaderAsync(OrderHeader entity);
Task<Int32> UpdateOrderHeaderAsync(OrderHeader entity);
Task<Int32> RemoveOrderHeaderAsync(OrderHeader entity);
Task<IEnumerable<OrderStatus>>
GetOrderStatusesAsync(int pageSize = 10, int pageNumber = 1);
Task<OrderStatus> GetOrderStatusAsync(OrderStatus entity);
Task<Int32> AddOrderStatusAsync(OrderStatus entity);
Task<Int32> UpdateOrderStatusAsync(OrderStatus entity);
Task<Int32> RemoveOrderStatusAsync(OrderStatus entity);
Task<IEnumerable<PaymentMethod>>
GetPaymentMethodsAsync(int pageSize = 10, int pageNumber = 1);
Task<PaymentMethod> GetPaymentMethodAsync(PaymentMethod entity);
Task<Int32> AddPaymentMethodAsync(PaymentMethod entity);
Task<Int32> UpdatePaymentMethodAsync(PaymentMethod entity);
Task<Int32> RemovePaymentMethodAsync(PaymentMethod entity);
Task<IEnumerable<Shipper>> GetShippersAsync(int pageSize = 10, int pageNumber = 1);
Task<Shipper> GetShipperAsync(Shipper entity);
Task<Int32> AddShipperAsync(Shipper entity);
Task<Int32> UpdateShipperAsync(Shipper entity);
Task<Int32> RemoveShipperAsync(Shipper entity);
Task<IEnumerable<OrderSummary>>
GetOrderSummariesAsync(int pageSize = 10, int pageNumber = 1,
Int64? orderHeaderID = null, Int16? orderStatusID = null,
Int32? customerID = null, Int32? employeeID = null, Int32? shipperID = null);
Task<IEnumerable<UfnGetCustomerContactResult>>
GetUfnGetCustomerContactsAsync(Int32? customerID);
}
}
Code for Repository
class:
using System.Data;
using OnlineStore.Core.EntityLayer;
namespace OnlineStore.Core.DataLayer.Contracts
{
public class Repository
{
public Repository(IDbConnection connection)
{
Connection = connection;
}
protected IDbConnection Connection { get; }
}
}
Code for HumanResourcesRepository
:
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Dapper;
using OnlineStore.Core.EntityLayer;
using OnlineStore.Core.DataLayer.Contracts;
using OnlineStore.Core.EntityLayer.HumanResources;
using OnlineStore.Core.EntityLayer.Sales;
using OnlineStore.Core.EntityLayer.Warehouse;
namespace OnlineStore.Core.DataLayer.Repositories
{
public class HumanResourcesRepository : Repository, IHumanResourcesRepository
{
public HumanResourcesRepository(IDbConnection connection)
: base(connection)
{
}
public async Task<IEnumerable<EmployeeInfo>>
GetEmployeeInfosAsync(Int32? employeeID = null)
{
var query = new StringBuilder();
query.Append(" select ");
query.Append(" [EmployeeID], ");
query.Append(" [EmployeeName], ");
query.Append(" [EmployeeAddresses], ");
query.Append(" [EmployeeEmails] ");
query.Append(" from ");
query.Append(" [HumanResources].[EmployeeInfo] ");
query.Append(" where ");
query.Append(" (@employeeID is null or [EmployeeID] = @employeeID) and ");
var parameters = new DynamicParameters();
parameters.Add("@employeeID", employeeID);
return await Connection.QueryAsync<EmployeeInfo>(query.ToString());
}
}
}
Code for SalesRepository
class:
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Dapper;
using OnlineStore.Core.EntityLayer;
using OnlineStore.Core.DataLayer.Contracts;
using OnlineStore.Core.EntityLayer.HumanResources;
using OnlineStore.Core.EntityLayer.Sales;
using OnlineStore.Core.EntityLayer.Warehouse;
namespace OnlineStore.Core.DataLayer.Repositories
{
public class SalesRepository : Repository, ISalesRepository
{
public SalesRepository(IDbConnection connection)
: base(connection)
{
}
public async Task<IEnumerable<OrderHeader>>
GetOrderHeadersAsync(int pageSize = 10, int pageNumber = 1,
String currencyID = null, Int32? customerID = null,
Int32? employeeID = null, Int16? orderStatusID = null,
Guid? paymentMethodID = null, Int32? shipperID = null)
{
var query = new StringBuilder();
query.Append(" select ");
query.Append(" [OrderHeaderID], ");
query.Append(" [OrderStatusID], ");
query.Append(" [CustomerID], ");
query.Append(" [EmployeeID], ");
query.Append(" [ShipperID], ");
query.Append(" [OrderDate], ");
query.Append(" [Total], ");
query.Append(" [CurrencyID], ");
query.Append(" [PaymentMethodID], ");
query.Append(" [DetailsCount], ");
query.Append(" [ReferenceOrderID], ");
query.Append(" [Comments], ");
query.Append(" [CreationUser], ");
query.Append(" [CreationDateTime], ");
query.Append(" [LastUpdateUser], ");
query.Append(" [LastUpdateDateTime], ");
query.Append(" [Timestamp] ");
query.Append(" from ");
query.Append(" [Sales].[OrderHeader] ");
query.Append(" where ");
query.Append(" (@currencyID is null or [CurrencyID] = @currencyID) and ");
query.Append(" (@customerID is null or [CustomerID] = @customerID) and ");
query.Append(" (@employeeID is null or [EmployeeID] = @employeeID) and ");
query.Append(" (@orderStatusID is null or [OrderStatusID] = @orderStatusID) and ");
query.Append(" (@paymentMethodID is null _
or [PaymentMethodID] = @paymentMethodID) and ");
query.Append(" (@shipperID is null or [ShipperID] = @shipperID) ");
query.Append(" order by ");
query.Append(" [OrderHeaderID] ");
query.Append(" offset @pageSize * (@pageNumber - 1) rows ");
query.Append(" fetch next @pageSize rows only ");
var parameters = new DynamicParameters();
parameters.Add("@pageSize", pageSize);
parameters.Add("@pageNumber", pageNumber);
parameters.Add("@currencyID", currencyID);
parameters.Add("@customerID", customerID);
parameters.Add("@employeeID", employeeID);
parameters.Add("@orderStatusID", orderStatusID);
parameters.Add("@paymentMethodID", paymentMethodID);
parameters.Add("@shipperID", shipperID);
return await Connection.QueryAsync<OrderHeader>
(new CommandDefinition(query.ToString(), parameters));
}
public async Task<OrderHeader> GetOrderHeaderAsync(OrderHeader entity)
{
var query = new StringBuilder();
query.Append(" select ");
query.Append(" [OrderHeaderID], ");
query.Append(" [OrderStatusID], ");
query.Append(" [CustomerID], ");
query.Append(" [EmployeeID], ");
query.Append(" [ShipperID], ");
query.Append(" [OrderDate], ");
query.Append(" [Total], ");
query.Append(" [CurrencyID], ");
query.Append(" [PaymentMethodID], ");
query.Append(" [DetailsCount], ");
query.Append(" [ReferenceOrderID], ");
query.Append(" [Comments], ");
query.Append(" [CreationUser], ");
query.Append(" [CreationDateTime], ");
query.Append(" [LastUpdateUser], ");
query.Append(" [LastUpdateDateTime], ");
query.Append(" [Timestamp] ");
query.Append(" from ");
query.Append(" [Sales].[OrderHeader] ");
query.Append(" where ");
query.Append(" [OrderHeaderID] = @orderHeaderID ");
var parameters = new DynamicParameters();
parameters.Add("orderHeaderID", entity.OrderHeaderID);
return await Connection.QueryFirstOrDefaultAsync<OrderHeader>
(query.ToString(), parameters);
}
public async Task<Int32> AddOrderHeaderAsync(OrderHeader entity)
{
var query = new StringBuilder();
query.Append(" insert into ");
query.Append(" [Sales].[OrderHeader] ");
query.Append(" ( ");
query.Append(" [OrderStatusID], ");
query.Append(" [CustomerID], ");
query.Append(" [EmployeeID], ");
query.Append(" [ShipperID], ");
query.Append(" [OrderDate], ");
query.Append(" [Total], ");
query.Append(" [CurrencyID], ");
query.Append(" [PaymentMethodID], ");
query.Append(" [DetailsCount], ");
query.Append(" [ReferenceOrderID], ");
query.Append(" [Comments], ");
query.Append(" [CreationUser], ");
query.Append(" [CreationDateTime] ");
query.Append(" ) ");
query.Append(" values ");
query.Append(" ( ");
query.Append(" @orderStatusID, ");
query.Append(" @customerID, ");
query.Append(" @employeeID, ");
query.Append(" @shipperID, ");
query.Append(" @orderDate, ");
query.Append(" @total, ");
query.Append(" @currencyID, ");
query.Append(" @paymentMethodID, ");
query.Append(" @detailsCount, ");
query.Append(" @referenceOrderID, ");
query.Append(" @comments, ");
query.Append(" @creationUser, ");
query.Append(" @creationDateTime ");
query.Append(" ) ");
query.Append(" select @orderHeaderID = @@identity ");
var parameters = new DynamicParameters();
parameters.Add("orderStatusID", entity.OrderStatusID);
parameters.Add("customerID", entity.CustomerID);
parameters.Add("employeeID", entity.EmployeeID);
parameters.Add("shipperID", entity.ShipperID);
parameters.Add("orderDate", entity.OrderDate);
parameters.Add("total", entity.Total);
parameters.Add("currencyID", entity.CurrencyID);
parameters.Add("paymentMethodID", entity.PaymentMethodID);
parameters.Add("detailsCount", entity.DetailsCount);
parameters.Add("referenceOrderID", entity.ReferenceOrderID);
parameters.Add("comments", entity.Comments);
parameters.Add("creationUser", entity.CreationUser);
parameters.Add("creationDateTime", entity.CreationDateTime);
parameters.Add("orderHeaderID",
dbType: DbType.Int64, direction: ParameterDirection.Output);
var affectedRows = await Connection.ExecuteAsync
(new CommandDefinition(query.ToString(), parameters));
entity.OrderHeaderID = parameters.Get<Int64?>("orderHeaderID");
return affectedRows;
}
public async Task<Int32> UpdateOrderHeaderAsync(OrderHeader entity)
{
var query = new StringBuilder();
query.Append(" update ");
query.Append(" [Sales].[OrderHeader] ");
query.Append(" set ");
query.Append(" [OrderStatusID] = @orderStatusID, ");
query.Append(" [CustomerID] = @customerID, ");
query.Append(" [EmployeeID] = @employeeID, ");
query.Append(" [ShipperID] = @shipperID, ");
query.Append(" [OrderDate] = @orderDate, ");
query.Append(" [Total] = @total, ");
query.Append(" [CurrencyID] = @currencyID, ");
query.Append(" [PaymentMethodID] = @paymentMethodID, ");
query.Append(" [DetailsCount] = @detailsCount, ");
query.Append(" [ReferenceOrderID] = @referenceOrderID, ");
query.Append(" [Comments] = @comments, ");
query.Append(" [LastUpdateUser] = @lastUpdateUser, ");
query.Append(" [LastUpdateDateTime] = @lastUpdateDateTime ");
query.Append(" where ");
query.Append(" [OrderHeaderID] = @orderHeaderID ");
var parameters = new DynamicParameters();
parameters.Add("orderStatusID", entity.OrderStatusID);
parameters.Add("customerID", entity.CustomerID);
parameters.Add("employeeID", entity.EmployeeID);
parameters.Add("shipperID", entity.ShipperID);
parameters.Add("orderDate", entity.OrderDate);
parameters.Add("total", entity.Total);
parameters.Add("currencyID", entity.CurrencyID);
parameters.Add("paymentMethodID", entity.PaymentMethodID);
parameters.Add("detailsCount", entity.DetailsCount);
parameters.Add("referenceOrderID", entity.ReferenceOrderID);
parameters.Add("comments", entity.Comments);
parameters.Add("lastUpdateUser", entity.LastUpdateUser);
parameters.Add("lastUpdateDateTime", entity.LastUpdateDateTime);
parameters.Add("orderHeaderID", entity.OrderHeaderID);
return await Connection.ExecuteAsync
(new CommandDefinition(query.ToString(), parameters));
}
public async Task<Int32> RemoveOrderHeaderAsync(OrderHeader entity)
{
var query = new StringBuilder();
query.Append(" delete from ");
query.Append(" [Sales].[OrderHeader] ");
query.Append(" where ");
query.Append(" [OrderHeaderID] = @orderHeaderID ");
var parameters = new DynamicParameters();
parameters.Add("orderHeaderID", entity.OrderHeaderID);
return await Connection.ExecuteAsync
(new CommandDefinition(query.ToString(), parameters));
}
public async Task<IEnumerable<OrderSummary>>
GetOrderSummariesAsync(int pageSize = 10, int pageNumber = 1,
Int64? orderHeaderID = null, Int16? orderStatusID = null,
Int32? customerID = null, Int32? employeeID = null, Int32? shipperID = null)
{
var query = new StringBuilder();
query.Append(" select ");
query.Append(" [OrderHeaderID], ");
query.Append(" [OrderStatusID], ");
query.Append(" [CustomerID], ");
query.Append(" [CustomerName], ");
query.Append(" [EmployeeID], ");
query.Append(" [EmployeeName], ");
query.Append(" [ShipperID], ");
query.Append(" [ShipperName], ");
query.Append(" [OrderDate], ");
query.Append(" [Total], ");
query.Append(" [CurrencyName], ");
query.Append(" [PaymentMethodName], ");
query.Append(" [DetailsCount] ");
query.Append(" from ");
query.Append(" [Sales].[OrderSummary] ");
query.Append(" where ");
query.Append(" (@orderHeaderID is null or [OrderHeaderID] = @orderHeaderID) and ");
query.Append(" (@orderStatusID is null or [OrderStatusID] = @orderStatusID) and ");
query.Append(" (@customerID is null or [CustomerID] = @customerID) and ");
query.Append(" (@employeeID is null or [EmployeeID] = @employeeID) and ");
query.Append(" (@shipperID is null or [ShipperID] = @shipperID) and ");
query.Append(" order by ");
query.Append(" [OrderHeaderID], _
[OrderStatusID], [CustomerID], [EmployeeID], [ShipperID] ");
query.Append(" offset @pageSize * (@pageNumber - 1) rows ");
query.Append(" fetch next @pageSize rows only ");
var parameters = new DynamicParameters();
parameters.Add("@orderHeaderID", orderHeaderID);
parameters.Add("@orderStatusID", orderStatusID);
parameters.Add("@customerID", customerID);
parameters.Add("@employeeID", employeeID);
parameters.Add("@shipperID", shipperID);
return await Connection.QueryAsync<OrderSummary>(query.ToString());
}
public async Task<IEnumerable<UfnGetCustomerContactResult>>
GetUfnGetCustomerContactsAsync(Int32? customerID)
{
var query = new StringBuilder();
query.Append(" select ");
query.Append(" [CustomerID], ");
query.Append(" [AddressLine1], ");
query.Append(" [AddressLine2], ");
query.Append(" [City], ");
query.Append(" [State], ");
query.Append(" [ZipCode], ");
query.Append(" [PhoneNumber] ");
query.Append(" from ");
query.Append(" [Sales].[ufnGetCustomerContact](@customerID) ");
var parameters = new DynamicParameters();
parameters.Add("@customerID", customerID);
return await Connection.QueryAsync
<UfnGetCustomerContactResult>(query.ToString(), parameters);
}
}
}
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 to set values for all settings because we have a lot of settings for DapperProjectSettings
. I think at this moment, it is simpler to create a console project to generate the code and then developer moves generated files for existing project and makes a code refactor if applies. CatFactory
doesn't have UI now because at the beginning of this project, .NET Core had no 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. :)
- We're working on continuous updates to provide better help for users.
Related Links
Code Improvements
- Add children for entity in get by key method (
Order
-> OrderDetail
) - Handling concurrency for update and delete operations
- Something else? Let me know your feedback in the comments :)
Bugs?
If you get any exception with CatFactory
packages, please use these links:
I'll appreciate your feedback to improve CatFactory
.
Source picture for "CatFactory
" concept =^^=
History
- 1st November, 2017: Initial version
- 27th November, 2017: Addition of trivia and workshop
- 8th February, 2018: Addition of project selections, database type map and event handlers in workshop
- 2nd May, 2018: Upgrade package version to beta
- 11th July, 2018: Update on scaffolding code
- 25th September, 2018: Update for code samples (CRUD) and
Dapper
project settings - 11th October, 2018: Addition of Scalar Functions and refactor for Article
- 25th October, 2018: Addition of Packages list
- 23th December, 2018: Database Update
- 12th May, 2019: Expand Scaffolding Engine