Scaffolding TypeScript with CatFactory
Scaffolding TypeScript with CatFactory
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 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.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 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:
- This implementation breaks Dapper concept?
- 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.
CatFactory
This package provides all definitions for CatFactory engine, this is the core for child packages.
Namespaces:
- CodeFactory: Contains objects to perform code generation.
- Diagnostics: Contains objects for diagnostics.
- Markup: Contains objects for markup languages.
- ObjectOrientedProgramming: Contains objects to modeling definitions: classes, interfaces and enums.
- ObjectRelationalMapping: Contains objects for ORM: database, tables, views, scalar functions, table functions and stored procedures.
CatFactory.SqlServer
This packages contains logic to import existing databases from SQL Server instances.
Object | Supported |
---|---|
Tables | Yes |
Views | Yes |
Scalar Functions | Yes |
Table Functions | Yes |
Stored Procedures | Yes |
Sequences | Not yet |
Extended Properties | Yes |
Data types | Yes |
CatFactory.NetCore
This package contains code builders and definitions for .NET Core (C#).
Object | Feature | Supported |
---|---|---|
Interface | Inheritance | Yes |
Interface | Events | Yes |
Interface | Properties | Yes |
Interface | Methods | Yes |
Class | Inheritance | Yes |
Class | Events | Yes |
Class | Fields | Yes |
Class | Constructors | Yes |
Class | Properties | Yes |
Class | Methods | Yes |
Enum | Options | Yes |
Struct | All | Not yet |
CatFactory.EntityFrameworkCore
This package provides scaffolding for Entity Framework Core.
Object | Supported |
---|---|
Class for entity | Yes |
Class for view | Yes |
Class for table function result | Not yet |
Class for stored procedure result | Not yet |
Class for DbContext | Yes |
Class for entity configuration (table) | Yes |
Class for entity configuration (view) | Yes |
Interface for Repository | Yes |
Class for Repository | Yes |
Method for scalar function invocation | Yes |
Method for table function invocation | Not yet |
Method for stored procedure invocation | Not yet |
Entity Framework Core 2 Feature Compatibility Chart
Category | Feature | Supported |
---|---|---|
Modeling | Table splitting | Not yet |
Modeling | Owned types | Not yet |
Modeling | Model-level query filters | Not yet |
Modeling | Database scalar function mapping | Not yet |
High Performance | DbContext pooling | Not yet |
High Performance | Explicitly compiled queries | Not yet |
Read more here: New features in EF Core 2.0
CatFactory.AspNetCore
This package provides scaffolding for Asp .NET Core.
Feature | Supported |
---|---|
Controllers | Yes |
Requests | Yes |
Responses | Yes |
Client Scaffolding | Not yet |
Help Page for Web API | Not yet |
Unit Tests | Not yet |
Not yet |
CatFactory.Dapper
This package provides scaffolding for Dapper.
Object | Supported |
---|---|
Class for entity | Yes |
Class for view | Yes |
Class for table function result | Yes |
Class for stored procedure result | Not yet |
Interface for Repository | Yes |
Class for Repository | Yes |
Method for scalar function invocation | Yes |
Method for table function invocation | Yes |
Method for stored procedure invocation | Not yet |
CatFactory.TypeScript
This package provides scaffolding for Type Script.
Object | Feature | Supported |
---|---|---|
Interface | Inheritance | Yes |
Interface | Fields | Yes |
Interface | Properties | Yes |
Interface | Methods | Yes |
Class | Inheritance | Yes |
Class | Fields | Yes |
Class | Constructor | Yes |
Class | Properties | Yes |
Class | Methods | Yes |
Module | All | Not yet |
Trivia
- The name for this framework it was F4N1 before than CatFactory
- Framework's name is related to kitties
- Import logic uses sp_help stored procedure to retrieve the database object's definition, I learned that in my database course at college
- Load mapping for entities with MEF, it's inspired in "OdeToCode" (Scott Allen) article for Entity Framework 6.x
- Expose all settings in one class inside of project's definition is inspired on DevExpress settings for Web controls (Web Forms)
- This implementation exists thanks to Edson Ferreira and my person
Background
Code generation it's a common technique developers use to reduce time in code writing, I know the most programmers build a code generator in their professional lifes.
EF 6.x had a wizard for code generation, that tool generates DbContext and POCOs but there isn't code for Fluent API, Repositories and other things like those; with .NET Core there is a command line tool for code generation but we have the same scenario, there is generation only for DbContext and Entities; with CatFactory we're looking for a simple way to generate code with enterprise patterns, please don't forget this is an alpha version of CatFactory, don't pretend to have in this date a full version of code generation engine.
Why don't use code CodeDOM? CodeDOM it's a complex code generation engine, I don't saying CodeDOM sucks or something like that, but at this moment we're focus on generate code in the more simple way, maybe in the incoming versions we'll add some integration with CodeDOM.
I don't know about TypeScript code generation engines, there are some of course but I don't know any; the idea in this case is to install a nuget package, write C# code and run that code for generating TypeScript code.
Using the Code
Create a console application with VS 2017 for .NET Core and install the following packages:
Name | Version | Description |
CatFactory.TypeScript | 1.0.0-beta-sun-build15 | Provides scaffolding for TypeScript |
Save changes and build the project to resolve dependencies, now we can add any of the following codes according to the code we want to generate:
Scaffolding Interfaces
var definition = new TypeScriptInterfaceDefinition
{
Name = "IWarehouseService",
Methods =
{
new MethodDefinition("Response", "getProducts", new ParameterDefinition("string", "productName")),
new MethodDefinition("Response", "getProduct", new ParameterDefinition("Product", "entity"))
}
};
definition.AddImport("Response", "./Response");
definition.AddImport("Product", "./Product");
TypeScriptInterfaceBuilder.CreateFiles("C:\\Ts", string.Empty, true, definition);
Now check the output file:
import { Response } from './Response';
import { Product } from './Product';
export interface IWarehouseService {
getProducts(productName: string): Response;
getProduct(entity: Product): Response;
}
Scaffolding Classes
var definition = new TypeScriptClassDefinition
{
Name = "Product",
Fields =
{
new FieldDefinition("number", "id"),
new FieldDefinition("string", "productName"),
new FieldDefinition("number", "categoryId"),
new FieldDefinition("string", "unitPrice"),
new FieldDefinition("string", "description"),
new FieldDefinition("string", "tags"),
new FieldDefinition("Date", "releaseDate")
}
};
TypeScriptClassBuilder.CreateFiles("C:\\Ts", string.Empty, true, definition);
Now check the output file:
export class Product {
public id : number;
public productName : string;
public categoryId : number;
public unitPrice : string;
public description : string;
public tags : string;
public releaseDate : Date;
}
Scaffolding Service Class
var definition = new TypeScriptClassDefinition
{
Name = "WarehouseService",
BaseClass = "Service",
Constructors =
{
new TypeScriptClassConstructorDefinition
{
Lines =
{
new CodeLine("super();")
}
}
},
Methods =
{
new MethodDefinition("Response", "getProducts", new ParameterDefinition("string", "productName"))
{
Lines =
{
new TodoLine("Apply productName parameter to filter products by product name"),
new CodeLine(),
new CodeLine("return new Response();")
}
},
new MethodDefinition("Response", "getProduct", new ParameterDefinition("Product", "entity"))
{
Lines =
{
new TodoLine("Search product by id"),
new CodeLine(),
new CodeLine("return new Response();")
}
}
}
};
definition.AddImport("Response", "./Response");
definition.AddImport("Service", "./Service");
definition.AddImport("Product", "./Product");
TypeScriptClassBuilder.CreateFiles("C:\\Ts", string.Empty, true, definition);
Now check the output file:
import { Response } from './Response';
import { Service } from './Service';
import { Product } from './Product';
export class WarehouseService extends Service {
constructor() {
super();
}
public getProducts(productName: string): Response {
// todo: Apply productName parameter to filter products by product name
return new Response();
}
public getProduct(entity: Product): Response {
// todo: Search product by id
return new Response();
}
}
Refact Interface from Class Definition
var classDefinition = new TypeScriptClassDefinition
{
Name = "IPerson",
Properties =
{
new PropertyDefinition("number", "id"),
new PropertyDefinition("string", "firstName"),
new PropertyDefinition("string", "middleName"),
new PropertyDefinition("string", "lastName"),
new PropertyDefinition("string", "gender"),
new PropertyDefinition("Date", "birthDate")
}
};
TypeScriptClassBuilder.CreateFiles("C:\\Ts", string.Empty, true, classDefinition);
var interfaceDefinition = classDefinition.RefactInterface();
TypeScriptInterfaceBuilder.CreateFiles("C:\\Ts", string.Empty, true, interfaceDefinition);
Now check the class output file:
export class Person {
public m_id : number;
public m_firstName : string;
public m_middleName : string;
public m_lastName : string;
public m_gender : string;
public m_birthDate : Date;
public get id(): number {
return this.m_id;
}
public set id(value: number) {
this.m_id = value;
}
public get firstName(): string {
return this.m_firstName;
}
public set firstName(value: string) {
this.m_firstName = value;
}
public get middleName(): string {
return this.m_middleName;
}
public set middleName(value: string) {
this.m_middleName = value;
}
public get lastName(): string {
return this.m_lastName;
}
public set lastName(value: string) {
this.m_lastName = value;
}
public get gender(): string {
return this.m_gender;
}
public set gender(value: string) {
this.m_gender = value;
}
public get birthDate(): Date {
return this.m_birthDate;
}
public set birthDate(value: Date) {
this.m_birthDate = value;
}
}
Now check the interface output file:
export interface IPerson {
id: number;
firstName: string;
middleName: string;
lastName: string;
gender: string;
birthDate: Date;
}
How does it work? First, we need to create a definition of class or interface, then we set the name for object, inheritance and implements, next, add the members for definition: fields, properties and methods; next, use TypeScriptClassBuilder
for classes and TypeScriptInterfaceBuilder
for interfaces to scaffold definitions as ts files. Please make sure you have the rights for output directory before of scaffolding.
Scaffolding Enums
var definition = new TypeScriptEnumDefinition
{
Name = "Direction",
Sets =
{
new NameValue { Name = "Up", Value = "100" },
new NameValue { Name = "Down", Value = "200" },
new NameValue { Name = "Right", Value = "300" },
new NameValue { Name = "Left", Value = "400" }
}
};
TypeScriptEnumBuilder.CreateFiles("C:\\Ts", string.Empty, true, definition);
Now check the interface output file:
enum Direction {
Up = 100,
Down = 200,
Right = 300,
Left = 400
}
Code Improvements
- Add support for modules
- Add documentation for ts definitions
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 =^^=
Points of Interest
- We can add the logic to generate TypeScript code from an array of definitions, in this guide, we're working with one single definition.
- How can we add the code for methods? Methods have a
list
property ofCodeLine
, so just add all lines inlist
andCodeBuilder
object will add all lines inside of method definition.
Related Links
History
- 19th January, 2017: Initial version
- 9th July, 2017: Addition of refact interface method
- 7th March, 2018: Addition of refact class from anonymous object
- 2nd May, 2018: Upgrade package version
- 26th October, 2018: Refactor for article sections
- 10th November, 2018: Addition of enums