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:
- Get disconnected user object
- Create context
- Create Order object
- Attach user via user object or id
- 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;}
}
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;
}
}
}
}
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
.
精彩评论