Click here to Skip to main content
15,392,813 members
Articles / Programming Languages / C#
Tip/Trick
Posted 8 Jan 2017

Tagged as

Stats

35.5K views
20 bookmarked

Scaffolding View Models with CatFactory

Rate me:
Please Sign up or sign in to vote.
4.79/5 (9 votes)
3 Jun 2019CPOL8 min read
Scaffolding View Models 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:

C#
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:

C#
// 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:

C#
// 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.ForceOverwrite = true;
    settings.AuditEntity = new AuditEntity("CreationUser", "CreationDateTime", "LastUpdateUser", "LastUpdateDateTime");
    settings.ConcurrencyToken = "Timestamp";
    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

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

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

Select by key

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

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

Insert

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

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

Update

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

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

Delete

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

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

Select by

C#
// 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:

C#
public interface IEntity
{
	Table ToTable();
}

Then create a class with name Shipper and implement interface:

C#
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

C#
// 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#:

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:

C#
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:

C#
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:

C#
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:

C#
var interfaceDefinition = classDefinition.RefactInterface();

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

This is the output code:

C#
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

  • C#

Software Prerequisites

  • .NET Core
  • Visual Studio 2017 or VS Code

Using the Code

Step 01 - Create Console Project

Create a new console project with Visual Studio.

Step 02 - Add Package for Console Project

Add the following NuGet package for project:

Name Version Description
CatFactory.NetCore 1.0.0-beta-sun-build28 Provides object model and scaffolding for .NET Core (C#)

Save changes and build the project.

Now add the following code in Main method for Program.cs file:

C#
var definition = new CSharpClassDefinition
{
    Namespaces =
    {
        "System",
        "System.ComponentModel"
    },
    Namespace = "DesignPatterns",
    Name = "Product",
    Implements =
    {
        "INotifyPropertyChanged"
    },
    Events =
    {
        new EventDefinition("PropertyChangedEventHandler", "PropertyChanged")
    }
};

definition.AddViewModelProperty("int?", "ProductID");
definition.AddViewModelProperty("string", "ProductName");
definition.AddViewModelProperty("int?", "SupplierID");
definition.AddViewModelProperty("int?", "CategoryID");
definition.AddViewModelProperty("string", "QuantityPerUnit");
definition.AddViewModelProperty("decimal?", "UnitPrice");
definition.AddViewModelProperty("short?", "UnitsInStock");
definition.AddViewModelProperty("short?", "UnitsOnOrder");
definition.AddViewModelProperty("short?", "ReorderLevel");
definition.AddViewModelProperty("bool?", "Discontinued");

How It Works?

  1. Set the value for Namespace property, that's class' namespace.
  2. Set the name of the class in Name property.
  3. Add all namespaces for class.
  4. This class has a dependency from INotifyPropertyChanged, it's an interface that's the reason why we added on Implements property
  5. For this case, we want to notify all changes in properties, so we need to add the event PropertyChanged in class definition.
  6. We add all properties for class definition, as we know the properties aren't automatic because we need to raise the event PropertyChanged so we need to specify the code for set block, the method AddViewModelProperty is an extension method, this method adds a field for class definition and a property with get and set block also, an invocation for notify event.
  7. As a last step, we create an instance of CSharpClassBuilder class and set the object definition and output directory in order to generate the file with CreateFile() method invocation.

Now run the project and check the output file:

C#
using System;
using System.ComponentModel;

namespace DesignPatterns
{
 public class Product : INotifyPropertyChanged
 {
  public event PropertyChangedEventHandler PropertyChanged;

  private int? m_productID;
  private string m_productName;
  private int? m_supplierID;
  private int? m_categoryID;
  private string m_quantityPerUnit;
  private decimal? m_unitPrice;
  private short? m_unitsInStock;
  private short? m_unitsOnOrder;
  private short? m_reorderLevel;
  private bool? m_discontinued;

  public int? ProductID
  {
   get
   {
    return m_productID;
   }
   set
   {
    if (m_productID != value)
    {
     m_productID = value;
    
     PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ProductID)));
    }
   }
  }

  public string ProductName
  {
   get
   {
    return m_productName;
   }
   set
   {
    if (m_productName != value)
    {
     m_productName = value;
    
     PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ProductName)));
    }
   }
  }

  public int? SupplierID
  {
   get
   {
    return m_supplierID;
   }
   set
   {
    if (m_supplierID != value)
    {
     m_supplierID = value;
    
     PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SupplierID)));
    }
   }
  }

  public int? CategoryID
  {
   get
   {
    return m_categoryID;
   }
   set
   {
    if (m_categoryID != value)
    {
     m_categoryID = value;
    
     PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CategoryID)));
    }
   }
  }

  public string QuantityPerUnit
  {
   get
   {
    return m_quantityPerUnit;
   }
   set
   {
    if (m_quantityPerUnit != value)
    {
     m_quantityPerUnit = value;
    
     PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(QuantityPerUnit)));
    }
   }
  }

  public decimal? UnitPrice
  {
   get
   {
    return m_unitPrice;
   }
   set
   {
    if (m_unitPrice != value)
    {
     m_unitPrice = value;
    
     PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(UnitPrice)));
    }
   }
  }

  public short? UnitsInStock
  {
   get
   {
    return m_unitsInStock;
   }
   set
   {
    if (m_unitsInStock != value)
    {
     m_unitsInStock = value;
    
     PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(UnitsInStock)));
    }
   }
  }

  public short? UnitsOnOrder
  {
   get
   {
    return m_unitsOnOrder;
   }
   set
   {
    if (m_unitsOnOrder != value)
    {
     m_unitsOnOrder = value;
    
     PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(UnitsOnOrder)));
    }
   }
  }

  public short? ReorderLevel
  {
   get
   {
    return m_reorderLevel;
   }
   set
   {
    if (m_reorderLevel != value)
    {
     m_reorderLevel = value;
    
     PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ReorderLevel)));
    }
   }
  }

  public bool? Discontinued
  {
   get
   {
    return m_discontinued;
   }
   set
   {
    if (m_discontinued != value)
    {
     m_discontinued = value;
    
     PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Discontinued)));
    }
   }
  }
 }
}

