40

I'm using EF Core with database-first approach using the "Scaffold-DbContext"-command to generate my DbContext / Entities.

How can I instruct Scaffold-DbContext that a certain field in a certain table should generate code to use an Enum instead of just an int?

This is how you used to do it in regular EF: Map Enum to Entity Properties

Example

This enum is already defined in code:

public enum StateEnum {
  Ok = 1,
  Fail = 2
}

This is what Scaffold-DbContext gives me

public partial class Foo
{
    public int Id { get; set; }
    public int State { get; set; }
}

This is what I want it to create:

public partial class Foo
{
    public int Id { get; set; }
    public StateEnum State { get; set; }
}
Kissaki
  • 8,810
  • 5
  • 40
  • 42
Bassebus
  • 682
  • 1
  • 6
  • 14
  • 6
    At present there is no way to create enum property while running scaffold-dbcontext. It is simply because, enums are stored as int (or enum's underlying type in database) and when scaffolding the model, EF looks at metadata hence has no info about the column being int vs enum. You can always change the type of property to enum from int after scaffolding and it would just work fine. – Smit May 31 '17 at 05:56
  • Possible duplicate of [Does EF7 support enums?](https://stackoverflow.com/questions/35298829/does-ef7-support-enums) – schnitty Apr 19 '18 at 06:06
  • @schnitty Not a duplicate. That one asks about read/value conversion. This question asks about scaffolding (which is a superset). – Kissaki Aug 04 '23 at 06:24

9 Answers9

34

Doesn't value conversion in EF Core 2.1 do what you need now?

https://learn.microsoft.com/en-us/ef/core/modeling/value-conversions

Quick Example:

  entity.Property(e => e.MyEnumField)
            .HasMaxLength(50)
            .HasConversion(
                v => v.ToString(),
                v => (MyEnum)Enum.Parse(typeof(MyEnum),v))
                .IsUnicode(false);
MrKobayashi
  • 1,045
  • 2
  • 12
  • 19
  • 1
    This does work, but does not consider the OP's original requirements of using the Scaffold command which will overwrite the contents of the `OnModelCreating` method every time it is ran. – Ryan Griffith Apr 24 '21 at 17:37
  • 4
    OnModelCreating is not the problem here, because at its end, there is a OnModelCreatingPartial partial method, so you can add your configuration there (in a separate file). The problem here is that you need to define your property type (in you entity) as an Enum. Which you cannot do because the entity is generated using Scaffold-DbContext in the question. And every manual modification done after the generation will be overriden after reexecuting Scaffold-DbContext So for me, this does not answer the question – ihebiheb Jun 11 '22 at 23:58
34

Starting with Entity Framework Core 2.1, EF supports Value Conversions to specifically address scenarios where a property needs to be mapped to a different type for storage.

Specifically for Enums, you can use the provided EnumToStringConverter or EnumToNumberConverter.

Dejan
  • 9,150
  • 8
  • 69
  • 117
  • 2
    Finally! Thanks! – Bassebus Sep 26 '18 at 15:32
  • 3
    The problem with all of these answers is that they store the Enum as either a string or number column of the enum's parent table. From a database point of view, this is wrong. From the database point of view, this enum should be stored as an ID field in the parent table, which references a lookup table via a foreign key. – Pavel Dec 04 '18 at 20:59
  • 9
    I disagree. An enum is CODE , so there is no database representation otherwise you can simply add another value to the database which renders then the code invalid as that enum value is unknown. – user1029883 Dec 26 '18 at 16:28
  • 2
    However, there is a big caveat here: "Use of value conversions may impact the ability of EF Core to translate expressions to SQL. A warning will be logged for such cases. Removal of these limitations is being considered for a future release" https://learn.microsoft.com/en-us/ef/core/modeling/value-conversions – Cito Jan 09 '19 at 10:44
  • 12
    Scaffold-DbContext will create/overwrite my DbContext, so it overrides the `OnModelCreating`, how do you handle that? – jsgoupil Apr 06 '20 at 00:34
  • @jsgoupil The scaffolded DbContext is a `partial class` , so you can add your own overridden methods in a separate file that won't be overwritten. – Dai Nov 18 '21 at 01:33
  • @Dai Nope, I need to add things inside `OnModelCreating` – jsgoupil Nov 18 '21 at 21:43
  • 6
    @jsgoupil there is a OnModelCreatingPartial at the end of OnModelCreating. Its purpose is exactly what you want to achieve – ihebiheb Jun 12 '22 at 00:00
  • @Dai overriding OnModelCreating allows the converter being known across scaffolds. But the entity property will still be generated as - and when manually changed overwritten to - the enum-underlying type instead of the enum type. So that's still missing. – Kissaki Jul 26 '23 at 14:13
12

I got here because of the question title. Not sure if this works in the "Scaffold-DbContext" but it does in DbContext (Microsoft.EntityFrameworkCore 2.0.1.0) by setting the base type of the enum explicitly even if the default underlying type of enumeration elements is int. You can use Fluent API to set default values with it too (especially this case where the enum starts with 1).

public enum StateEnum : int
{
    Ok = 1,
    Fail = 2
}

The approved types for an enum are byte, sbyte, short, ushort, int, uint, long, or ulong.

So I think this would work for any of these. enum (C# Reference)

public class MyDbContext : DbContext
{      
    protected override void OnModelCreating(ModelBuilder builder) 
    {
        builder.Entity<Foo>().Property(x => x.State).HasDefaultValue(StateEnum.Ok);
    }
}
A.J.Bauer
  • 2,803
  • 1
  • 26
  • 35
  • I had a legacy table in our DB which was using a small int to represent an enum, so i needed to change the enum to inherit from short: ( MyType: short) for ef core to convert it correctly. Thanks!! – Timmy Fuller Jul 19 '18 at 09:25
  • Alternatively, `.HasConversion();` would work as opposed to `.HasDefaultValue(..)` – Fraze Nov 01 '18 at 18:26
  • This assumes the entity property already exists and is of enum type? A scaffold will generate an int property? – Kissaki Jul 21 '23 at 07:26
11

The accepted answers are not solving the problem. The questions states "How can I instruct Scaffold-DbContext that a certain field in a certain table should generate code to use an Enum instead of just an int?" A lot of answers are stating that with Entity Framework Core 2.1, there is now support for Value Conversions. That is useful if you use Code-first, NOT Database-first. The Scaffold-DbContext will overwrite DBContext every time. Personally i have no problem with the enum being a integer in the database. But I don't want to use integers within the code.

You could either

  1. Change the DBContext to use ValueConversion. You will loose that code every time you scaffold.
  2. Change the Ints in you Entities to the specific Enum. It works fine but you will also loose that when you scaffold.
  3. Create a partial class where you create a methods called GetStateEnum() and SetStateEnum(StateEnum stateEnum) etc. It's quite verbose but it will stay between scaffolding.
  4. There is something called Source Generators that maybe could solve this in the future. I have not found an easy solution to this.

I chose alternative 2. It's the most simple one I have found so far and it's quite easy to revert the classes with git-compare. I can change all my enums within a minute.

If someone has a better solution. Please tell me.

Bjorn
  • 181
  • 2
  • 11
4

Try this solution:

public enum StateEnum {
      Ok = 1,
      Fail = 2
}

public partial class Foo
{
    public int Id { get; set; }
    public int StateId { get; set; }
    public StateEnum State
    {
        get => (StateEnum)StateId;
        set => StateId = (int)value;
    }
}
Yann Duran
  • 3,842
  • 1
  • 24
  • 24
Aliaksei Zhukau
  • 217
  • 4
  • 12
2

You can have you enum (U) and an entity (T) representing enum values in the database

public static T[] BuildEntityObjectsFromEnum<T, U>() where U: Enum where T : new()
    {
        var listObjectsToReturn = new List<T>();
        Dictionary<string, int> dictionary = Enum.GetValues(typeof(U)).Cast<U>().ToDictionary(t => t.ToString(), t =>  Convert.ToInt32(t));

        foreach (var item in dictionary)
        {
            var newObject = new T();
            Type classType = typeof(T);
            classType.GetProperties()[0].SetValue(newObject, item.Value); // Enum int id
            classType.GetProperties()[1].SetValue(newObject, item.Key); // Enum string value
            listObjectsToReturn.Add(newObject);
        }
        return listObjectsToReturn.ToArray();
    }

Then you can seed the table from the enum

modelBuilder.Entity<T>().HasData(BuildEntityObjectsFromEnum<T,U>());
sofsntp
  • 1,964
  • 23
  • 34
  • 1
    I like the idea, but could you please provide a concrete example? Specifically, I need an example of what T look like. – Greg Z. Jan 20 '23 at 15:52
  • Can you describe the idea behind this? Describe the intention / concept? – Kissaki Jul 21 '23 at 07:28
2

Following the link provided in the accepted answer, I used pre-defined conversion for a code first approach and it works, using Entity Framework Core 5:

modelBuilder.Entity<Model>(model => {
    model.Property(m => m.EnumType)
        .HasConversion<int>();
});
netotz
  • 193
  • 1
  • 4
  • 12
  • This assumes the entity property already exists and is of enum type? A scaffold will generate an int property? – Kissaki Jul 21 '23 at 07:26
1

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:

  1. Adjust the scaffolding template to produce the entity property with enum type
  2. 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;
Kissaki
  • 8,810
  • 5
  • 40
  • 42
0

Accepted answer is correct (i will not duplicate it here), and OnModelCreatingPartial solve partially the scaffolding issue. But it didn't solve the problem of the property type in the scaffolded Entity. And manual revert base on git diff didn't work for us.

I addition of the accepted answer, my final approach was to wrap the scaffolding command in a PowerShell script, which also parse and edit the generated files automatically.

On the assumption that all entities that have a State property match the same enum:

$modelFiles = Get-ChildItem $modelsPath *.cs -exclude "*.partial.cs" -rec
foreach ($file in $modelFiles)
{
    (Get-Content $file.PSPath) |
    Foreach-Object { $_ -replace "public int State { get; set; }", "public StateEnum State { get; set; }" } |
    Out-File $file.PSPath -Encoding utf8BOM
}
fhewitt
  • 91
  • 1
  • 5