Efcore: Update owned types in SQLite throws an exception

Created on 19 Sep 2019  路  5Comments  路  Source: dotnet/efcore

I'm updating two owned types using In-Memory Database Provider and works fine. When I use SQLite Database Provider I'm getting an exception.

Steps to reproduce

Add-Migration Initial -Verbose
Update-Database -Verbose
Set Copy to output Directory to Copy if newer for newly created MyDatabase.db
Run the program

``` C#
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace OwnedTypesTest
{
public class Material
{
public int Id { get; set; }
public string Name { get; set; }
public List MaterialLocations { get; set; }

    private Material() { }
    public Material(string name, IEnumerable<MaterialLocationDto> materialLocationDtos)
    {
        Name = name;
        MaterialLocations = materialLocationDtos?.Select(ml => new MaterialLocation(this, ml.Location, ml.RewardAmount, ml.TransportationRewardAmount, ml.TransportationRewardMinimumAmount)).ToList();
    }

    public void Update(string name, IEnumerable<MaterialLocationDto> materialLocationDtos)
    {
        Name = name;
        MaterialLocations = materialLocationDtos?.Select(ml => new MaterialLocation(this, ml.Location, ml.RewardAmount, ml.TransportationRewardAmount, ml.TransportationRewardMinimumAmount)).ToList();
    }
}

public class MaterialLocationDto
{
    public Location Location { get; set; }
    public decimal? RewardAmount { get; set; }
    public decimal? TransportationRewardAmount { get; set; }
    public int? TransportationRewardMinimumAmount { get; set; }
}

public class Location
{
    public int Id { get; set; }
    public string Name { get; set; }
    public List<MaterialLocation> MaterialLocations { get; set; }

    private Location() { }
    public Location(string name, IEnumerable<LocationMaterialDto> locationMaterialDtos)
    {
        Name = name;
        MaterialLocations = locationMaterialDtos?.Select(lm => new MaterialLocation(lm.Material, this, lm.RewardAmount, lm.TransportationRewardAmount, lm.TransportationRewardMinimumAmount)).ToList();
    }

    public void Update(string name, IEnumerable<LocationMaterialDto> locationMaterialDtos)
    {
        Name = name;
        MaterialLocations = locationMaterialDtos?.Select(lm => new MaterialLocation(lm.Material, this, lm.RewardAmount, lm.TransportationRewardAmount, lm.TransportationRewardMinimumAmount)).ToList();
    }
}

public class LocationMaterialDto
{
    public Material Material { get; set; }
    public decimal? RewardAmount { get; set; }
    public decimal? TransportationRewardAmount { get; set; }
    public int? TransportationRewardMinimumAmount { get; set; }
}

public class MaterialLocation
{
    public int MaterialId { get; protected set; }
    public Material Material { get; protected set; }
    public int LocationId { get; protected set; }
    public Location Location { get; protected set; }
    public Reward Reward { get; protected set; }
    public TransportationReward TransportationReward { get; protected set; }

    private MaterialLocation() { }
    public MaterialLocation(Material material, Location location, decimal? rewardAmount, decimal? transportationRewardAmount, int? transportationRewardMinmumAmount)
    {
        Material = material;
        Location = location;
        Reward = new Reward(rewardAmount);
        TransportationReward = new TransportationReward(transportationRewardAmount, transportationRewardMinmumAmount);
    }
}

// Owned type
public class Reward
{
    public decimal? Amount { get; protected set; }

    private Reward() { }
    public Reward(decimal? amount)
    {
        Amount = amount;
    }
}

// Owned type
public class TransportationReward
{
    public decimal? Amount { get; protected set; }
    public int? MinimumAmount { get; protected set; }

    private TransportationReward() { }
    public TransportationReward(decimal? amount, int? minimumAmount)
    {
        Amount = amount;
        MinimumAmount = minimumAmount;
    }
}

public class MyDbContext : DbContext
{
    public DbSet<Material> Materials { get; set; }
    public DbSet<Location> Locations { get; set; }
    public DbSet<MaterialLocation> MaterialLocations { get; set; }

    public MyDbContext() { }
    public MyDbContext(DbContextOptions options) : base(options) { }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<MaterialLocation>().HasKey(ml => new { ml.MaterialId, ml.LocationId });
        modelBuilder.Entity<MaterialLocation>().OwnsOne(ml => ml.Reward);
        modelBuilder.Entity<MaterialLocation>().OwnsOne(ml => ml.TransportationReward);
    }
}

// Used for SQLite initial migration 
public class MyDbContextFactory : IDesignTimeDbContextFactory<MyDbContext>
{
    public MyDbContext CreateDbContext(string[] args)
    {
        DbContextOptionsBuilder<MyDbContext> optionBuilder = new DbContextOptionsBuilder<MyDbContext>();

        optionBuilder.EnableSensitiveDataLogging();
        optionBuilder.UseSqlite("Data Source=MyDatabase.db");

        return new MyDbContext(optionBuilder.Options);
    }
}

class Program
{
    static async Task Main()
    {
        ServiceCollection services = new ServiceCollection();

        //services.AddDbContext<MyDbContext>(options => options.UseInMemoryDatabase("MyDatabase"));
        services.AddDbContext<MyDbContext>(options => options.UseSqlite("Data Source=MyDatabase.db").EnableSensitiveDataLogging());

        using (ServiceProvider serviceProvider = services.BuildServiceProvider())
        {
            int locationId, material1Id, material2Id;

            using (IServiceScope scope = serviceProvider.CreateScope())
            {
                MyDbContext context = scope.ServiceProvider.GetRequiredService<MyDbContext>();
                Material material1 = new Material("Material1", null);
                Material material2 = new Material("Material2", null);
                Location location = new Location("Location1",
                    new LocationMaterialDto[]
                    {
                        new LocationMaterialDto { Material = material1, RewardAmount = 10.23M, TransportationRewardAmount = 5.25M, TransportationRewardMinimumAmount = 10 },
                        new LocationMaterialDto { Material = material2 },
                    });

                context.Materials.Add(material1);
                context.Materials.Add(material2);
                context.Locations.Add(location);

                context.SaveChanges();

                locationId = location.Id;
                material1Id = material1.Id;
                material2Id = material2.Id;
            }

            using (IServiceScope scope = serviceProvider.CreateScope())
            {
                MyDbContext context = scope.ServiceProvider.GetRequiredService<MyDbContext>();
                Location location = await context.Locations.Include(m => m.MaterialLocations).ThenInclude(ml => ml.Material).FirstOrDefaultAsync(l => l.Id == locationId);
                Material material1 = await context.FindAsync<Material>(material1Id);
                Material material2 = await context.FindAsync<Material>(material2Id);

                foreach (MaterialLocation materialLocation in location.MaterialLocations)
                {
                    Console.WriteLine($"({materialLocation.Location.Name} {materialLocation.Material.Name}) - Reward amount before changing: {materialLocation.Reward?.Amount}");
                    Console.WriteLine($"({materialLocation.Location.Name} {materialLocation.Material.Name}) - Transportation reward amount before changing: {materialLocation.TransportationReward?.Amount}");
                    Console.WriteLine($"({materialLocation.Location.Name} {materialLocation.Material.Name}) - Transportation reward minimum amount before changing: {materialLocation.TransportationReward?.MinimumAmount}");
                }

                Console.WriteLine();

                LocationMaterialDto[] locationMaterialDtos = new LocationMaterialDto[]
                {
                    new LocationMaterialDto
                    {
                        Material = material1,
                        RewardAmount = location.MaterialLocations[0].Reward?.Amount,
                        TransportationRewardAmount = location.MaterialLocations[0].TransportationReward?.Amount,
                        TransportationRewardMinimumAmount = location.MaterialLocations[0].TransportationReward?.MinimumAmount
                    },
                    new LocationMaterialDto
                    {
                        Material = material2,
                        RewardAmount = 7.88M,
                        TransportationRewardAmount = 1.22M,
                        TransportationRewardMinimumAmount = 2
                    }
                };

                location.Update(location.Name, locationMaterialDtos);

                foreach (MaterialLocation materialLocation in location.MaterialLocations)
                {
                    Console.WriteLine($"({materialLocation.Location.Name} {materialLocation.Material.Name}) - Reward amount before saving changes: {materialLocation.Reward.Amount}");
                    Console.WriteLine($"({materialLocation.Location.Name} {materialLocation.Material.Name}) - Transportation reward amount before saving changes: {materialLocation.TransportationReward.Amount}");
                    Console.WriteLine($"({materialLocation.Location.Name} {materialLocation.Material.Name}) - Transportation reward minimum amount before saving changes: {materialLocation.TransportationReward.MinimumAmount}");
                }

                Console.WriteLine();

                await context.SaveChangesAsync();

                foreach (MaterialLocation materialLocation in location.MaterialLocations)
                {
                    Console.WriteLine($"({materialLocation.Location.Name} {materialLocation.Material.Name}) - Reward amount after saving changes: {materialLocation.Reward.Amount}");
                    Console.WriteLine($"({materialLocation.Location.Name} {materialLocation.Material.Name}) - Transportation reward amount after saving changes: {materialLocation.TransportationReward.Amount}");
                    Console.WriteLine($"({materialLocation.Location.Name} {materialLocation.Material.Name}) - Transportation reward minimum amount after saving changes: {materialLocation.TransportationReward.MinimumAmount}");
                }

                Console.WriteLine();
                Console.ReadLine();
            }
        }
    }
}

}

### Expected
In-Memory Database Provider

(Location1 Material1) - Reward amount before changing: 10.23
(Location1 Material1) - Transportation reward amount before changing: 5.25
(Location1 Material1) - Transportation reward minimum amount before changing: 10
(Location1 Material2) - Reward amount before changing:
(Location1 Material2) - Transportation reward amount before changing:
(Location1 Material2) - Transportation reward minimum amount before changing:

(Location1 Material1) - Reward amount before saving changes: 10.23
(Location1 Material1) - Transportation reward amount before saving changes: 5.25
(Location1 Material1) - Transportation reward minimum amount before saving changes: 10
(Location1 Material2) - Reward amount before saving changes: 7.88
(Location1 Material2) - Transportation reward amount before saving changes: 1.22
(Location1 Material2) - Transportation reward minimum amount before saving changes: 2

(Location1 Material1) - Reward amount after saving changes: 10.23
(Location1 Material1) - Transportation reward amount after saving changes: 5.25
(Location1 Material1) - Transportation reward minimum amount after saving changes: 10
(Location1 Material2) - Reward amount after saving changes: 7.88
(Location1 Material2) - Transportation reward amount after saving changes: 1.22
(Location1 Material2) - Transportation reward minimum amount after saving changes: 2

SQLite Database Provider

System.InvalidOperationException
HResult=0x80131509
Message=The instance of entity type 'Reward' with the key value '{MaterialLocationMaterialId: 1, MaterialLocationLocationId: 1}' is marked as 'Added', but the instance of entity type 'MaterialLocation' with the key value '{MaterialId: 1, LocationId: 1}' is marked as 'Modified' and both are mapped to the same row.
Source=Microsoft.EntityFrameworkCore.Relational
StackTrace:
at Microsoft.EntityFrameworkCore.Update.ModificationCommand.AddEntry(IUpdateEntry entry)
at Microsoft.EntityFrameworkCore.Update.Internal.CommandBatchPreparer.CreateModificationCommands(IList1 entries, IUpdateAdapter updateAdapter, Func1 generateParameterName)
at Microsoft.EntityFrameworkCore.Update.Internal.CommandBatchPreparer.d__11.MoveNext()
at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.d__10.MoveNext()
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.d__10.MoveNext()
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter1.GetResult() at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.<SaveChangesAsync>d__95.MoveNext() at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.<SaveChangesAsync>d__93.MoveNext() at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at System.Runtime.CompilerServices.TaskAwaiter1.GetResult()
at Microsoft.EntityFrameworkCore.DbContext.d__54.MoveNext()
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
at OwnedTypesTest.Program.

d__0.MoveNext() in C:\Users\stefan.simion\source\repos\OwnedTypesTest\OwnedTypesTest\Program.cs:line 224
```

