DbContext Override SaveChanges not firing - c#

I am trying to override SaveChanges DbContect using Entity Framework.
The
override int SaveChanges()
is not running and the break-point is not being hit.
I have moved the class into the root where the EDMX file is located but that still did not help.
How can get I the SaveChanges override to work?
using System;
using System.Data.Entity;
namespace DAL
{
public class MyEntities : DbContext
{
public override int SaveChanges()
{
throw new Exception("override DbContext>SaveChanges working");
// return base.SaveChanges();
}
}
}
Code to call SaveChanges
using (var ctx = new Entities())
{
// model.clientID = data.clientID;
// model.clientGUID = data.clientGUID;
model.clientName = data.clientName;
model.clientDept = data.clientDept;
model.clientWebsite = data.clientWebsite;
model.clientEmail = data.clientEmail;
model.isActive = data.isActive;
model.clientModDate = data.clientModDate;
model.clientCreatedDate = data.clientCreatedDate;
ctx.Clients.Add(model);
ctx.SaveChanges();
}

The easiest way: the EF class is defined with "partial". So add another class file with the class name being:
public partial class MyEntities
{
public void SavingChanges()
{
//Do custom code
this.SaveChanges();
}
}
And change all of your SaveChanges() calls to SavingChanges(). Then you can customize the process however you want. The key is to add another partial class so make sure the EF context has the partial defined (not in your code sample but was the default implementation).

I was able to get the code working using the comments provided.
Create the class file in the same project the EDMX is located
public partial class Entities <--- same name as project Entity
all working now!
https://exceptionnotfound.net/entity-change-tracking-using-dbcontext-in-entity-framework-6/
namespace ModelApp_MVC.Models
{
public partial class Entities : DbContext
{
public override int SaveChanges()
{
//Do custom code
// throw new Exception("override DbContext>SaveChanges working");
// this.SaveChanges();
var modifiedEntities = ChangeTracker.Entries()
.Where(p => p.State == EntityState.Modified).ToList();
var now = DateTime.UtcNow;
foreach (var change in modifiedEntities)
{
var entityName = change.Entity.GetType().Name;
var primaryKey = GetPrimaryKeyValue(change);
foreach (var prop in change.OriginalValues.PropertyNames)
{
var originalValue = change.OriginalValues[prop].ToString();
var currentValue = change.CurrentValues[prop].ToString();
if (originalValue != currentValue) //Only create a log if the value changes
{
ChangeLog log = new ChangeLog()
{
EntityName = entityName,
PrimaryKeyValue = primaryKey.ToString(),
PropertyName = prop,
OldValue = originalValue,
NewValue = currentValue,
DateChanged = now
};
ChangeLogs.Add(log);
}
}
}
return base.SaveChanges();
}
object GetPrimaryKeyValue(DbEntityEntry entry)
{
var objectStateEntry = ((IObjectContextAdapter)this).ObjectContext.ObjectStateManager.GetObjectStateEntry(entry.Entity);
return objectStateEntry.EntityKey.EntityKeyValues[0].Value;
}
}
}

Related

How to set navigation property in Entity Framework 6 in case of generic entities

I have update a generic update method as follows
public virtual void Update<TEntity>(TEntity entity, string modifiedBy =
null) where TEntity : class,IEntity
{
using (var context = new BanyanDbContext())
{
entity.ModifiedDate = DateTime.UtcNow;
entity.ModifiedBy = modifiedBy;
var existingEntiy = context.Set<TEntity>().Find(entity.Id);
context.Entry(existingEntiy).CurrentValues.SetValues(entity);
context.Entry(existingEntiy).State = EntityState.Modified;
Save(context);
}
}
This method is working fine for updating non reference type values, but CurrentValues.SetValues() does not set or update navigation properties.
How can I set navigation property in this scenario.
public TEntity RemoveNavigationProperties(TEntity input)
{
foreach (var item in input.GetType().GetProperties().Where(x => x.PropertyType.Namespace == input.GetType().Namespace))
{
item.SetValue(input, null);
}
return input;
}
This worked for me. (I just needed to null them)

