1

Context

I am trying my own spin at DDD architecture. The key difference from other projects I've seen is that I am not using my Domain models as data entities, but instead I have separate models I called Stores that map from the Domain models and represent the state of the database.

If you're not familiar with DDD the idea is to completely decouple the core business logic from other elements of the application, such as database. In order to achieve that I have defined Domain models that contain the business logic and validation and then Entity Models, which represent the same sate as the Domain models (striped of business and validation logic) but also represent EF specific relationship properties.

Problem

The EF operations work for simpler operations. Let's say we have a Contest, which can contain several Trials.

Example in pseudo-code:

contest = new Contest
contest.Add(new Trial(1))
contest.Add(new Trial(2))

data.Save(contest) // performs mapping to ContestEntity and calls dbContext.Add
// So far so good

contestWithTrials = data.Get() // contest comes with 2 Included Trials
contestWithTrials.Add(new Trial(3))
data.Save(contestWithTrials) // performs mapping, calls dbContext.Update and tries to save but fails.

The error is:

The instance of entity type 'Trial' cannot be tracked because another instance with the key value '{Id: 1}' is already being tracked

or

Attempted to update or delete an entity that does not exist in the store

For some reason the mapping confuses EF and it tries to re-create the already existing Trial, but I cannot understand why - I can see that the entities are added correctly in DbSet.Local just before SaveChanges is called, but still it throws.

I've setup a PoC branch here. It's a console application with minimal reproducible example per Progrman's advice bellow. Since the setup requires several packages I think it's better in a repo instead of a single file.

Progman
  • 16,827
  • 6
  • 33
  • 48
Alex
  • 715
  • 1
  • 8
  • 29
  • It is unclear what the problem is or what you are trying to fix. Please [edit] your question to include the full source code you have as a [mcve], which can be compiled and tested by others. There are a lot of questions on StackOverflow which deal with errors like "The instance of entity type ... cannot be tracked because another instance with the key value". – Progman Feb 13 '21 at 10:23
  • @Progman Thanks for the tip, while reducing the code base to the simplest configuration possible I've narrowed down the problem. I still can't figure it out, but I completely rewrote my question and provided code sample, albeit in a repo. – Alex Feb 13 '21 at 14:48
  • Please do not add the MCVE to an external site, add it to your question itself. Also keep it "minimal" which shows the problem you have. – Progman Feb 13 '21 at 15:18
  • @Progman I don't understand. Why do you prefer to have one big copy paste, rather than simply cloning a repo? Also copy pasting wont work, since you need to install Nuget packages. I's better off this way. And as I have stated previously - it is already "minimal". I went from bare console app ground zero and added components untill I managed to simulate the same problem. – Alex Feb 13 '21 at 16:01
  • 1
    The issue is related to AutoMapper, you might want to check other questions like https://stackoverflow.com/questions/41482484/ef-automapper-update-nested-collections, https://stackoverflow.com/questions/50459427/concisely-insert-update-delete-enumerable-child-entities-using-automapper or https://stackoverflow.com/questions/48359363/ef-core-adding-updating-entity-and-adding-updating-removing-child-entities-in/48365782 which explains the problems in updating child entities. – Progman Feb 13 '21 at 16:47
  • @Progman Thannks, I didn't think of that. I think I found a solution with AutoMapper.Collections.EntityFrameworkCore. If you write down an answer I'll accept it as a solution. – Alex Feb 15 '21 at 11:32
  • When you have found a solution by yourself, you can add an answer to your question by yourself, see https://stackoverflow.com/help/self-answer. – Progman Feb 15 '21 at 15:52

2 Answers2

1

It is a good idea to separate domain model classes containing business logic from infrastructure dependencies, in your case database concerns. But as you are utilizing EF Core you can dismiss your Entity Models altogether as EF Core is already designed in a way that allows you to separate domain and database concerns.

Let's look at an example from the Microsoft powered EShopOnWeb project. The domain model class Order (an aggregate root of the Ordering context) contains the domain logic and is structured so that business invariants can be adhered to the best way.

When you look at the Order class you see that it has no database or other infrastructure dependencies. The domain model class is also located in the

https://github.com/dotnet-architecture/eShopOnWeb/blob/master/src/ApplicationCore/Entities/OrderAggregate/Order.cs

of the solution.

public class Order : BaseEntity, IAggregateRoot
{
    private Order()
    {
        // required by EF
    }

    public Order(string buyerId, Address shipToAddress, List<OrderItem> items)
    {
        Guard.Against.NullOrEmpty(buyerId, nameof(buyerId));
        Guard.Against.Null(shipToAddress, nameof(shipToAddress));
        Guard.Against.Null(items, nameof(items));

        BuyerId = buyerId;
        ShipToAddress = shipToAddress;
        _orderItems = items;
    }

