Unfortunately overriding GetHashcode and Equals methods on our entities does not appear to have any effect on how the object state manager determines whether an entity is already attached.
The only way I was able to get the country restrictions to save was if I checked to see if the shipping method was already attached by passing in an EntityKey:
public static bool IsAttached(this ObjectContext context, EntityKey key) {
if(key == null) {
throw new ArgumentNullException("key");
}
ObjectStateEntry entry;
if(context.ObjectStateManager.TryGetObjectStateEntry(key, out entry)) {
return (entry.State != EntityState.Detached);
}
return false;
}
...
var sm = GetShippingMethodById(shippingMethodId);
if (!context.IsAttached(context.CreateEntityKey("NopEntities.ShippingMethods", sm)))
context.ShippingMethods.Attach(sm);
However, this did not work at all times and often would result in trying to attach the same entity twice.
In these test I was caching the entire object graph of shipping methods and detaching them from the object context.
I can't afford to spend any more time on this, this week but my thoughts are this.
In order to successfully implement caching with entity framework, we need to first refactor (heavily) how we work with our entities.
In the case of this ShippingMethod example, the process of saving the shipping method involves retrieving the same entity 4 times.
We don't need to do this, we already have the entity and its being tracked by our object context.
Instead we should have methods on our entities that allow us to work directly with their collections:
public void AddRestrictedShippingMethod(ShippingMethod sm) {
if (!this.HasRestrictedShippingMethod(sm))
this.NpRestrictedShippingMethods.Add(sm);
}
public void RemoveRestrictedShippingMethod(ShippingMethod sm) {
var shippingMethod = this.NpRestrictedShippingMethods
.RemoveAll(x => x.ShippingMethodId == sm.ShippingMethodId);
}
public bool HasRestrictedShippingMethod(ShippingMethod sm) {
return (this.NpRestrictedShippingMethods
.Where(x => x.ShippingMethodId == sm.ShippingMethodId)
.Any());
}
Then we just save the entity and the entire object graph is persisted. Plus we only hit the database once!
//return result;
Country country = new Country();
foreach (var shippingMethod in ShippingMethodManager.GetAllShippingMethods()) {
country.AddRestrictedShippingMethod(shippingMethod);
}
ShippingMethodManager.SaveCountry(country);
In fact we only really need a SaveChanges() method on our repository classes so we can persist any changes to our entities.
I think until nopCommerce takes a more DDD approach then we will continue to run into brick walls. Moving to an ORM is a big architectural shift in any application. Unfortunately we are trying to use EF in a very database driven way. To get the real benefit of ORM we need to embrace DDD principles. Let's work with the navigation properties directly rather than having our entities calling our repositories.
Until this is done I can't see caching being an easy thing to implement.
I know that some people are concerned that introducing all these "new" design patterns and principles (TDD, DDD, IoC, DI, ORM - to name a few) will be too much of a learning curve for the community. However (and here's where I get opinionated), if you are a developer you
should take the time to learn this stuff and if at the end of it we get an application that performs well, is easy to extend and is testable, then what's the big problem?