Why is EF inserting new data for entities that I'm not specifying?

I'm going to chunk this down to as simple a case as I can, but this happens for everything.
I'm basing most of my data model POCO objects on a BaseDataObject defined as follows:
public class BaseDataObject
{
public int Id { get; set; }
public bool Deleted { get; set; }
}
My code-first data model has a Client object:
public class Client : BaseDataObject
{
public string Name { get; set; }
public virtual Category Category { get; set; }
public virtual Category Subcategory { get; set; }
}
The Category object is pretty simple:
public class Category : BaseDataObject
{
public string Name { get; set; }
}
The required Id property exists in the inherited BaseDataObject.
To add entities, I'm using the following repo:
public class DataRepository<TModel, TContext>
where TModel : BaseDataObject
where TContext : DbContext
{
public int AddItem(T item)
{
using (var db = (TContext)Activator.CreateInstance(typeof(TContext)))
{
db.Set<T>().Add(item);
db.SaveChanges();
}
}
// These are important as well.
public List<T> ListItems(int pageNumber = 0)
{
using (var db = (TContext)Activator.CreateInstance(typeof(TContext)))
{
// Deleted property is also included in BaseDataObject.
return db.Set<T>().Where(x => !x.Deleted).OrderBy(x => x.Id).Skip(10 * pageNumber).ToList();
}
public T GetSingleItem(int id)
{
using (var db = (TContext)Activator.CreateInstance(typeof(TContext)))
{
return db.Set<T>().SingleOrDefault(x => x.Id == id && !x.Deleted);
}
}
}
This adds a new client perfectly fine, but there's something weird about my data model here that's causing Entity Framework to also add 2 new Categories every time I add a client based on which categories I'm selecting on my form.
Here's my form's code:
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
try
{
BindDropDownList<Category>(CategoryList);
BindDropDownList<Category>(SubcategoryList);
}
// Error handling things
}
}
private void BindDropDownList<TModel>(DropDownList control) where TModel : BaseDataObject
{
var repo = new DataRepository<TModel, ApplicationDbContext>();
control.DataSource = repo.ListItems();
control.DataTextField = "Name";
control.DataValueField = "Id";
control.DataBind();
control.Items.Insert(0, new ListItem("-- Please select --", "0"));
}
private TModel GetDropDownListSelection<TModel>(DropDownList control) where TModel : BaseDataObject
{
var repo = new DataRepository<TModel, ApplicationDbContext>();
int.TryParse(control.SelectedItem.Value, out int selectedItemId);
return repo.GetSingleItem(selectedItemId);
}
protected void SaveButton_Click(object sender, EventArgs e)
{
try
{
var repo = new DataRepository<Client, ApplicationDbContext();
var selectedCategory = GetDropDownListSelection<Category>(CategoryList);
var selectedSubcategory = GetDropDownListSelection<Category>(SubcategoryList);
var name = NameTextBox.Text;
var client = new Client
{
Name = name,
Category = selectedCategory,
Subcategory = selectedSubcategory
};
repo.AddItem(client);
}
// Error handling things
}
Unless there's something wrong with the way I'm creating the relationship here (using the virtual keyword or something maybe) then I can't see any reason why this would add new Categories to the database as duplicates of existing ones based on the selections I make in the drop down lists.
Why is this happening? What have I got wrong here?
The DbSet<T>.Add method cascades recursively to navigation properties which are not currently tracked by the context and marks them as Added. So when you do
db.Set<T>().Add(item);
it actually marks both Client class referenced Category entities as Added, hence SaveChanges inserts two new duplicate Category records.
The usual solution is to tell EF that entities are existing by attaching them to the context in advance. For instance, if you replace repo.AddItem(client); with
using (var db = new ApplicationDbContext())
{
if (client.Category != null) db.Set<Category>().Attach(client.Category);
if (client.Subcategory != null) db.Set<Category>().Attach(client.Subcategory);
db.Set<Client>().Add(item);
db.SaveChanges();
}
everything will be fine.
The problem is that you use generic repository implementation which does not provide you the necessary control. But that's your design decision issue, not EF. The above is EF intended way to handle such operation. How you can fit it into your design is up to you (I personally would eliminate the generic repository anti-pattern and use directly the db context).
It is really hard to judge from your listing because no FK mappings are included nor the base model details are provided.
However, it would appear that the Category that you assigned to client does not have PK set, and (most likely) only has the Name set, and you have no unique IX on that.
So EF has no reasonable way to work out that this is the right category.
One way to sort it is
protected void SaveButton_Click(object sender, EventArgs e)
{
try
{
var repo = new DataRepository<Client, ApplicationDbContext>();
var selectedCategory = GetDropDownListSelection<Category>(CategoryList);
var selectedSubcategory = GetDropDownListSelection<Category>(SubcategoryList);
var name = NameTextBox.Text;
var client = new Client
{
Name = name,
// either
Category = new DataRepository<Category , ApplicationDbContext>().GetSingleItem(selectedCategory.id),
// or, easier (assuming you have FK properties defined on the model)
CategoryId = selectedCategory.Id,
// repeat as needed
Subcategory = selectedSubcategory
};
repo.AddItem(client);
}
// Error handling things
}