    public string BuyerId { get; private set; }
    public DateTimeOffset OrderDate { get; private set; } = DateTimeOffset.Now;
    public Address ShipToAddress { get; private set; }

    private readonly List<OrderItem> _orderItems = new List<OrderItem>();

    public IReadOnlyCollection<OrderItem> OrderItems => _orderItems.AsReadOnly();

    public decimal Total()
    {
        var total = 0m;
        foreach (var item in _orderItems)
        {
            total += item.UnitPrice * item.Units;
        }
        return total;
    }
}

In order to map the business model to the database in order to persist the data built-in functionality from EF Core can be used by simply defining a corresponding configuration class as shown below. To separate it from the business layer it is, amongst other things, also located in the infrastructure layer (or data layer) of the project.

public class OrderConfiguration : IEntityTypeConfiguration<Order>
{
    public void Configure(EntityTypeBuilder<Order> builder)
    {
        var navigation = builder.Metadata.FindNavigation(nameof(Order.OrderItems));

        navigation.SetPropertyAccessMode(PropertyAccessMode.Field);

        builder.OwnsOne(o => o.ShipToAddress, a =>
        {
            a.WithOwner();
            
            a.Property(a => a.ZipCode)
                .HasMaxLength(18)
                .IsRequired();

            a.Property(a => a.Street)
                .HasMaxLength(180)
                .IsRequired();

            a.Property(a => a.State)
                .HasMaxLength(60);

            a.Property(a => a.Country)
                .HasMaxLength(90)
                .IsRequired();

            a.Property(a => a.City)
                .HasMaxLength(100)
                .IsRequired();
        });
    }
}

The only thing required by EF Core is the private parameterless constructor in the Order domain model class which is, from my point-of-view, an acceptable trade-off considering you can save the effort of writing database mapping classes.

If I am constrained by other frameworks that do not provide such capabilities I often also go a similar way as you are doing now, but in case of having the features of EF Core at hand I would suggest to reconsider you approach an give EF Core configuration features a try.

I know this is not the exact answer to the technical problem you are facing but I wanted to show you an alternative approach.

Andreas Hütter
  • 3,288
  • 1
  • 11
  • 19
  • 1
    Thanks for the input, I was aware of entity configurations but I didn't know they are that capable. While I won't end up going with this approach for other reasons, it's certainly a viable alternative and hence the thumbs up :) – Alex Feb 15 '21 at 11:35
0

Your problem is, that as you are loading your entities from the database EF Core starts tracking them in its change tracker to identify changes you make to the loaded entities as soon as SaveChanges() is called. This behaviour works fine as long as you modify the actual object that was loaded by EF.

What you are doing is: loading a DatabaseTrial (lets say it has id 1), then mapping it to DomainTrial, potentially modify it, and then mapping it to NEW instance of DatabaseTrial which also has id 1 and adding it to the context. This confuses EF because it now has two diffent objects (by reference) which both have id 1. This is not allowed as ids have to be unique (if EF did not throw this exception which DatabaseTrial object should used to update the database entry?).

The solution is quite simple: Just use AsNoTracking() when loading the entities from the database. This will prevent the change tracker from keeping track of the originally loaded object and as soon as Update() is called only the new entity will be tracked in the "Modified" state and used to update the database entry. As the documentation states:

For entity types with generated keys if an entity has its primary key value set then it will be tracked in the Modified state. If the primary key value is not set then it will be tracked in the Added state. This helps ensure new entities will be inserted, while existing entities will be updated. An entity is considered to have its primary key value set if the primary key property is set to anything other than the CLR default for the property type.

this will also work for your Trial which is being added to your Contest as its primary key is set to the default value after creation and EF will know that it must be inserted.

Nannanas
  • 591
  • 2
  • 8
  • Thanks for the answer. You're close, but modifying a single entity works. The problem comes when I try to update collections - for example if I add new Trial in the Contest.Trials collection. Then during the mapping from Domain->Entity the collection gets recreated and that's what's causing the error. I've found a potential solution with AutoMapper.Collections.EntityFrameworkCore, but I haven't yet tested and thus I haven't written an answer here. If you can confirm it works and change ur answer I'll accept it. (you're welcome to use my repo as PoC, if u want). – Alex Feb 23 '21 at 15:22
  • @Alex I'm personally not a fan of using AutoMapper for several reasons so I'm not going to dig further into this solution. Have you tried my solution and it didn't work for you? Because we are using this approach and we also have collections where objects are added and then saved and it works fine. – Nannanas Feb 23 '21 at 17:28