2

I have been coding C# for a good while now, and I generally use Entity Framework and implement the repository pattern. The repository pattern tells us that we should generally only maintain and access repositories for our aggregate roots. Consider the following example, where Person is the root:

public class Person
{
    public int ID { get; set; }
    public string Name { get; set; }
    public virtual ICollection<Pet> Pets { get; set; }
}

public class Pet 
{
    public int ID { get; set; }
    public string Name { get; set; }
}

The above model would mean that we should generally access pets through the PersonRepository. However, if I want to modify or add a pet to a person, I have never found an elegant way to do this.

In order to correctly identify what to update, I need to call

DbContext.Entry(myPet).State = EntityState.Modified;

However, this messes with my repository pattern. As far as I can see, I have three options:

  1. Create a PersonRepository.AttachPet(Pet pet) method. With a complex and deeper nested model, this quickly becomes cumbersome.
  2. Fetch DbContext directly to prepare the pet for modification or adding. However, I implemented a repository to NOT access DbContext directly.
  3. Modify PersonRepository.Update(Person person) to automatically update the state of underlying pets. Not very elegant either, and possibly a large task.

What am I missing here? Is there a better approach?

Harold Smith
  • 882
  • 3
  • 12
  • 21
  • Sounds to me as if your Pet might be its own Aggregate root if you're assigning existing pets to a Person (That's what it reads like to me). It sometimes helps to look at Aggregates as a matter of dependent or independent existence. If the pet can exist without the Person then the Pet should be an AggregateRoot of its own. – Peter Karlsson May 08 '14 at 23:44
  • Controversial in some quarters, but the problems you are having are probably because you're trying to implement the repository pattern on top of an ORM ... http://stackoverflow.com/questions/15734485/is-there-a-reason-for-using-the-repository-pattern-with-entity-framework-if-i-kn – Mashton May 09 '14 at 07:59

1 Answers1

1

Yes there is a better approach. For a new person, use:

_context.People.Add(myPerson); //myPerson can have Pets attached

This will traverse all sub objects and mark them as NEW. When updating person, after calling the above code, you need to set which pet objects are modified/deleted.

I learned this in a Pluralsight course Entity Framework in the Enterprise. In it, Julie adds an extra field to Pets.

public enum ObjectState
{
    Unchanged,
    Added,
    Deleted,
    Modified
}

public interface IObjectWithState
{
    [NotMapped]
    [JsonIgnore]
    ObjectState ObjectState { get; set; }
}

public class Pet : IObjectWithState
{
    public int ID { get; set; }
    public string Name { get; set; }
}

You might want this on all of your database entities.

In your repository

public void InsertOrUpdateGraph(Person entity)
{
    _context.People.Add(entity);
    if (entity.ID != default(int)) _context.ApplyStateChanges();
}

Some extensions

public static class ContextExtension
{
    public static void ApplyStateChanges(this DbContext context)
    {
        foreach (var entry in context.ChangeTracker.Entries<IObjectWithState>())
        {
            IObjectWithState stateInfo = entry.Entity;
            entry.State = stateInfo.ObjectState.ConvertState();
        }
    }

    public static EntityState ConvertState(this ObjectState state)
    {
        switch (state)
        {
            case ObjectState.Modified:
                return EntityState.Modified;
            case ObjectState.Added:
                return EntityState.Added;
            case ObjectState.Deleted:
                return EntityState.Deleted;
            default:
                return EntityState.Unchanged;
        }
    }
}

Works every time.

Agent Shark
  • 517
  • 2
  • 6
  • This is a very promising answer. I haven't had the time to try it out yet, but it is not forgotten. However, one little detail: since all entries in the ChangeTracker are iterated, wouldn't this cause all objects to be saved, including those not related to the current Person? – Harold Smith May 12 '14 at 11:15
  • No it would not cause all the objects to be saved by iterating them. The change tracker is connected to the context, so when you create it, the only person objects in there should be the one you just input. Remember it is looking for objects that **changed** that would be updated in the db. – Agent Shark May 12 '14 at 22:39