Entity Framework Interceptor set and populate custom value back after insert

I have an entity class that has a DateTime property, LastModified, that I would like to set its value using the Interceptor feature in Entity framework 6.1
I have created an EntityFramework interceptor that populates the insert command and correct sets the value in the INSERT statement.
My pseudo code looks like this
Simple entity class with a generated id and the LastModified field
public class Item {
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id{get;set;}
public DateTime LastModified{get;set;}
}
The interceptor (which is added using a DbConfiguration, not covered here)
public class TestInterceptor : IDbCommandTreeInterceptor
{
void IDbCommandTreeInterceptor.TreeCreated(DbCommandTreeInterceptionContext interceptionContext)
{
if (interceptionContext.OriginalResult.DataSpace != DataSpace.SSpace)
return;
var insertCommand = interceptionContext.Result as DbInsertCommandTree;
if (insertCommand != null)
{
//get setClauses of insert command
var setClauses = insertCommand.SetClauses.ToList();
//this index is hardcoded to simplify the example
//that we change the value of one of the setClauses to a custom value.
var clause = setClauses[0];
clause = DbExpressionBuilder.SetClause(clause.Property(),DbExpression.FromDateTime(DateTime.UtcNow));
setClauses[0] = clause;
interceptionContext.Result = DbInsertCommandTree(
insertCommand.MetadataWorkspace,
insertCommand.DataSpace,
insertCommand.Target,
setClauses.AsReadOnly(),
insertCommand.Returning);
}
}
}
The code to create an instance object
using(var ctx = new MyDbContext()){
var item = new Item();
ctx.Items.Add(item);
ctx.SaveChanges();
}
Problem is that the database has the correct value for the LastModified column, but the item instance has not. It has the Id correctly set. I guess that I need to modify the insertCommand.Returning but how?
UPDATE:
Like to clarify that I am aware that there are much simpler ways to do this but the purpose of this post is to use Interceptors, the example above is trimmed down for clarity. The end result will use attributes to mark the entity properties that should be affected by this.
I have an entity class that has a DateTime property, LastModified, that I would like to set its value using the Interceptor feature in Entity framework 6.1
If you want to set value for LastModified property in one place of your application, you can do this before SaveChanges() using ChangeTracker:
var entities = ChangeTracker.Entries().Where(x => x.Entity is Item && (x.State == EntityState.Added || x.State == EntityState.Modified));
foreach (var entity in entities)
{
if (entity.State == EntityState.Added)
{
((BaseEntity)entity.Entity).LastModified = DateTime.UtcNow;
}
}
If you want to set value for LastModified property by your database engine (using DEFAULT or TRIGGER ), you can mark your property with this:
[DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public DateTime? LastUpdated { get; set; }
Using IDbCommandTreeInterceptor for this kind of scenarios looks like an overengineering to me.
As explained here http://marisks.net/2016/02/27/entity-framework-soft-delete-and-automatic-created-modified-dates/
public class LastChangeInterceptor : IDbCommandTreeInterceptor
{
public const string LastChangeColumnName = "LastChange";
public const string LastChangeByColumnName = "LastChangeBy";
public void TreeCreated(DbCommandTreeInterceptionContext interceptionContext)
{
if (interceptionContext.OriginalResult.DataSpace != DataSpace.SSpace)
{
return;
}
var lastChange = DateTime.Now;
var lastChangeBy = HttpContext.Current.User.Identity.Name;
var insertCommand = interceptionContext.Result as DbInsertCommandTree;
var updateCommand = interceptionContext.OriginalResult as DbUpdateCommandTree;
if (insertCommand != null)
{
var setClauses = insertCommand.SetClauses
.Select(clause => clause.UpdateIfMatch(LastChangeColumnName, DbExpression.FromDateTime(lastChange)))
.Select(clause => clause.UpdateIfMatch(LastChangeByColumnName, DbExpression.FromString(lastChangeBy)))
.ToList();
interceptionContext.Result = new DbInsertCommandTree(insertCommand.MetadataWorkspace, insertCommand.DataSpace, insertCommand.Target, setClauses.AsReadOnly(), insertCommand.Returning);
}
else if (updateCommand != null)
{
var setClauses = updateCommand.SetClauses
.Select(clause => clause.UpdateIfMatch(LastChangeColumnName, DbExpression.FromDateTime(lastChange)))
.Select(clause => clause.UpdateIfMatch(LastChangeByColumnName, DbExpression.FromString(lastChangeBy)))
.ToList();
interceptionContext.Result = new DbUpdateCommandTree(updateCommand.MetadataWorkspace, updateCommand.DataSpace, updateCommand.Target, updateCommand.Predicate, setClauses.AsReadOnly(), null);
}
}
}
public static class Extensions
{
public static DbModificationClause UpdateIfMatch(this DbModificationClause clause, string property, DbExpression value)
{
var propertyExpression = (DbPropertyExpression)((DbSetClause)clause).Property;
if (propertyExpression.Property.Name == property)
{
return DbExpressionBuilder.SetClause(propertyExpression, value);
}
return clause;
}
}
You can use quite good library for management your Interceptors.
EntityHooks
This is example how you can easy achieve your goal:
public class MyDbContext : DbContext
{
public MyDbContext()
{
this.CreateHook()
.OnSave<Item>()
.Do(i=> i.LastUpdated = DateTime.Now)
}
}

InsertOrUpdate Generic Repository

Please advise, I'm using generic repository with UOW, and I got this error when I'm testing my InsertOrUpdate method.(I'm new in both c# and EF)
Result Message:
Test method UnitTestProject1.ManifestUOW.ManifestUOWTest threw exception:
System.InvalidOperationException: Attaching an entity of type 'DomainClasses.ManifestDetail'
failed because another entity of the same type already has the same primary key value.
This can happen when using the 'Attach' method or setting the state of an entity to 'Unchanged'
or 'Modified' if any entities in the graph have conflicting key values. This may be because some
entities are new and have not yet received database-generated key values. In this case use the 'Add'
method or the 'Added' entity state to track the graph and then set the state of non-new entities to
'Unchanged' or 'Modified' as appropriate.
My generic repository
public T FindById(object id)
{
return _set.Find(id);
}
public void Add(T entity)
{
DbEntityEntry entry = this._ctx.Entry(entity);
if (entry.State != EntityState.Detached)
{
entry.State = EntityState.Added;
}
else
{
this._set.Add(entity);
}
}
public void Update(T entity)
{
DbEntityEntry entry = this._ctx.Entry(entity);
if (entry.State != EntityState.Detached)
{
this._set.Attach(entity);
}
entry.State = EntityState.Modified;
}
public void InsertOrUpdate(T entity, object id)
{
var _Record = FindById(id);
if (_Record != null)
{
Update(entity);
}
else
{
Add(entity);
}
}
My test implementation
[TestMethod]
public void ManifestUOWTest()
{
ApplicationUOW appUOW = new ApplicationUOW();
ManifestDetail manD=new ManifestDetail();
for (var i = 20; i <= 22; i++)
{
manD = new ManifestDetail();
manD.ID = "kkke" + i;
manD.ManifestID = "kkke";
manD.ModifiedDate = DateTime.Now;
manD.PriorityID = 1;
manD.JobNo = "8888777";
manD.PartNo = "ppppp";
manD.OpSeq = "9000";
manD.QTY = 9;
manD.Comment = "";
manD.LitNO = "Lit no";
appUOW.ManifestDetails.InsertOrUpdate(manD, manD.ID);
}
var man = new Manifest();
man.ID = "kkke";
man.CreatedDate = DateTime.Now;
man.ManifestStateID = 2;
man.MFBldgID = 1;
man.MFDestBldgID = 2;
man.UserID = "X6344";
appUOW.Manifests.InsertOrUpdate(man, man.ID);
appUOW.SaveChanges();
}
My UOW
namespace DataLayer
{
public class ApplicationUOW:IDisposable
{
private AuditorStationDB _context = new AuditorStationDB();
private IRepository<Manifest> _manifests = null;
public IRepository<Manifest> Manifests
{
get
{
if (this._manifests == null)
{
this._manifests = new GenericRepository<Manifest>(this._context);
}
return this._manifests;
}
}
private IRepository<ManifestDetail> _manifestDetails = null;
public IRepository<ManifestDetail> ManifestDetails
{
get
{
if (this._manifestDetails == null)
{
this._manifestDetails = new GenericRepository<ManifestDetail>(this._context);
}
return this._manifestDetails;
}
}
public void SaveChanges()
{
this._context.SaveChanges();
}
public void Dispose()
{
if (this._context != null)
{
this._context.Dispose();
}
}
}
}
The problem you are having is by your InsertOrUpdate() method calling the Update() method.
You first asked EF to locate your entity by Id, so at this point, in EF it is tracking that entity.
Then you pass in the T entity into Update(), in which your Update() method ask it to Attach() it to the EF.
Now the EF already tracked a same entity at step 1, you are trying to ask it to Attach() same entity(both
entity has same primary key) at step 2, that is why it failed.
So to solve your problem, there is possible 2 ways (not tested):
don't use FindById() to determine InsertOrUpdate() - maybe if id = 0 will mean need insert else update.
before calling Update(), detach your entity FindById() - this._ctx.Entry(entity).State = EntityState.Detached
Although in this article (http://msdn.microsoft.com/en-us/data/jj592676.aspx), it is not talking about Generic repository, but the idea of how to implement InsertOrUpdate method in EF can be found at bottom.

Entity Framework not saving modified children

Frustrating, this. Here's a pair of related objects, as generated by database-first Entity Framework:
public partial class DevelopmentType
{
public DevelopmentType()
{
this.DefaultCharges = new HashSet<DefaultCharge>();
}
public System.Guid RowId { get; set; }
public string Type { get; set; }
public virtual ICollection<DefaultCharge> DefaultCharges { get; set; }
}
public partial class DefaultCharge
{
public System.Guid RowId { get; set; }
public decimal ChargeableRate { get; set; }
public Nullable<System.Guid> DevelopmentType_RowId { get; set; }
public virtual DevelopmentType DevelopmentType { get; set; }
}
Here's the code that I'm calling to save a DevelopmentType - it involves automapper since we differentiate entity objects from DTOs:
public void SaveDevelopmentType(DevelopmentType_dto dt)
{
Entities.DevelopmentType mappedDevType = Mapper.Map<DevelopmentType_dto, Entities.DevelopmentType>(dt);
_Context.Entry(mappedDevType).State = System.Data.EntityState.Modified;
_Context.DevelopmentTypes.Attach(mappedDevType);
_Context.SaveChanges();
}
In my user interface, the most common operation will be for a user to look at a list of DevelopmentTypes and update their DefaultCharge. So when I test this using the above code, it runs without error, but nothing actually changes.
If I pause in the debugger it's clear that the changed DefaultCharge is being passed into the function, and that it's attached to the DevelopmentType to be saved.
Stepping through it, if I change the value manually inside visual studio, it does save the updated value. Which is just even more confusing.
Monitoring the database with SQL Server Profiler reveals that update commands are issued only for the parent object and not for any attached objects.
I have other similar code elsewhere that functions as expected. What am I doing wrong here?
EDIT:
I have discovered that if you do this prior to the call to SaveDevelopmentType:
using (TransactionScope scope = new TransactionScope())
{
dt.Type = "Test1";
dt.DefaultCharges.First().ChargeableRate = 99;
_CILRepository.SaveDevelopmentType(dt);
scope.Complete();
}
The change to Type saves, but the change to ChargeableRate does not. I don't think it helps, massively, but thought I'd add it.
The problem is, that EF is not aware of the changed DefaultCharges.
By setting the State of the DevelopmentType to EntityState.Modified, EF only knows that the object DevelopmentType has been changed. However, this means that EF will only update DevelopmentType but not it's navigation properties.
A workaround - which isn't best practice - would be to iterate over all DefaultCharge of the current DevelopmentType and set the entity state to EntityState.Modified.
Additionally I would recommend to attach the entity to the context first, and change the state afterwards.
EDIT after comment
As you are using DTOs I suppose you are transfering these objects either through different layers or different machines.
In this case I would recommend to use self tracking entities, because it is not possible to share one context. These entities additionally holds their current state (ie. new, updated, deleted etc). There are many tutorials on the net about self tracking entities.
e.g. MSDN - Working with Self-Tracking Entities
As far as I know EF can save child entities only if the parent object was retrieved with the same Context that is trying to save it. That is attaching an object that was retrieved by one context to another context, will allow you to save changes to parent objects but not children. This was the result of a on old search based on which we switched to NHibernate. If memory serves correctly I was able to find a link where EF team member(s) confirmed this and that there WAS no plan to change this behavior. Unfortunately all links related to that search have been erased from my PC since.
As I am not aware of how you are retrieving the objects in your case, I am not sure this is relevant to your case, but put it out there just in case it helps.
Here is a link on attaching detached objects to a context.
http://www.codeproject.com/Articles/576330/Attaching-detached-POCO-to-EF-DbContext-simple-and
Context.Entry() already "Attaches" the Entity internally in order to have the context change its EntityState.
By calling Attach() you're changing the EntityState back to Unchanged. Try to comment out this line.
The Graphdiff library was a great help for me to handle all of these complexities.
You only need to set up the navigation properties that you wish to insert/update/delete (using fluent syntax) and Graphdiff will take care of it
Note: It seems to be that the project is not updated anymore but i'm using it since more than a year and is quite stable
This is not a workaround for every case, but I did discover that you can get around this by updating foreign keys on an object instead of updating navigation property objects.
For example... instead of:
myObject.myProperty = anotherPropertyObject;
Try this:
myObject.myPropertyID = anotherPropertyObject.ID;
Make sure the object is flagged as modified in EF's mind (as mentioned in other posts) and then call your save method.
Worked for me at least! It'll be a no-go when working with nested properties, but perhaps you can break your contexts up into smaller chunks and work over objects in multiple parts to avoid context bloat.
Good luck! :)
If I understand the question correctly, you have problem updating child fields. I had problems with child collection fields. I tried this and it worked for me.
You should update all child collections after attaching the object to the database context change the modified state of the parent object and save changes to the context.
Database.Products.Attach(argProduct);
argProduct.Categories = Database.Categories.Where(x => ListCategories.Contains(x.CategoryId)).ToList();
Database.Entry(argProduct).State = EntityState.Modified;
Database.SaveChanges();
I created a helper method to solve this problem.
Consider this:
public abstract class BaseEntity
{
/// <summary>
/// The unique identifier for this BaseEntity.
/// </summary>
[Key]
public Guid Id { get; set; }
}
public class BaseEntityComparer : IEqualityComparer<BaseEntity>
{
public bool Equals(BaseEntity left, BaseEntity right)
{
if (ReferenceEquals(null, right)) { return false; }
return ReferenceEquals(left, right) || left.Id.Equals(right.Id);
}
public int GetHashCode(BaseEntity obj)
{
return obj.Id.GetHashCode();
}
}
public class Event : BaseEntity
{
[Required(AllowEmptyStrings = false)]
[StringLength(256)]
public string Name { get; set; }
public HashSet<Manager> Managers { get; set; }
}
public class Manager : BaseEntity
{
[Required(AllowEmptyStrings = false)]
[StringLength(256)]
public string Name { get; set; }
public Event Event{ get; set; }
}
DbContext with the helper method:
public class MyDataContext : DbContext
{
public MyDataContext() : base("ConnectionName") { }
//Tables
public DbSet<Event> Events { get; set; }
public DbSet<Manager> Managers { get; set; }
public async Task AddOrUpdate<T>(T entity, params string[] ignoreProperties) where T : BaseEntity
{
if (entity == null || Entry(entity).State == EntityState.Added || Entry(entity).State == EntityState.Modified) { return; }
var state = await Set<T>().AnyAsync(x => x.Id == entity.Id) ? EntityState.Modified : EntityState.Added;
Entry(entity).State = state;
var type = typeof(T);
RelationshipManager relationship;
var stateManager = ((IObjectContextAdapter)this).ObjectContext.ObjectStateManager;
if (stateManager.TryGetRelationshipManager(entity, out relationship))
{
foreach (var end in relationship.GetAllRelatedEnds())
{
var isForeignKey = end.GetType().GetProperty("IsForeignKey", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(end) as bool?;
var navigationProperty = end.GetType().GetProperty("NavigationProperty", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(end);
var propertyName = navigationProperty?.GetType().GetProperty("Identity", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(navigationProperty) as string;
if (string.IsNullOrWhiteSpace(propertyName) || ignoreProperties.Contains(propertyName)) { continue; }
var property = type.GetProperty(propertyName);
if (property == null) { continue; }
if (end is IEnumerable) { await UpdateChildrenInternal(entity, property, isForeignKey == true); }
else { await AddOrUpdateInternal(entity, property, ignoreProperties); }
}
}
if (state == EntityState.Modified)
{
Entry(entity).OriginalValues.SetValues(await Entry(entity).GetDatabaseValuesAsync());
Entry(entity).State = GetChangedProperties(Entry(entity)).Any() ? state : EntityState.Unchanged;
}
}
private async Task AddOrUpdateInternal<T>(T entity, PropertyInfo property, params string[] ignoreProperties)
{
var method = typeof(EasementDataContext).GetMethod("AddOrUpdate");
var generic = method.MakeGenericMethod(property.PropertyType);
await (Task)generic.Invoke(this, new[] { property.GetValue(entity), ignoreProperties });
}
private async Task UpdateChildrenInternal<T>(T entity, PropertyInfo property, bool isForeignKey)
{
var type = typeof(T);
var method = isForeignKey ? typeof(EasementDataContext).GetMethod("UpdateForeignChildren") : typeof(EasementDataContext).GetMethod("UpdateChildren");
var objType = property.PropertyType.GetGenericArguments()[0];
var enumerable = typeof(IEnumerable<>).MakeGenericType(objType);
var param = Expression.Parameter(type, "x");
var body = Expression.Property(param, property);
var lambda = Expression.Lambda(Expression.Convert(body, enumerable), property.Name, new[] { param });
var generic = method.MakeGenericMethod(type, objType);
await (Task)generic.Invoke(this, new object[] { entity, lambda, null });
}
public async Task UpdateForeignChildren<T, TProperty>(T parent, Expression<Func<T, IEnumerable<TProperty>>> childSelector, IEqualityComparer<TProperty> comparer = null) where T : BaseEntity where TProperty : BaseEntity
{
var children = (childSelector.Invoke(parent) ?? Enumerable.Empty<TProperty>()).ToList();
foreach (var child in children) { await AddOrUpdate(child); }
var existingChildren = await Set<T>().Where(x => x.Id == parent.Id).SelectMany(childSelector).AsNoTracking().ToListAsync();
if (comparer == null) { comparer = new BaseEntityComparer(); }
foreach (var child in existingChildren.Except(children, comparer)) { Entry(child).State = EntityState.Deleted; }
}
public async Task UpdateChildren<T, TProperty>(T parent, Expression<Func<T, IEnumerable<TProperty>>> childSelector, IEqualityComparer<TProperty> comparer = null) where T : BaseEntity where TProperty : BaseEntity
{
var stateManager = ((IObjectContextAdapter)this).ObjectContext.ObjectStateManager;
var currentChildren = childSelector.Invoke(parent) ?? Enumerable.Empty<TProperty>();
var existingChildren = await Set<T>().Where(x => x.Id == parent.Id).SelectMany(childSelector).AsNoTracking().ToListAsync();
if (comparer == null) { comparer = new BaseEntityComparer(); }
var addedChildren = currentChildren.Except(existingChildren, comparer).AsEnumerable();
var deletedChildren = existingChildren.Except(currentChildren, comparer).AsEnumerable();
foreach (var child in currentChildren) { await AddOrUpdate(child); }
foreach (var child in addedChildren) { stateManager.ChangeRelationshipState(parent, child, childSelector.Name, EntityState.Added); }
foreach (var child in deletedChildren)
{
Entry(child).State = EntityState.Unchanged;
stateManager.ChangeRelationshipState(parent, child, childSelector.Name, EntityState.Deleted);
}
}
public static IEnumerable<string> GetChangedProperties(DbEntityEntry dbEntry)
{
var propertyNames = dbEntry.State == EntityState.Added ? dbEntry.CurrentValues.PropertyNames : dbEntry.OriginalValues.PropertyNames;
foreach (var propertyName in propertyNames)
{
if (IsValueChanged(dbEntry, propertyName))
{
yield return propertyName;
}
}
}
private static bool IsValueChanged(DbEntityEntry dbEntry, string propertyName)
{
return !Equals(OriginalValue(dbEntry, propertyName), CurrentValue(dbEntry, propertyName));
}
private static string OriginalValue(DbEntityEntry dbEntry, string propertyName)
{
string originalValue = null;
if (dbEntry.State == EntityState.Modified)
{
originalValue = dbEntry.OriginalValues.GetValue<object>(propertyName) == null
? null
: dbEntry.OriginalValues.GetValue<object>(propertyName).ToString();
}
return originalValue;
}
private static string CurrentValue(DbEntityEntry dbEntry, string propertyName)
{
string newValue;
try
{
newValue = dbEntry.CurrentValues.GetValue<object>(propertyName) == null
? null
: dbEntry.CurrentValues.GetValue<object>(propertyName).ToString();
}
catch (InvalidOperationException) // It will be invalid operation when its in deleted state. in that case, new value should be null
{
newValue = null;
}
return newValue;
}
}
Then I call it like this
// POST: Admin/Events/Edit/5
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Edit(Event #event)
{
if (!ModelState.IsValid) { return View(#event); }
await _db.AddOrUpdate(#event);
await _db.SaveChangesAsync();
return RedirectToAction("Index");
}

Categories