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

Migration fails when adding a check constraint on a second owned entity #23399

Closed
Neme12 opened this issue Nov 19, 2020 · 8 comments · Fixed by #26112
Closed

Migration fails when adding a check constraint on a second owned entity #23399

Neme12 opened this issue Nov 19, 2020 · 8 comments · Fixed by #26112
Labels
area-model-building closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. customer-reported type-bug
Milestone

Comments

@Neme12
Copy link

Neme12 commented Nov 19, 2020

Steps to reproduce

  1. Start with this project file and a C# file:
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="5.0.0" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.0">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
  </ItemGroup>

</Project>
using Microsoft.EntityFrameworkCore;
using System;

namespace EfCore5Test
{
    public sealed class Portal
    {
        public Guid Id { get; set; }

        public UserAction Created { get; set; }

        public UserAction Updated { get; set; }
    }

    public sealed class UserAction
    {
        public DateTime TimeUtc { get; set; }

        public Guid? UserId { get; set; }
    }

    public sealed class ApplicationDbContext : DbContext
    {
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            base.OnConfiguring(optionsBuilder);
            optionsBuilder.UseSqlServer("Server=(localdb)\\mssqllocaldb;Database=aspnet-WebApplication-6E483A89-BEE6-4A76-96CC-CEB276E5E112;Trusted_Connection=True;MultipleActiveResultSets=true");
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            modelBuilder.Entity<Portal>().OwnsOne(x => x.Created, action =>
            {
                action.HasCheckConstraint("Created_Foo", "[UserId] >= 0");
            });

            modelBuilder.Entity<Portal>().OwnsOne(x => x.Updated, action =>
            {
                // action.HasCheckConstraint("Updated_Foo", "[UserId] >= 0");
            });
        }

        public DbSet<Portal> Portals { get; set; }
    }

    class Program
    {
        static void Main(string[] args)
        {
        }
    }
}
  1. Create a migration: Add-Migration Initial
  2. Uncomment the second HasCheckConstraint call
  3. Create a migration: Add-Migration Second
    An exception is thrown and the migration cannot be created:
