Generic repository to update an entire aggregate
S

3

16

I am using the repository pattern to provide access to and saving of my aggregates.

The problem is the updating of aggregates which consist of a relationship of entities.

For example, take the Order and OrderItem relationship. The aggregate root is Order which manages its own OrderItem collection. An OrderRepository would thus be responsible for updating the whole aggregate (there would be no OrderItemRepository).

Data persistence is handled using Entity Framework 6.

Update repository method (DbContext.SaveChanges() occurs elsewhere):

public void Update(TDataEntity item)
{
    var entry = context.Entry<TDataEntity>(item);

    if (entry.State == EntityState.Detached)
    {
        var set = context.Set<TDataEntity>();

        TDataEntity attachedEntity = set.Local.SingleOrDefault(e => e.Id.Equals(item.Id));

        if (attachedEntity != null)
        {
            // If the identity is already attached, rather set the state values
            var attachedEntry = context.Entry(attachedEntity);
            attachedEntry.CurrentValues.SetValues(item);
        }
        else
        {
            entry.State = EntityState.Modified;
        }
    }
}

In my above example, only the Order entity will be updated, not its associated OrderItem collection.

Would I have to attach all the OrderItem entities? How could I do this generically?

Socalled answered 29/1, 2014 at 9:26 Comment(2)
There is a proposal to add better support for change tracking with graphs - entityframework.codeplex.com/workitem/864 and there is a link there to graphdiff that might helpSidetrack
Consider to use github.com/WahidBitar/EF-Core-Simple-Graph-Update. It works well for me.Carrasquillo
D
19

Julie Lerman gives a nice way to deal with how to update an entire aggregate in her book Programming Entity Framework: DbContext.

As she writes:

When a disconnected entity graph arrives on the server side, the server will not know the state of the entities. You need to provide a way for the state to be discovered so that the context can be made aware of each entity’s state.

This technique is called painting the state.

There are mainly two ways to do that:

  • Iterate through the graph using your knowledge of the model and set the state for each entity
  • Build a generic approach to track state

The second option is really nice and consists in creating an interface that every entity in your model will implement. Julie uses an IObjectWithState interface that tells the current state of the entity:

 public interface IObjectWithState
 {
  State State { get; set; }
 }
 public enum State
 {
  Added,
  Unchanged,
  Modified,
  Deleted
 }

First thing you have to do is to automatically set the state to Unchanged for every entity retrieved from the DB, by adding a constructor in your Context class that hooks up an event:

public YourContext()
{
 ((IObjectContextAdapter)this).ObjectContext
  .ObjectMaterialized += (sender, args) =>
 {
  var entity = args.Entity as IObjectWithState;
  if (entity != null)
  {
   entity.State = State.Unchanged;
  }
 };
}

Then change your Order and OrderItem classes to implement the IObjectWithState interface and call this ApplyChanges method accepting the root entity as parameter:

private static void ApplyChanges<TEntity>(TEntity root)
 where TEntity : class, IObjectWithState
{
 using (var context = new YourContext())
 {
  context.Set<TEntity>().Add(root);

  CheckForEntitiesWithoutStateInterface(context);

  foreach (var entry in context.ChangeTracker
  .Entries<IObjectWithState>())
  {
   IObjectWithState stateInfo = entry.Entity;
   entry.State = ConvertState(stateInfo.State);
  }
  context.SaveChanges();
 }
}

private static void CheckForEntitiesWithoutStateInterface(YourContext context)
{
 var entitiesWithoutState =
 from e in context.ChangeTracker.Entries()
 where !(e.Entity is IObjectWithState)
 select e;

 if (entitiesWithoutState.Any())
 {
  throw new NotSupportedException("All entities must implement IObjectWithState");
 }
}

Last but not least, do not forget to set the right state of your graph entities before calling ApplyChanges ;-) (You could even mix Modified and Deleted states within the same graph.)

Julie proposes to go even further in her book:

you may find yourself wanting to be more granular with the way modified properties are tracked. Rather than marking the entire entity as modified, you might want only the properties that have actually changed to be marked as modified. In addition to marking an entity as modified, the client is also responsible for recording which properties have been modified. One way to do this would be to add a list of modified property names to the state tracking interface.

But as my answer is already too long, go read her book if you want to know more ;-)

