Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nullable Model Differences between EF Core 6 -> EF Core 7 #30502

Closed
PhilBroderickCrezco opened this issue Mar 16, 2023 · 3 comments
Closed

Nullable Model Differences between EF Core 6 -> EF Core 7 #30502

PhilBroderickCrezco opened this issue Mar 16, 2023 · 3 comments

Comments

@PhilBroderickCrezco
Copy link

File a bug

Somwhere between EF Core V6.0.5 and V7.0.4, a breaking change to the design time model generation has caused our model validation logic to fail, expecting there to be differences (using the HasDifferences/GetDifferences methods) between what's in the Database vs the design model.

This might be quite a niche problem due to our current model set up (see example below), but in EF Core 6.0.5 the integer field is marked as NonNullable, whereas upgrading to EF Core 7.0.4 and attempting to compare, the design time model shows the field as Nullable. We are making use of the SmartEnum package version 2.1.0 (this has not changed).

Include your code

var dbContext = new MyContext();

var sp = dbContext.GetInfrastructure();

var modelDiffer = sp.GetRequiredService<IMigrationsModelDiffer>();
var migrationsAssembly = sp.GetRequiredService<IMigrationsAssembly>();

var modelInitializer = sp.GetRequiredService<IModelRuntimeInitializer>();
var sourceModel = modelInitializer.Initialize(migrationsAssembly.ModelSnapshot!.Model);

var designTimeModel = sp.GetRequiredService<IDesignTimeModel>();
var readOptimizedModel = designTimeModel.Model;

var sourceRelationalModel = sourceModel.GetRelationalModel();
var readOptimizedModelRelational = readOptimizedModel.GetRelationalModel();

// EF Core 6 - No diffs
// EF Core 7 - Diff in Nullable column
var diffs = modelDiffer.GetDifferences(sourceRelationalModel, readOptimizedModelRelational);

class MyContext : DbContext
{
    public DbSet<MyDbModel> MyDbModels { get; set; }
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer("Server=localhost;Database=Test;Trusted_Connection=True;Integrated Security=True;ConnectRetryCount=0;Connect Timeout=30;TrustServerCertificate=True");
    }
    
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<MyDbModel>(t =>
        {
            t.HasKey(k => k.Id);

            t.OwnsOne(p => p.MySmartEnumStatusRecord, t =>
            {
                t.Property(p => p.Status).HasConversion(p => p.Value,
                    p => MySmartEnum.FromValue(p));
            });
        });
    }
}

public class MyDbModel
{
    public int Id { get; set; }
    public StatusRecord<MySmartEnum> MySmartEnumStatusRecord { get; set; }
}

public class MySmartEnum : SmartEnum<MySmartEnum>
{
    public static readonly MySmartEnum Created = new(nameof(Created), 0);

    private MySmartEnum(string name, int value) : base(name, value)
    {
    }
}

public sealed record StatusRecord<TStatus>(TStatus Status)
    where TStatus : SmartEnum<TStatus, int>;

Include verbose output

EF Core 6.0.5 Design time Model DebugView:

RelationalModel: 
  Table: MyDbModels
    PK_MyDbModels {'Id'} PrimaryKey
    EntityTypeMappings: 
      MyDbModel - MyDbModels IncludesDerivedTypes
      .StatusRecord<MySmartEnum> - MyDbModels IncludesDerivedTypes
    Columns: 
      Id (int) NonNullable)
        Annotations: 
          SqlServer:Identity: 1, 1
      MySmartEnumStatusRecord_Status (int) NonNullable)

EF Core 7.0.4 Design time Model DebugView:

RelationalModel: 
  Table: MyDbModels
    PK_MyDbModels {'Id'} PrimaryKey
    EntityTypeMappings: 
      MyDbModel - MyDbModels IncludesDerivedTypes IsSharedTablePrincipal
      .StatusRecord<MySmartEnum> - MyDbModels IncludesDerivedTypes !IsSharedTablePrincipal
    Columns: 
      Id (int) NonNullable)
        Annotations: 
          SqlServer:Identity: 1, 1
      MySmartEnumStatusRecord_Status (int) Nullable)

Migration Operation when comparing in EF Core 7.0.4:

ALTER TABLE MyDbModels ALTER COLUMN MySmartEnumStatusRecord_Status

Include provider and version information

EF Core version: 6.0.5/7.0.4
Database provider: (e.g. Microsoft.EntityFrameworkCore.SqlServer)
Target framework: (e.g. .NET 7.0)
Operating system: Windows 10
IDE: Rider 2022.3.1

@ajcvickers
Copy link
Member

Note for triage: still repros in latest daily build.

@PhilBroderickCrezco
Copy link
Author

Hi @ajcvickers, just wondering if this has been prioritized for a release?

@AndriySvyryd
Copy link
Member

This looks to be by design. StatusRecord<TStatus>.Status is of a reference type and should be nullable by default. I couldn't find the commit in 7.0 that changed this behavior, but the expected workaround is to call t.Property(p => p.Status).IsRequired()
Related to #24685

@AndriySvyryd AndriySvyryd removed this from the 7.0.x milestone Sep 7, 2023
@AndriySvyryd AndriySvyryd removed their assignment Sep 7, 2023
@ajcvickers ajcvickers removed their assignment Sep 28, 2023
@ajcvickers ajcvickers closed this as not planned Won't fix, can't repro, duplicate, stale Sep 28, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants