Scaffold EF Core models using Handlebars templates.
View the EF Core Community Standup episode featuring this framework for scaffolding entities with Handlebars templates. The demos for the episode can be found on this GitHub repo.
Before creating a pull request, please refer to the Contributing Guidelines.
dotnet-ef
tool.
dotnet tool install --global dotnet-ef
dotnet-ef
tool.
dotnet tool update --global dotnet-ef
(localdb)\MsSqlLocalDb
.NorthwindSlim.sql
file from https://github.com/TrackableEntities/northwind-slim.docker run -e "ACCEPT_EULA=1" -e "MSSQL_SA_PASSWORD=MyPass@word" -e "MSSQL_PID=Developer" -e "MSSQL_USER=SA" -p 1433:1433 -d --name=sql mcr.microsoft.com/azure-sql-edge
sa
and password MyPass@word
NorthwindSlim.sql
file from https://github.com/TrackableEntities/northwind-slim.TargetFramework
in .csproj file to net8.0
.
ImplicitUsings
to enable
.Nullable
to enable
.8.0.0
or later:
EnableNullableReferenceTypes
option from services.AddHandlebarsScaffolding
in ScaffoldingDesignTimeServices.ConfigureDesignTimeServices
.
dotnet ef dbcontext scaffold
command to regenerate entities.
Create a new .NET 8 class library.
Add EF Core SQL Server and Tools NuGet packages.
Microsoft.EntityFrameworkCore.SqlServer
Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Design
Add the EntityFrameworkCore.Scaffolding.Handlebars NuGet package:
EntityFrameworkCore.Scaffolding.Handlebars
dotnet add package EntityFrameworkCore.Scaffolding.Handlebars
Remove Class1.cs and add a ScaffoldingDesignTimeServices class.
IDesignTimeServices
by adding a ConfigureDesignTimeServices
method
that calls services.AddHandlebarsScaffolding
.ReverseEngineerOptions
enum to indicate if you wish
to generate only entity types, only a DbContext class, or both (which is the default).public class ScaffoldingDesignTimeServices : IDesignTimeServices
{
public void ConfigureDesignTimeServices(IServiceCollection services)
{
services.AddHandlebarsScaffolding();
}
}
Open a command prompt at the project level and use the dotnet ef
tool to reverse engineer a context and models from an existing database.
dotnet ef dbcontext scaffold -h
dotnet ef dbcontext scaffold "Data Source=(localdb)\MSSQLLocalDB; Initial Catalog=NorthwindSlim; Integrated Security=True" Microsoft.EntityFrameworkCore.SqlServer -o Models -c NorthwindSlimContext -f --context-dir Contexts
-d
to the command to use data annotations. You will need to add the System.ComponentModel.Annotations package to a .NET Standard library containing linked entity classes.You may edit any of the template files which appear under the CodeTemplates folder.
Take advantage of C# nullable reference types by enabling them in your .csproj file. (This is by default in .NET 6 or greater.)
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
Non-nullable properties will include the null forgiving operator.
public partial class Product
{
public string ProductName { get; set; } = null!;
public decimal? UnitPrice { get; set; }
}
You can optionally exclude certain tables from code generation. These may also be qualified by schema name.
services.AddHandlebarsScaffolding(options =>
{
// Exclude some tables
options.ExcludedTables = new List<string> { "dbo.Territory" };
});
You may find it useful to add your own custom template data for use in your Handlebars templates. For example, the model namespace is not included by default in the DbContext
class import statements. To compensate you may wish to add a models-namespace
template to the DbImports.hbs template file.
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata; // Comment
using {{models-namespace}};
Likewise you may wish to specify the name of a model base class in the same way.
public partial class {{class}} : {{base-class}}
{
{{{> constructor}}}
{{> properties}}
}
You can then set the value of these templates in the TemplateData
property of HandlebarsScaffoldingOptions
.
services.AddHandlebarsScaffolding(options =>
{
// Add custom template data
options.TemplateData = new Dictionary<string, object>
{
{ "models-namespace", "ScaffoldingSample.Models" },
{ "base-class", "EntityBase" }
};
});
You can generate models in different folders by database schema.
services.AddHandlebarsScaffolding(options =>
{
// Put Models into folders by DB Schema
options.EnableSchemaFolders = true;
});
Handlebars templates may be embdedded in a separate .NET Standard project that can be shared among multiple .NET Core scaffolding projects. Simply copy the CodeTemplates folder to the .NET Standard project and edit the .csproj file to embed them as a resource in the assembly.
<ItemGroup>
<EmbeddedResource Include="CodeTemplates\**\*.hbs" />
</ItemGroup>
Then reference the .NET Standard project from the .NET Core projects and specify the templates assembly when adding Handlebars scaffolding in the ScaffoldingDesignTimeServices
class.
public class ScaffoldingDesignTimeServices : IDesignTimeServices
{
public void ConfigureDesignTimeServices(IServiceCollection services)
{
// Get templates assembly
var templatesAssembly = Assembly.Load("TemplatesAssembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null");
// Add Handlebars scaffolding using embedded templates templates
services.AddHandlebarsScaffolding(options => options.EmbeddedTemplatesAssembly = templatesAssembly);
}
}
You can register Handlebars helpers in the ScaffoldingDesignTimeServices
where setup takes place.
myHelper
below.context
parameter of the helper method provides model data injected by the Handlebars scaffolding extension.AddHandlebarsHelpers
extension method.{{my-helper}}
You can pass transform functions to AddHandlebarsTransformers
in order to customize generation of entity type definitions, including class names, constructors and properties.
public class ScaffoldingDesignTimeServices : IDesignTimeServices
{
public void ConfigureDesignTimeServices(IServiceCollection services)
{
// Add Handlebars scaffolding templates
services.AddHandlebarsScaffolding(options =>
{
// Generate both context and entities
options.ReverseEngineerOptions = ReverseEngineerOptions.DbContextAndEntities;
// Enable Nullable reference types
options.EnableNullableReferenceTypes = true;
// Put Models into folders by DB Schema
//options.EnableSchemaFolders = true;
// Exclude some tables
options.ExcludedTables = new List<string> { "Territory", "EmployeeTerritories" };
// Add custom template data
options.TemplateData = new Dictionary<string, object>
{
{ "models-namespace", "ScaffoldingSample.Models" },
{ "base-class", "EntityBase" }
};
});
// Register Handlebars helper
var myHelper = (helperName: "my-helper", helperFunction: (Action<TextWriter, Dictionary<string, object>, object[]>) MyHbsHelper);
// Add optional Handlebars helpers
services.AddHandlebarsHelpers(myHelper);
// Add Handlebars transformer for Country property
services.AddHandlebarsTransformers(
propertyTransformer: p =>
p.PropertyName == "Country"
? new EntityPropertyInfo("Country?", p.PropertyName, false)
: new EntityPropertyInfo(p.PropertyType, p.PropertyName, p.PropertyIsNullable));
// Add Handlebars transformer for Id property
//services.AddHandlebarsTransformers2(
// propertyTransformer: (e, p) =>
// $"{e.Name}Id" == p.PropertyName
// ? new EntityPropertyInfo(p.PropertyType, "Id", false)
// : new EntityPropertyInfo(p.PropertyType, p.PropertyName, p.PropertyIsNullable));
// Add optional Handlebars transformers
//services.AddHandlebarsTransformers2(
// entityTypeNameTransformer: n => n + "Foo",
// entityFileNameTransformer: n => n + "Foo",
// constructorTransformer: (e, p) => new EntityPropertyInfo(p.PropertyType + "Foo", p.PropertyName + "Foo"),
// propertyTransformer: (e, p) => new EntityPropertyInfo(p.PropertyType, p.PropertyName + "Foo"),
// navPropertyTransformer: (e, p) => new EntityPropertyInfo(p.PropertyType + "Foo", p.PropertyName + "Foo"));
}
// Sample Handlebars helper
void MyHbsHelper(TextWriter writer, Dictionary<string, object> context, object[] parameters)
{
writer.Write("// My Handlebars Helper");
}
}
There are times when you might like to modify generated code, for example, by adding a HasConversion
method to an entity property in the OnModelCreating
method of the generated class that extends DbContext
. However, doing so may prove futile because added code would be overwritten the next time you run the dotnet ef dbcontext scaffold
command.
DbContext
class is already defined using the partial
keyword, and it contains a partial OnModelCreatingPartial
method that is invoked at the end of the OnModelCreating
method.DbContext
class, and define it as partial
. Then add a OnModelCreatingPartial
method with the same signature as the partial method defined in the generated DbContext
class.// Place in separate class file (NorthwindSlimContextPartial.cs)
public partial class NorthwindSlimContext
{
partial void OnModelCreatingPartial(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Employee>()
.Property(e => e.Country)
.HasConversion(
v => v.ToString(),
v => (Country)Enum.Parse(typeof(Country), v));
modelBuilder.Entity<Customer>()
.Property(e => e.Country)
.HasConversion(
v => v.ToString(),
v => (Country)Enum.Parse(typeof(Country), v));
}
}
To generate TypeScript entities simply pass LanguageOptions.TypeScript
to AddHandlebarsScaffolding
. Since generating a DbContext
class is strictly a server-side concern, you should also pass ReverseEngineerOptions.EntitiesOnly
to AddHandlebarsScaffolding
.
public class ScaffoldingDesignTimeServices : IDesignTimeServices
{
public void ConfigureDesignTimeServices(IServiceCollection services)
{
// Generate entities only
var options = ReverseEngineerOptions.EntitiesOnly;
// Generate TypeScript files
var language = LanguageOptions.TypeScript;
// Add Handlebars scaffolding templates
services.AddHandlebarsScaffolding(options, language);
}
}
For an example of this approach, see
MyHbsCSharpEntityTypeGenerator
in the ef-core-community-handlebars repo.
To take full control of context and entity generation, you can extend HbsCSharpDbContextGenerator
and HbsCSharpEntityTypeGenerator
, overriding select virtual methods. Then register your custom generators in ScaffoldingDesignTimeServices.ConfigureDesignTimeServices
.
For example, you may want to add property-isprimarykey
to the template data in order to insert some code or a comment.
Add a MyHbsCSharpEntityTypeGenerator
to the .Tooling project.
HbsCSharpEntityTypeGenerator
.GenerateProperties
.GenerateProperties
method.Add code that inserts property-isprimarykey
into the template data.
protected override void GenerateProperties(IEntityType entityType)
{
var properties = new List<Dictionary<string, object>>();
foreach (var property in entityType.GetProperties().OrderBy(p => p.GetColumnOrdinal()))
{
// Code elided for clarity
properties.Add(new Dictionary<string, object>
{
{ "property-type", propertyType },
{ "property-name", property.Name },
{ "property-annotations", PropertyAnnotationsData },
{ "property-comment", property.GetComment() },
{ "property-isnullable", property.IsNullable },
{ "nullable-reference-types", _options?.Value?.EnableNullableReferenceTypes == true },
// Add new item to template data
{ "property-isprimarykey", property.IsPrimaryKey() }
});
}
var transformedProperties = EntityTypeTransformationService.TransformProperties(properties);
// Add to transformed properties
for (int i = 0; i < transformedProperties.Count ; i++)
{
transformedProperties[i].Add("property-isprimarykey", properties[i]["property-isprimarykey"]);
}
TemplateData.Add("properties", transformedProperties);
}
MyHbsCSharpEntityTypeGenerator
in ScaffoldingDesignTimeServices.ConfigureDesignTimeServices
.
services.AddSingleton<ICSharpEntityTypeGenerator, MyHbsCSharpEntityTypeGenerator>();
property-isprimarykey
.
{{#if property-isprimarykey}} // Primary Key{{/if}}
dotnet ef dbcontext scaffold
command from above.