Build started...
Build succeeded.
System.InvalidOperationException: Sequence contains no elements
   at System.Linq.ThrowHelper.ThrowNoElementsException()
   at System.Linq.Enumerable.First[TSource](IEnumerable`1 source)
   at Microsoft.EntityFrameworkCore.Migrations.Internal.MigrationsModelDiffer.DiffContext.GetTable(IEntityType entityType)
   at Microsoft.EntityFrameworkCore.Migrations.Internal.MigrationsModelDiffer.<>c.<Diff>b__63_0(ICheckConstraint s, ICheckConstraint t, DiffContext c)
   at Microsoft.EntityFrameworkCore.Migrations.Internal.MigrationsModelDiffer.DiffCollection[T](IEnumerable`1 sources, IEnumerable`1 targets, DiffContext diffContext, Func`4 diff, Func`3 add, Func`3 remove, Func`4[] predicates)+MoveNext()
   at System.Linq.Enumerable.ConcatIterator`1.MoveNext()
   at Microsoft.EntityFrameworkCore.Migrations.Internal.MigrationsModelDiffer.Diff(ITable source, ITable target, DiffContext diffContext)+MoveNext()
   at Microsoft.EntityFrameworkCore.Migrations.Internal.MigrationsModelDiffer.DiffCollection[T](IEnumerable`1 sources, IEnumerable`1 targets, DiffContext diffContext, Func`4 diff, Func`3 add, Func`3 remove, Func`4[] predicates)+MoveNext()
   at System.Linq.Enumerable.ConcatIterator`1.MoveNext()
   at Microsoft.EntityFrameworkCore.Migrations.Internal.MigrationsModelDiffer.Sort(IEnumerable`1 operations, DiffContext diffContext)
   at Microsoft.EntityFrameworkCore.Migrations.Internal.MigrationsModelDiffer.GetDifferences(IRelationalModel source, IRelationalModel target)
   at Microsoft.EntityFrameworkCore.Migrations.Design.MigrationsScaffolder.ScaffoldMigration(String migrationName, String rootNamespace, String subNamespace, String language)
   at Microsoft.EntityFrameworkCore.Design.Internal.MigrationsOperations.AddMigration(String name, String outputDir, String contextType, String namespace)
   at Microsoft.EntityFrameworkCore.Design.OperationExecutor.AddMigrationImpl(String name, String outputDir, String contextType, String namespace)
   at Microsoft.EntityFrameworkCore.Design.OperationExecutor.AddMigration.<>c__DisplayClass0_0.<.ctor>b__0()
   at Microsoft.EntityFrameworkCore.Design.OperationExecutor.OperationBase.<>c__DisplayClass3_0`1.<Execute>b__0()
   at Microsoft.EntityFrameworkCore.Design.OperationExecutor.OperationBase.Execute(Action action)
Sequence contains no elements

Verbose output from dotnet ef migrations add Second --verbose:

Using project 'C:\Users\SimonaKonickova\source\repos\EfCore5Test\EfCore5Test\EfCore5Test.csproj'.
Using startup project 'C:\Users\SimonaKonickova\source\repos\EfCore5Test\EfCore5Test\EfCore5Test.csproj'.
Writing 'C:\Users\SimonaKonickova\source\repos\EfCore5Test\EfCore5Test\obj\EfCore5Test.csproj.EntityFrameworkCore.targets'...
dotnet msbuild /target:GetEFProjectMetadata /property:EFProjectMetadataFile=C:\Users\SimonaKonickova\AppData\Local\Temp\tmpAD5A.tmp /verbosity:quiet /nologo C:\Users\SimonaKonickova\source\repos\EfCore5Test\EfCore5Test\EfCore5Test.csproj
Writing 'C:\Users\SimonaKonickova\source\repos\EfCore5Test\EfCore5Test\obj\EfCore5Test.csproj.EntityFrameworkCore.targets'...
dotnet msbuild /target:GetEFProjectMetadata /property:EFProjectMetadataFile=C:\Users\SimonaKonickova\AppData\Local\Temp\tmpB00B.tmp /verbosity:quiet /nologo C:\Users\SimonaKonickova\source\repos\EfCore5Test\EfCore5Test\EfCore5Test.csproj
Build started...
dotnet build C:\Users\SimonaKonickova\source\repos\EfCore5Test\EfCore5Test\EfCore5Test.csproj /verbosity:quiet /nologo

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:00:01.72
Build succeeded.
dotnet exec --depsfile C:\Users\SimonaKonickova\source\repos\EfCore5Test\EfCore5Test\bin\Debug\net5.0\EfCore5Test.deps.json --additionalprobingpath C:\Users\SimonaKonickova\.nuget\packages --additionalprobingpath "C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages" --runtimeconfig C:\Users\SimonaKonickova\source\repos\EfCore5Test\EfCore5Test\bin\Debug\net5.0\EfCore5Test.runtimeconfig.json C:\Users\SimonaKonickova\.dotnet\tools\.store\dotnet-ef\5.0.0-preview.7.20365.15\dotnet-ef\5.0.0-preview.7.20365.15\tools\netcoreapp3.1\any\tools\netcoreapp2.0\any\ef.dll migrations add InitialCreate2 --assembly C:\Users\SimonaKonickova\source\repos\EfCore5Test\EfCore5Test\bin\Debug\net5.0\EfCore5Test.dll --startup-assembly C:\Users\SimonaKonickova\source\repos\EfCore5Test\EfCore5Test\bin\Debug\net5.0\EfCore5Test.dll --project-dir C:\Users\SimonaKonickova\source\repos\EfCore5Test\EfCore5Test\ --language C# --working-dir C:\Users\SimonaKonickova\source\repos\EfCore5Test\EfCore5Test --verbose --root-namespace EfCore5Test
Using assembly 'EfCore5Test'.
Using startup assembly 'EfCore5Test'.
Using application base 'C:\Users\SimonaKonickova\source\repos\EfCore5Test\EfCore5Test\bin\Debug\net5.0'.
Using working directory 'C:\Users\SimonaKonickova\source\repos\EfCore5Test\EfCore5Test'.
Using root namespace 'EfCore5Test'.
Using project directory 'C:\Users\SimonaKonickova\source\repos\EfCore5Test\EfCore5Test\'.
Remaining arguments: .
Finding DbContext classes...
Finding IDesignTimeDbContextFactory implementations...
Finding application service provider in assembly 'EfCore5Test'...
Finding Microsoft.Extensions.Hosting service provider...
No static method 'CreateHostBuilder(string[])' was found on class 'Program'.
No application service provider was found.
Finding DbContext classes in the project...
Found DbContext 'ApplicationDbContext'.
Using context 'ApplicationDbContext'.
Finding design-time services for provider 'Microsoft.EntityFrameworkCore.SqlServer'...
Using design-time services from provider 'Microsoft.EntityFrameworkCore.SqlServer'.
Finding design-time services referenced by assembly 'EfCore5Test'...
Finding design-time services referenced by assembly 'EfCore5Test'...
No referenced design-time services were found.
Finding IDesignTimeServices implementations in assembly 'EfCore5Test'...
No design-time services were found.
'ApplicationDbContext' disposed.
System.InvalidOperationException: Sequence contains no elements
   at System.Linq.ThrowHelper.ThrowNoElementsException()
   at System.Linq.Enumerable.First[TSource](IEnumerable`1 source)
   at Microsoft.EntityFrameworkCore.Migrations.Internal.MigrationsModelDiffer.DiffContext.GetTable(IEntityType entityType)
   at Microsoft.EntityFrameworkCore.Migrations.Internal.MigrationsModelDiffer.<>c.<Diff>b__63_0(ICheckConstraint s, ICheckConstraint t, DiffContext c)
   at Microsoft.EntityFrameworkCore.Migrations.Internal.MigrationsModelDiffer.DiffCollection[T](IEnumerable`1 sources, IEnumerable`1 targets, DiffContext diffContext, Func`4 diff, Func`3 add, Func`3 remove, Func`4[] predicates)+MoveNext()
   at System.Linq.Enumerable.ConcatIterator`1.MoveNext()
   at Microsoft.EntityFrameworkCore.Migrations.Internal.MigrationsModelDiffer.Diff(ITable source, ITable target, DiffContext diffContext)+MoveNext()
   at Microsoft.EntityFrameworkCore.Migrations.Internal.MigrationsModelDiffer.DiffCollection[T](IEnumerable`1 sources, IEnumerable`1 targets, DiffContext diffContext, Func`4 diff, Func`3 add, Func`3 remove, Func`4[] predicates)+MoveNext()
   at System.Linq.Enumerable.ConcatIterator`1.MoveNext()
   at Microsoft.EntityFrameworkCore.Migrations.Internal.MigrationsModelDiffer.Sort(IEnumerable`1 operations, DiffContext diffContext)
   at Microsoft.EntityFrameworkCore.Migrations.Internal.MigrationsModelDiffer.GetDifferences(IRelationalModel source, IRelationalModel target)
   at Microsoft.EntityFrameworkCore.Migrations.Design.MigrationsScaffolder.ScaffoldMigration(String migrationName, String rootNamespace, String subNamespace, String language)
   at Microsoft.EntityFrameworkCore.Design.Internal.MigrationsOperations.AddMigration(String name, String outputDir, String contextType, String namespace)
   at Microsoft.EntityFrameworkCore.Design.OperationExecutor.AddMigrationImpl(String name, String outputDir, String contextType, String namespace)
   at Microsoft.EntityFrameworkCore.Design.OperationExecutor.AddMigration.<>c__DisplayClass0_0.<.ctor>b__0()
   at Microsoft.EntityFrameworkCore.Design.OperationExecutor.OperationBase.<>c__DisplayClass3_0`1.<Execute>b__0()
   at Microsoft.EntityFrameworkCore.Design.OperationExecutor.OperationBase.Execute(Action action)
Sequence contains no elements

Include provider and version information

EF Core version: 5.0.0
Database provider: Microsoft.EntityFrameworkCore.SqlServer
Target framework: .NET 5.0
Operating system: Windows 10 Version 1909
IDE: Visual Studio 16.9.0 Preview 1.0

@Neme12
Copy link
Author

Neme12 commented Nov 19, 2020

Some screenshots from debugging:
image

image

I'm guessing the owned entity shouldn't show up as keyless and is missing its defining navigation?

@Neme12
Copy link
Author

Neme12 commented Nov 19, 2020

I made a hacky workaround by overriding MigrationsModelDiffer:

[SuppressMessage("Usage", "EF1001:Internal EF Core API usage.")]
public sealed class CustomMigrationsModelDiffer : MigrationsModelDiffer
{
    private static readonly FieldInfo _entityTypeField = typeof(CheckConstraint)
        .GetField("<EntityType>k__BackingField", BindingFlags.Instance | BindingFlags.NonPublic);

    private static void FixModel(IRelationalModel? relationalModel)
    {
        if (relationalModel is null)
            return;

        foreach (var table in relationalModel.Tables)
        {
            foreach (var mapping in table.EntityTypeMappings)
            {
                foreach (var checkConstraint in CheckConstraint.GetCheckConstraints(mapping.EntityType))
                    _entityTypeField.SetValue(checkConstraint, mapping.EntityType);
            }
        }
    }

    public CustomMigrationsModelDiffer(
        IRelationalTypeMappingSource typeMappingSource,
        IMigrationsAnnotationProvider migrationsAnnotations,
        IChangeDetector changeDetector,
        IUpdateAdapterFactory updateAdapterFactory,
        CommandBatchPreparerDependencies commandBatchPreparerDependencies) : base(
            typeMappingSource,
            migrationsAnnotations,
            changeDetector,
            updateAdapterFactory,
            commandBatchPreparerDependencies)
    {
    }

    protected override IEnumerable<MigrationOperation> Diff(IRelationalModel? source, IRelationalModel? target, DiffContext diffContext)
    {
        FixModel(source);
        FixModel(target);

        return base.Diff(source, target, diffContext);
    }
}

It seems that inside EntityTypeMappings, the entity type is correct but when we get it back from the check constraint's EntityType property, it shows up as keyless without a defining navigation. This hack changes the value of that property to the right entity type.

@ajcvickers
Copy link
Contributor

@AndriySvyryd Looks like this could be a model building bug when using CheckConstraint on an owned type.

/cc @bricelam

@AndriySvyryd
Copy link
Member

@Neme12 Thanks for all the details and the debugging!

The issue is that the second check constraint keeps a stale reference to the entity type after the owned entity type is converted to an entity type with defined navigation.

As a workaround add the owned types before configuring them:

modelBuilder.Entity<Portal>().OwnsOne(x => x.Created);
modelBuilder.Entity<Portal>().OwnsOne(x => x.Updated);

modelBuilder.Entity<Portal>().OwnsOne(x => x.Created, action =>
{
    action.HasCheckConstraint("Created_Foo", "[UserId] >= 0");
});

modelBuilder.Entity<Portal>().OwnsOne(x => x.Updated, action =>
{
    action.HasCheckConstraint("Updated_Foo", "[UserId] >= 0");
});

@AndriySvyryd AndriySvyryd self-assigned this Nov 20, 2020
@ajcvickers ajcvickers added this to the 6.0.0 milestone Nov 20, 2020
@ajcvickers
Copy link
Contributor

We discussed this in triage and based on the reasonable workaround and that the fix may require new API surface we are not currently planning to patch this for 5.0. We could revisit this with more feedback.

@Neme12
Copy link
Author

Neme12 commented Nov 22, 2020

@AndriySvyryd Thanks for the workaround, but I'll probably stay with the one I put above - even though it's a lot hackier, it's universal and we already use owned entities and check constraints a lot. I actually copied the extension methods from the PR that added the ability to add check constraints on owned entities: https://github.com/dotnet/efcore/pull/20632/files#diff-a56b326c639183f70932d8d8650e727f into my EF Core 3.1 project and it worked perfectly until upgrading to EF Core 5 - I guess it stopped working because of some of the other changes made to owned entity types in 5.0.

@ajcvickers I understand that this cannot be considered a regression because this wasn't provided in-box with 3.1, but since it worked before just I'm curious why the fix requires a new API surface?

@AndriySvyryd
Copy link
Member

@Neme12 The fix requires API changes compared to 5.0 because the metadata for check constraints changed.

@Neme12
Copy link
Author

Neme12 commented Dec 13, 2020

How does this even happen? When the ability to add check constraints to owned entities was added, were there no tests added with just more than 1 check constraint? 😕

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-model-building closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. customer-reported type-bug
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants