dotnet / efcore

EF Core is a modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations.
https://docs.microsoft.com/ef/
MIT License
13.49k stars 3.13k forks source link

Allow underlying type to be used for HasDefaultValue when the property is an enum #32539

Open flier268 opened 7 months ago

flier268 commented 7 months ago

I had a table [Plu]

USE [FPSDI771]
GO

/****** Object:  Table [dbo].[Plu]    Script Date: 2023/12/7 下午 01:43:23 ******/
SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE TABLE [dbo].[Plu](
    [PluCode] [bigint] NOT NULL,
    [CommName] [nvarchar](64) NOT NULL,
    [CustomDate1Flag] [int] NOT NULL,
 CONSTRAINT [PK_Plu_1] PRIMARY KEY CLUSTERED 
(
    [PluCode] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO

ALTER TABLE [dbo].[Plu] ADD  CONSTRAINT [DF_Plu_CommName]  DEFAULT ('') FOR [CommName]
GO

ALTER TABLE [dbo].[Plu] ADD  CONSTRAINT [DF_Plu_CustomDate1Flag]  DEFAULT ((1)) FOR [CustomDate1Flag]
GO

I have the following C# code:

    public Plu
    {
        public long PluCode { get; set; }
        public string CommName { get; set; }
        public EDateIsPrintFlag CustomDate1Flag { get; set; }
    }

    [JsonConverter(typeof(JsonNumberEnumConverter<EDateIsPrintFlag>))]
    public enum EDateIsPrintFlag
    {
        NotPrint = 0,
        Print = 1,
    }

圖片

I am using the Entity Framework Core to scaffold my database context. The command I use is:

dotnet tool update --global dotnet-ef
dotnet ef dbcontext scaffold "Data Source=/*connectionstring*/" "Microsoft.EntityFrameworkCore.SqlServer" -o ./EntityModels -c EFCoreContext --context-dir DataContexts -f --data-annotations --project FPS2_Desktop.DB --no-onconfiguring

With EF Core version 8.0.0, the generated code is:

modelBuilder.Entity<Plu>(entity =>
{
    entity.HasKey(e => e.PluCode).HasName("PK_Plu_1");

    entity.Property(e => e.PluCode).ValueGeneratedNever();
    entity.Property(e => e.CommName).HasDefaultValue("");
    entity.Property(e => e.CustomDate1Flag).HasDefaultValue(1);
});

However, this results in an error:

System.InvalidOperationException: 'Cannot set default value '1' of type 'System.Int32' on property 'CustomDate1Flag' of type 'Project.Enums.EDateIsPrintFlag' in entity type 'A'.'

EF Core version: 8.0.0

Previously, with an older version of EF Core, the generated code was: (7.0.14)

modelBuilder.Entity<Plu>(entity =>
{
    entity.HasKey(e => e.PluCode).HasName("PK_Plu_1");

    entity.Property(e => e.PluCode).ValueGeneratedNever();
    entity.Property(e => e.CommName).HasDefaultValueSql("('')");
    entity.Property(e => e.CustomDate1Flag).HasDefaultValueSql("((1))");
});

But this approach no longer works after upgrading to version 8.0.0. Can you help me correct this?

EF Core version: 8.0.0 Database provider: Microsoft.EntityFrameworkCore.SqlServer Target framework: .NET 8 Operating system: Win11 IDE: Visual Studio 2022 17.8.2

roji commented 7 months ago

It seems that after scaffolding your database, you're changing the type of the CustomDate1Flag property - which should have been scaffolded as a simple int based on the database schema - to an enum type (EDateIsPrintFlag); at that point the default value scaffolded becomes incompatible.

@ajcvickers maybe we can make our default value compatibility check smarter, to allow an int value for an enum type backed by int... @flier268 in the meantime, you can manually change from the new HasDefaultValue(1) to the previous HasDefaultValueSql("1") - that should work.

flier268 commented 7 months ago

@roji I created a t4 template to change the type to enum

EntityType.t4 ```C# <#@ template hostSpecific="true" #> <#@ assembly name="bin\Debug\net7.0\FPS2_Desktop.Core.dll" #> <#@ assembly name="Microsoft.EntityFrameworkCore" #> <#@ assembly name="Microsoft.EntityFrameworkCore.Design" #> <#@ assembly name="Microsoft.EntityFrameworkCore.Relational" #> <#@ assembly name="Microsoft.Extensions.DependencyInjection.Abstractions" #> <#@ parameter name="EntityType" type="Microsoft.EntityFrameworkCore.Metadata.IEntityType" #> <#@ parameter name="Options" type="Microsoft.EntityFrameworkCore.Scaffolding.ModelCodeGenerationOptions" #> <#@ parameter name="NamespaceHint" type="System.String" #> <#@ import namespace="System.Collections.Generic" #> <#@ import namespace="System.Linq" #> <# // Template version: 703 - please do NOT remove this line if (EntityType.IsSimpleManyToManyJoinEntityType()) { // Don't scaffold these return ""; } var services = (IServiceProvider)Host; var annotationCodeGenerator = services.GetRequiredService(); var code = services.GetRequiredService(); var usings = new List { "System", "System.Collections.Generic" }; if (Options.UseDataAnnotations) { usings.Add("System.ComponentModel.DataAnnotations"); usings.Add("System.ComponentModel.DataAnnotations.Schema"); usings.Add("Microsoft.EntityFrameworkCore"); } if (!string.IsNullOrEmpty(NamespaceHint)) { #> namespace <#= NamespaceHint #>; <# } if (!string.IsNullOrEmpty(EntityType.GetComment())) { #> /// /// <#= code.XmlComment(EntityType.GetComment()) #> /// <# } if (Options.UseDataAnnotations) { foreach (var dataAnnotation in EntityType.GetDataAnnotations(annotationCodeGenerator)) { #> <#= code.Fragment(dataAnnotation) #> <# } } #> public partial class <#= EntityType.Name #> { <# var firstProperty = true; foreach (var property in EntityType.GetProperties().OrderBy(p => p.GetColumnOrder() ?? -1)) { if (!firstProperty) { WriteLine(""); } if (!string.IsNullOrEmpty(property.GetComment())) { #> /// /// <#= code.XmlComment(property.GetComment(), indent: 1) #> /// <# } if (Options.UseDataAnnotations) { var dataAnnotations = property.GetDataAnnotations(annotationCodeGenerator) .Where(a => !(a.Type == typeof(RequiredAttribute) && Options.UseNullableReferenceTypes && !property.ClrType.IsValueType)); foreach (var dataAnnotation in dataAnnotations) { #> <#= code.Fragment(dataAnnotation) #> <# } } Type propType = property.ClrType; var printFlagList = new string[] { "PdateFlag", "SdateFlag", "PtimeFlag", "StimeFlag", "UseByDateFlag", "CustomDate1Flag", "CustomDate2Flag" }; if (printFlagList.Contains(property.Name)) { propType = Type.GetType("FPS2_Desktop.Core.Enums.EDateIsPrintFlag, FPS2_Desktop.Core"); } if (property.Name == "NonWtFlag") propType = Type.GetType("FPS2_Desktop.Core.Enums.NonWtFlag, FPS2_Desktop.Core"); if (property.Name == "LanguageType") propType = Type.GetType("FPS2_Desktop.Core.Enums.ELanguageType, FPS2_Desktop.Core"); if (property.Name == "StockType") propType = Type.GetType("FPS2_Desktop.Core.Enums.EStockType, FPS2_Desktop.Core"); if (property.Name == "MwFlag") propType = Type.GetType("FPS2_Desktop.Core.Enums.EWeightFlag, FPS2_Desktop.Core"); if (property.Name == "Status") propType = Type.GetType("FPS2_Desktop.Core.Enums.EFormStatus, FPS2_Desktop.Core"); if (EntityType.Name == "OrderTypeSet" && property.Name.EndsWith("Enabled")) propType = Type.GetType("FPS2_Desktop.Core.Enums.OrderTypeSetColumnType, FPS2_Desktop.Core"); usings.AddRange(code.GetRequiredUsings(propType)); bool needsNullable = Options.UseNullableReferenceTypes && property.IsNullable && !propType.IsValueType; bool needsInitializer = Options.UseNullableReferenceTypes && !property.IsNullable && !propType.IsValueType; #> public <#= code.Reference(propType) #><#= needsNullable ? "?" : "" #> <#= property.Name #> { get; set; }<#= needsInitializer ? " = null!;" : "" #> <# firstProperty = false; } foreach (var navigation in EntityType.GetNavigations()) { WriteLine(""); if (Options.UseDataAnnotations) { foreach (var dataAnnotation in navigation.GetDataAnnotations(annotationCodeGenerator)) { #> <#= code.Fragment(dataAnnotation) #> <# } } var targetType = navigation.TargetEntityType.Name; if (navigation.IsCollection) { #> public virtual ICollection<<#= targetType #>> <#= navigation.Name #> { get; set; } = new List<<#= targetType #>>(); <# } else { bool needsNullable = Options.UseNullableReferenceTypes && !(navigation.ForeignKey.IsRequired && navigation.IsOnDependent); bool needsInitializer = Options.UseNullableReferenceTypes && navigation.ForeignKey.IsRequired && navigation.IsOnDependent; #> public virtual <#= targetType #><#= needsNullable ? "?" : "" #> <#= navigation.Name #> { get; set; }<#= needsInitializer ? " = null!;" : "" #> <# } } foreach (var skipNavigation in EntityType.GetSkipNavigations()) { WriteLine(""); if (Options.UseDataAnnotations) { foreach (var dataAnnotation in skipNavigation.GetDataAnnotations(annotationCodeGenerator)) { #> <#= code.Fragment(dataAnnotation) #> <# } } #> public virtual ICollection<<#= skipNavigation.TargetEntityType.Name #>> <#= skipNavigation.Name #> { get; set; } = new List<<#= skipNavigation.TargetEntityType.Name #>>(); <# } #> } <# var previousOutput = GenerationEnvironment; GenerationEnvironment = new(); while(usings.Contains("System")) usings.Remove("System"); usings.Remove("System.Collections.Generic"); usings.Remove("System.ComponentModel.DataAnnotations"); usings.Remove("System.ComponentModel.DataAnnotations.Schema"); usings.Remove("Microsoft.EntityFrameworkCore"); while(usings.Contains("FPS2_Desktop.Core.Enums")) usings.Remove("FPS2_Desktop.Core.Enums"); foreach (string ns in usings.Distinct().OrderBy(x => x, new NamespaceComparer())) { #> using <#= ns #>; <# } if(usings.Count > 0) WriteLine(""); previousOutput.Remove(previousOutput.Length - 2, 2); GenerationEnvironment.Append(previousOutput); #> ```

There are a lot of Entity, so I don't want to manually edit.

ajcvickers commented 6 months ago

@flier268 You should update your T4 template to handle the default value appropriately.

flier268 commented 3 months ago

@ajcvickers I don't know how to modify T4 to use HasDefaultValueSql instead of HasDefaultValue, can you give me a little help?

Just know how to remove HasDefaultValue

var propertyFluentApiCalls = property.GetFluentApiCalls(annotationCodeGenerator)
        ?.FilterChain(c => !(Options.UseDataAnnotations && c.IsHandledByDataAnnotations)
            && !(c.Method == "IsRequired" && Options.UseNullableReferenceTypes && !property.ClrType.IsValueType)
            && !c.Method.StartsWith("HasDefaultValue")); // Add this