I implemented this in a project and can confirm it working.
Entity Framework 7 introduced Custom Reverse Engineering Templates which allow you to customize the generated code of scaffolding.
For the full solution we need two parts:
- Adjust the scaffolding template to produce the entity property with enum type
- Configure the EF model property to have a value conversion
For setting up the scaffolding template you can follow the documentation on Custom Reverse Engineering Templates.
dotnet new install Microsoft.EntityFrameworkCore.Templates
dotnet new ef-templates
This will produce two files:
CodeTemplates/
EFCore/
DbContext.t4
EntityType.t4
Because we only want to adjust the entity generating we can delete the DbContext.t4
.
In the EntityType.t4
locate the code generating for properties:
<#
var firstProperty = true;
foreach (var property in EntityType.GetProperties().OrderBy(p => p.GetColumnOrder() ?? -1))
{
// […]
usings.AddRange(code.GetRequiredUsings(property.ClrType));
var needsNullable = Options.UseNullableReferenceTypes && property.IsNullable && !property.ClrType.IsValueType;
var needsInitializer = Options.UseNullableReferenceTypes && !property.IsNullable && !property.ClrType.IsValueType;
#>
public <#= code.Reference(property.ClrType) #><#= needsNullable ? "?" : "" #> <#= property.Name #> { get; set; }<#= needsInitializer ? " = null!;" : "" #>
<#
firstProperty = false;
}
We want to customize this to use an enum type instead of the default db-to-.net value type.
Without change it uses IProperty ClrType - which for a database column type of int would have been mapped to a ClrType (.NET) of int (or System.Int32).
You may implement more extensive logic to enable configuring the generating process with a PropTypeMappings.txt
to have a more scoped configuration - a more general approach. For now, let's hack in it though to show the process more directly and shorter:
If we are on our entity property - determined by EntityType.Name
and property.Name
- we intend not to use the EF determined property.ClrType
but our own enum type. Because this enum type will be in an additional namespace, we also have to make sure the namespace is imported (or alternatively referenced fully).
For your example of entity name Foo
and property name State
and enum StateEnum
:
<#
var firstProperty = true;
foreach (var property in EntityType.GetProperties().OrderBy(p => p.GetColumnOrder() ?? -1))
{
// […]
Type propType = property.ClrType;
if (EntityType.Name == "Foo" && property.Name = "State") propType = Type.GetType("Full.Namespace.To.StateEnum, TypeAssemblyName");
usings.AddRange(code.GetRequiredUsings(propType));
var needsNullable = Options.UseNullableReferenceTypes && property.IsNullable && !property.ClrType.IsValueType;
var needsInitializer = Options.UseNullableReferenceTypes && !property.IsNullable && !property.ClrType.IsValueType;
#>
public <#= code.Reference(propType) #><#= needsNullable ? "?" : "" #> <#= property.Name #> { get; set; }<#= needsInitializer ? " = null!;" : "" #>
<#
firstProperty = false;
}
Note that:
- We determine the clr type to use in a variable
propType
- We replaced the the references to
property.ClrType
with references to propType
- To get the
Type
of enum we use the assembly qualified name
- The namespace import generating already happens through
usings.AddRange(code.GetRequiredUsings(
The EF model adjustment is much simpler - although we have two choices.
The generated DB context is a partial class with a partial method OnModelCreatingPartial
and a virtual method ConfigureConventions
.
In both alternative approaches we create a second file DbContext.ModelExtend.cs
and add a partial class declaration.
For the OnModelCreatingPartial
approach we implement the partial method and declare a conversion for the enum type:
partial void OnModelCreatingPartial(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Foo>().Property(x => x.Type).HasConversion<StateEnum>();
}
This will register the default enum converter as a value converter on the EF model. The enum value will be converted to the underlying type and then the database type for storage - and the reverse too.
For the ConfigureConventions
approach, we create our own converter so we will not have to configure each property - but the enum will be converted transparently. Effectively, EF will know of the type and convert it transparently.
internal class StateEnumTypeConverter : ValueConverter<StateEnum, int>
{
public GWFileTypeConverter()
: base(x => (byte)x, x => (GWFileType)x)
{
}
}
Note that you will have to use the correct underlying type of the enum. For cases other than the question example, depending on the enum declaration, it may for example be byte or long instead of int. Also note that this can not be generalized through generic template parameters because we use casting (for efficiency over runtime runtime-type operations).
And then we register the value converter for the enum type:
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder.Properties<StateEnum>().HaveConversion<StateEnumTypeConverter>();
}
To use a nullable property for a nullable database column mapping:
The EF-determined property.ClrType
is Nullable<int>
, but property.IsNullable
is false
. We used an assembly qualified name - but for Nullable we mix two assemblies.
It may be possible to get the Type
through typeof()
or other means if you only intend to edit the template, without enum type reference as string (e.g. a separate PropTypeMappings.txt
file).
But to use a string type reference; As I don't know how or if it is even possible to use two mixed assembly qualified type names, I went with a workaround fixup for now, keeping the propType as enum but fixup-ing the needsNullable
, replacing
var needsNullable = Options.UseNullableReferenceTypes && property.IsNullable && !propType.IsValueType;
with
var needsNullable = Options.UseNullableReferenceTypes && property.IsNullable && !propType.IsValueType;
// Nullability fixup for nullable enum
if (EntityType.Name == "Foo" && property.Name = "State") needsNullable = true;