3

We have the following: Order with UserId and User table.

Let's say we want to cache all User objects as disconnected entities and then use them in ASP.NET or web services like environment.

Because the environmnet is using UnitOfWork and IOC frameworks we would like to avoid any context manipulation. Like in following unit test:

  1. Get disconnected user object
  2. Create context
  3. Create Order object
  4. Attach user via user object or id
  5. Persist the whole Order object.

After two days of furious googling I couldn't find any solution.

The problems are:

1. USER OBJECT

If we're attaching the user object (with userId = 1), with the default behaviour the EF thinks that it is a new user object and is trying to persist new user object into db. This blows because db already has userId == 1.

Possible solution is to override the DbContext.SaveChanges, try to figure out if the User is "detached" and forcely attach it to the context. I could not figure out how to do that, because extension methods EFExtensionMethods.IsAttached or EFExtensionMethods.AttachItem when called from SaveChanges think that T is an Object.

2. USER_ID

This works, except when you want to access Order.User object before persisting and reloading the whole entity.

On top of it we will need to split the API, so that saving only using id's of objects (i.e. Order.UserId) and not the actual object (Order.User). After we reload the object we can use both. But I can see no way of actually enforcing it through the API.

3. BOTH USER OBJECT and USER_ID

In this scenario even if the User is marked as using UserId as foreign key, EF is still trying to save User object into the context hitting the problems described in 1.

Looks like I'm missing something (or a lot) fundamental and the questions are:

  • What would you recommend to do?
  • Is there nice and generic way of making EFExtensionMethods.IsAttached work from DbContext.SaveChanges
  • Is there nice and generic way of making EFExtensionMethods.AttachItem work from DbContext.SaveChanges

Any help is greatly appreciated.

[TestMethod]
public void Creating_Order_With_Both_User_And_Id()
{
    int userCount = CountAll<User>();
    User user;
    using (var db = GetContext()) { user = db.Users.AsNoTracking().First(); }
    using (var db = GetContext())
    {
        var o = CreateOrder();
        o.OrderUser = user;         // attach user entity
        o.UserId = user.UserID;     // attach by id
        db.Orders.Add(o);
        db.SaveChanges();
    }
    int newUserCount = CountAll<User>();
    Assert.IsTrue(userCount == newUserCount, string.Format("Expected {0} got {1}", userCount, newUserCount));
}

Context and classes:

public class User
{
    public User() {}
    public int UserID {get;set;}
    public string UserName {get;set;}
}

public class Order
{
    public Order() { }

    public int OrderID { get; set; }
    public DateTime OrderDate { get; set; }
    public string OrderName { get; set; }

    public int UserId { get; set; }
    public virtual User OrderUser { get; set; }
}

public class OrderConfiguration : EntityTypeConfiguration<Order>
{
    public OrderConfiguration()
    {
        this.ToTable("ORDERS");
        this.Property(x => x.OrderName).HasMaxLength(200);
        this.HasRequired(u => u.OrderUser).WithMany().HasForeignKey(u => u.UserId);
    }
}

public static class EFExtensionMethods
{
    // does not work, when called from DbContext.SaveChanges thinks T is an Object.
    public static bool IsAttached<T>(this PPContext db, T entity) where T: class
    {
        return db.Set<T>().Local.Any(e => e == entity);
    }

    // does not work, when called from DbContext.SaveChanges thinks T is an Object.
    public static void AttachItem<T>(this PPContext db, T entity) where T: class
    {
        db.Set<T>().Attach(entity);
    }
}

public class PPContext : DbContext
{
    public PPContext() : base() { }
    public PPContext(DbConnection connection) : base(connection, true) { }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Configurations.Add(new OrderConfiguration());
    }

    public override int SaveChanges()
    {
        var modified = ChangeTracker.Entries().Where(e => e.State == EntityState.Modified || e.State == EntityState.Added);
        foreach ( var item in modified )
        {
            ????
        }
    }

    public DbSet<User> Users {get;set;}
}
leppie
  • 115,091
  • 17
  • 196
  • 297
b0rg
  • 1,879
  • 12
  • 17

2 Answers2

6

We've added marker interface to the user object and mark all ICacheableEntity entites as Entity. In the SaveChanges we're just marking items as Unchanged. That way it works.

public class User : ICacheableEntity
{
    public User() { }
    public int UserID {get;set;}
    public string UserName {get;set;}
}

class PPContext
{
    public bool IsSeeding {get;set;}

    public override int SaveChanges()
    {
        var modified = ChangeTracker.Entries().Where(e => e.State == EntityState.Modified || e.State == EntityState.Added);
        if (!IsSeeding)
        {
            foreach (var item in modified.Where(item => (item.Entity is ICacheableEntity)))
            {
                item.State = EntityState.Unchanged;
            }
        }
    }
}
b0rg
  • 1,879
  • 12
  • 17
  • Are all your cached items immutable? Since it looks like anything that is cached can never be changed? Or are you doing something with the IsSeeding flag? – Jafin Feb 05 '13 at 06:06
  • Immutability of cache is not considered for the scope of this excercise. And the more I think about it, the less it matters. Really :) – b0rg Feb 05 '13 at 15:57
1

Isn't there simply an Attach missing in your TestMethod:

// ...
using (var db = GetContext())
{
    var o = CreateOrder();

    db.Users.Attach(user);

    o.OrderUser = user;         // attach user entity
    o.UserId = user.UserID;     // attach by id
    db.Orders.Add(o);
    db.SaveChanges();
}
// ...

Otherwise EF will put the user into Added state when you add the order to the context and will create a new user when you call SaveChanges.

Slauma
  • 175,098
  • 59
  • 401
  • 420
  • Thanks Slauma. Calling Attach is exactly what I was trying to avoid. And I was trying to see if there were other possibilities like tracking all "Added" entities in the SaveChanges method and forcely attaching them into the context. – b0rg Jul 05 '11 at 14:36
  • @b0rg: Hm, perhaps you accepted my answer too fast. I didn't want to say that there is no other way than calling `Attach`. I wasn't aware that you wanted to avoid exactly this. Perhaps for `????` in your `SaveChanges` method something like `item.State = EntityState.Unchanged;` is possible. (But you have to distinguish the order from the user because both are in `Added` state.) But I don't understand why exactly you want to avoid `Attach`. It looks much easier to me. – Slauma Jul 05 '11 at 14:52
  • I'm still new to this. Your answer is correct that there's no easy way of doing. And I wanted to +1 for your efforts of reading a lot of code. Main reason for avoid attach, because in our app it is not an Order and User, but some big object with 20+ cached "dictionary" objects and attaching all of them every time is a little bit boring... :) – b0rg Jul 05 '11 at 15:38
  • Architecturallly I tend to agree with you. But I also think that framework should allow easy (or should I say fluent) way of operating with such "dictionary" entities, be it classes or other enumerations, so that people wouldn't have to resort to hacks. And another reason, is that all our contexts are hidden behind the UnitOfWork and sometimes it is quite difficult to get to them from the business logic layer where we're actually assigning the User to Order. – b0rg Jul 05 '11 at 16:37