Further technical details

EF Core version: 3.0.0-rc1.19456.14
Database provider: Microsoft.EntityFrameworkCore.InMemory and Microsoft.EntityFrameworkCore.Sqlite
Target framework: .NET Core 3.0.100-rc1-014190
Operating system: Windows 10
IDE: Visual Studio 2019 16.2.5

closed-fixed customer-reported type-bug

Most helpful comment

I don't get it. Nullable owned entity types are a new feature but when I simply assign to it to make it non-null, I get an exception...? This is the most basic usage I could think of. If this doesn't work... then what does? Owned entity types are now completely unusable 馃槥 And it's not like I have a choice in making them nullable - it didn't used to be null, and now it is, and I can't make it not null?

Is there really going to be no fix for this for 3.0? @AndriySvyryd

All 5 comments

I just installed .NET Core 3.1.100-preview1-014459 and updated all EF Core packages to 3.1.0-preview1.19506.2.
Run the program and everything is working fine now.
Thanks @AndriySvyryd

Is there no workaround for this?

@Neme12 I couldn't find any workarounds.
The only thing that saved me was to upgrade to .NET Core 3.1 preview1 and EF Core 3.1 preview 1.

I don't get it. Nullable owned entity types are a new feature but when I simply assign to it to make it non-null, I get an exception...? This is the most basic usage I could think of. If this doesn't work... then what does? Owned entity types are now completely unusable 馃槥 And it's not like I have a choice in making them nullable - it didn't used to be null, and now it is, and I can't make it not null?

Is there really going to be no fix for this for 3.0? @AndriySvyryd

@Neme12 For entities that own an entity and can be null in some cases (due all properties nullable) a woraround is to set entry state to Modified when changing it.

public class Parent {
 public Child Child1 {get;set;}
 public Child Child2 {get;set;}
}
public class Child
{
 public string Name {get;}
}

// modifiy
var parent = context.Parents.FirstOrDefault();
parent.Child2 = new Child();
context.Entry(parent.Child2).State = EntityState.Modified; // if child2 is null then this ensures that it will not have the state added
Was this page helpful?
0 / 5 - 0 ratings