Points of Interest

  • Inside the set block, there is an invocation like PropertyChanged?. That's because in .NET Core, we can invoke a method on instance in safe mode, the equivalence for that in previous versions of C# is if (PropertyChanged != null) { ... }, I think the extension method can have a flag parameter to indicate if we want to simply the event invocation, also there is a using of nameof operator, that operator gets the name from existing member, the benefit is there isn't magic string and if we change the name of member (property, method, etc.) the compiler shows a compilation error.
  • CatFactory.NetCore provides a model for .NET Core.
  • There is a code builder for C#, code builder requires an object definition for .NET Core to know object members: events, fields, constructors, properties and methods.
  • CatFactory.NetCore is independent from other packages such as CatFactory.EntityFrameworkCore, CatFactory.Dapper, etc. but with this package, we can build specific packages to scaffold C#, take a look at the links section to know more about this.

Related Links

Code Improvements

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 =^^=

Source Concept for CatFactory

History

  • 8th January, 2017: Initial version
  • 22nd November, 2017: Update for alpha 3 version
  • 30th Abril, 2018: Update for beta version
  • 28th October, 2018: Refactor for Article sections

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

 
PraiseGood job Pin
Yahya Mohammed Ammouri30-Apr-18 20:08
MemberYahya Mohammed Ammouri30-Apr-18 20:08 
GeneralRe: Good job Pin
HHerzl11-May-18 0:22
MemberHHerzl11-May-18 0:22 
GeneralMy vote of 5 Pin
mr. Duan11-Jan-17 15:33
professionalmr. Duan11-Jan-17 15:33 
GeneralRe: My vote of 5 Pin
HHerzl23-Jan-17 13:25
MemberHHerzl23-Jan-17 13:25 
QuestionMessage Closed Pin
11-Jan-17 5:54
MemberNithya Kk11-Jan-17 5:54 
AnswerRe: Nice article Pin
HHerzl23-Jan-17 13:25
MemberHHerzl23-Jan-17 13:25 
QuestionNeeds More Explanation Pin
#realJSOP8-Jan-17 23:24
mva#realJSOP8-Jan-17 23:24 
AnswerRe: Needs More Explanation Pin
HHerzl11-Jan-17 17:21
MemberHHerzl11-Jan-17 17:21 

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.