Detribalize answered 29/1, 2014 at 16:21 Comment(5)
This is almost exactly what I'm doing now with a project, and it works quite well. I have an extension method that I attached to DbContext that I call in SaveChanges that checks all entities that have the interface and sets the EntityState. Works like a charm.Torruella
@MaxS I have had problems with this approach. If I create an object and then try and use it, because the materialized event does not fire on that object, next time ApplyChanges gets called on another object which I am adding it to, there's a duplicate keys exception. Perhaps my approach is flawed in that I am persisting 1 or two things along the way and not all in one big transaction. The first operation is separate from the transaction. So I would have to do a Reload and database hit to get around that problem. Not ideal.Shannashannah
@Shannashannah I'm not sure I understood correctly the scenario you're describing. Could you tell me step by step what you're trying to do? Do you try to hook a newly added entity to an existing one? Anyway, have a look here to see how EF deals with Entity Sates.Detribalize
@MaxS-Betclic I was using it wrongly. I have a better understanding of it now and it does not really apply to my scenario as I'm not adding an object that had been detached from a context. Thanks for the link. Sometimes I yearn for the simple days of Stored Procedures and flattened data.Shannashannah
@Detribalize thanks for the info. Is there any particular reason for recreating the State enum, i.e. why not simply reuse the existing EntityState enum?Entropy
O
6

My opinionated (DDD specific) answer would be:

  1. Cut off the EF entities at the data layer.

  2. Ensure your data layer only returns domain entities (not EF entities).

  3. Forget about the lazy-loading and IQueryable() goodness (read: nightmare) of EF.

  4. Consider using a document database.

  5. Don't use generic repositories.

The only way I've found to do what you ask in EF is to first delete or deactivate all order items in the database that are a child of the order, then add or reactivate all order items in the database that are now part of your newly updated order.

Oldline answered 29/1, 2014 at 15:6 Comment(4)
I actually am returning domain entities (not shown in my above example). I just removed the mapping and factory logic to simplify the question. Regardless, I still have the problem of updating a graph of data entities in the EF repository implementation.Socalled
I once had a similar problem with an object graph about 5 levels deep and never found a nice/clean answer. You'd think EF would have some way of updating complex object graphs.Oldline
5. Don't use generic repositories (which 1. and 2. already implies).Virgil
@Dennis: The generic repository would only provide Find, Add and Update implementation. An IOrderRepository would define specific functionality (such as GetOrdersByCustomer()). The OrderRepository concrete class would then only be required to implement those specific methods defined in IOrderRepository. By no means am I attempting a full generic approach.Socalled
W
0

So you have done well on update method for your aggregate root, look at this domain model:

 public class ProductCategory : EntityBase<Guid>
    {
      public virtual string Name { get; set; }
    }

    public class Product : EntityBase<Guid>, IAggregateRoot
    {
    private readonly IList<ProductCategory> _productCategories = new List<ProductCategory>();

    public void AddProductCategory(ProductCategory productCategory)
        {
            _productCategories.Add(productCategory);
        }
    }

it was just a product which has a product category, I've just created the ProductRepository as my aggregateroot is product(not product category) but I want to add the product category when I create or update the product in service layer:

public CreateProductResponse CreateProduct(CreateProductRequest request)
        {
            var response = new CreateProductResponse();
         try
            {
                var productModel = request.ProductViewModel.ConvertToProductModel();   
                Product product=new Product();
                product.AddProductCategory(productModel.ProductCategory);
                _productRepository.Add(productModel);
                _unitOfWork.Commit();
            }
            catch (Exception exception)
            {
                response.Success = false;
            }
            return response;
        }

I just wanted to show you how to create domain methods for entities in domain and use it in service or application layer. as you can see the code below adds the ProductCategory category via productRepository in database:

product.AddProductCategory(productModel.ProductCategory);

now for updating the same entity you can ask for ProductRepository and fetch the entity and make changes on it. note that for retrieving entity and value object of and aggregate separately you can write query service or readOnlyRepository:

 public class BlogTagReadOnlyRepository : ReadOnlyRepository<BlogTag, string>, IBlogTagReadOnlyRepository
    {
        public IEnumerable<BlogTag> GetAllBlogTagsQuery(string tagName)
        {
            throw new NotImplementedException();
        }
    }

hope it helps

Weltschmerz answered 1/2, 2014 at 20:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.