Entity Framework, AutoMapper, handling entity updates - c#

I just started using the Entity Framework 1.0 recently and believe I am beginning to feel the pains everyone is talking about. I'm trying to use best practices so I have a set of DTO that get mapped to and from my Entities via AutoMapper.
The real catch is when I'm trying to update an object. The first gotcha was that I could not find a way to create a new entity, transfer the data from my DTO, and still have the entity ObjectContext realize that it has been changed. I used the following code:
public VideoDTO UpdateVideo(VideoDTO pVideo)
{
Video video = new Video();
Mapper.Map(pVideo, video);
context.Attach(video); //Successfully attaches
context.ApplyPropertyChanges("Videos", video); // no changes made as far as entity knows b/c it was attached in it's updated state
context.SaveChanges(); //doesn't save the entity
return pVideo;
}
I then figured, perhaps I need to just grab the entity from the database first, attach to the context, call the Map method on Mapper, then call SaveChanges. Here what I did:
public VideoDTO UpdateVideo(VideoDTO pVideo)
{
Video video = context.Videos.Where(v => v.VideoId == pVideo.VideoId).FirstOrDefault();
Mapper.Map(pVideo, video); //Error here: Can't change VideoId value on Video entity
//context.Attach(video);
//context.ApplyPropertyChanges("Videos", video);
context.SaveChanges();
return pVideo;
}
Now we get to the lovely EF issue of not being allowed to change the property, VideoId, because it's used by the EntityKey property on the Video entity. Lovely. I had setup the mappings so that when I mapped from my DTO to an EF Entity, the EntityKey property would get a value. Now I need a way to make an exception to that mapping rule, but have no clue where to begin. I suppose I could create a brand new Mapping rule right in this method and set the EntityKey & VideoId properties to be ignored, but that seems pretty sloppy. Furthermore, I'm not sure a mapping created at this point would stick. If it overrode the initial setup that allowed the DTO to map a value to the EntityKey on the entity, that would backfire in a whole different way.
Anyone have a better idea?

AutoMapper
Your first problem is that as far as I know AutoMapper is not designed to go from DTO->Entity only Entity->DTO. This could have changed recently so I'm not really sure. See this link for more information about what automapper is designed to do: The case for two way mapping
PK Mapping
You say: "Mapping rule right in this method and set the EntityKey & VideoId properties to be ignored, but that seems pretty sloppy"
I don't think thats sloppy at all. You really shouldn't touch a EntityKey/PK after its been persisted and probably should codify its staticness in some way.
Entity Framework
"Now we get to the lovely EF issue of not being allowed to change the property, VideoId, because it's used by the EntityKey property on the Video entity. Lovely."
Lovely? EF is not forcing you to not update your PK. Inside the generated models there is a property change check inside the setter for your keys. The solution would be to change the generated code. Depending on your model volatility this may not be practical but it is an option.

Try mapping to an existing object:
entity = Mapper.Map<MyDTO, NyEntity>(dto, entity);
And keep the Ignore()'s in place.
http://groups.google.com/group/automapper-users/browse_thread/thread/24a90f22323a27bc?fwc=1&pli=1

I'm in the same scenario.
The only solution I got is to Ignore the PK field in the mapping from DTO -> Entity.
Such Rule can be achieved by the following line of code during the Automapper Configuration:
Mapper.CreateMap<MyDTO, MyEntity>().ForMember("EntityPK",r=>r.Ignore());
As far as I know, the only way to get EF works with Detached Entities is mapping the DTO to the Entity you got from DB before the SaveChanges (as you did in the example).

This may help if you want to avoid putting .Ignore()s on every Entity you want to Map.
http://www.prosoftnearshore.com/blog/post/2012/03/14/Using-AutoMapper-to-update-Entity-Framework-properties.aspx
In essence, you'd configure AutoMapper to ignore all Entity properties that are not scalar:
AutoMapper.Mapper.CreateMap<EntityType, EntityType>()
.ForAllMembers(o => {
o.Condition(ctx =>
{
var members = ctx.Parent.SourceType.GetMember(ctx.MemberName); // get the MemberInfo that we are mapping
if (!members.Any())
return false;
return members.First().GetCustomAttributes(typeof(EdmScalarPropertyAttribute), false).Any(); // determine if the Member has the EdmScalar attribute set
});
});
Perhaps some additional work can be added to avoid resetting if the property is a PK (a property in the EdmScalarPropertyAttribute instance (EntityKey == true?) tells you this).

Please be aware that example provided by "Mauricio Morales" will work only if you do not use prefixes. If you use them then you need to change above code slightly in more or less way like this:
Mapper.CreateMap<tempOR_Order, OR_Order>()
.ForMember(m => m.OR_ID, exp => exp.Ignore())
.ForMember(m => m.OR_CU_ID, exp => exp.Ignore())
.ForAllMembers(o => o.Condition(ctx =>
{
var members = ctx.Parent.SourceType.GetMember(ctx.MemberName); // get the MemberInfo that we are mapping
if (!members.Any())
{
members = ctx.Parent.SourceType.GetMember("temp" + ctx.MemberName);
if (!members.Any())
return false;
}
return members.First().GetCustomAttributes(typeof(EdmScalarPropertyAttribute), false).Any(); // determine if the Member has the EdmScalar attribute set
}));
That is, you need to include additional checks inside if (!members.Any()) statement. Without this, function return false and mapping will not work.

Related

Entity Framework Instance tracking error with mapping sub-objects - is there an elegant solution?

Some 2 years+ ago I asked this question which was kindly solved by Steve Py.
I am having a similar but different problem now when mapping with sub-objects. I have had this issue a few times and worked around it, but facing doing so again, I can't help thinking there must be a more elegant solution. I am coding a memebership system in Blazor Wasm and wanting update membership details via a web-api. All very normal.
I have a library function to update the membership:
public async Task<MembershipLTDTO> UpdateMembershipAsync(APDbContext context, MembershipLTDTO sentmembership)
{
Membership? foundmembership = context.Memberships.Where(x =>x.Id == sentmembership.Id)
.Include(x => x.MembershipTypes)
.FirstOrDefault();
if (foundmembership == null)
{
return new MembershipLTDTO { Status = new InfoBool(false, "Error: Membership not found", InfoBool.ReasonCode.Not_Found) };
}
try
{
_mapper.Map(sentmembership, foundmembership, typeof(MembershipLTDTO), typeof(Membership));
//context.Entry(foundmembership).State = EntityState.Modified; <-This was a 'try-out'
context.Memberships.Update(foundmembership);
await context.SaveChangesAsync();
sentmembership.Status = new InfoBool(true, "Membership successfully updated");
return sentmembership;
}
catch (Exception ex)
{
return new MembershipLTDTO { Status = new InfoBool(false, $"{ex.Message}", InfoBool.ReasonCode.Not_Found) };
}
}
The Membership object is an EF DB object and references a many to many list of MembershipTypes:
public class Membership
{
[Key]
public int Id { get; set; }
...more stuff...
public List<MembershipType>? MembershipTypes { get; set; } // The users membership can be several types. e.g. Employee + Director + etc..
}
The MembershipLTDTO is a lightweight DTO with a few heavy objects removed.
Executing the code, I get an EF exception:
The instance of entity type 'MembershipType' cannot be tracked because another instance with the same key value for {'Id'} is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached.
I think (from the previous question I asked some time ago) that I understand what is happening, and previously, I have worked around this by having a seperate function that would in this case update the membership types. Then, stripping it out of the 'found' and 'sent' objects to allow Mapper to do the rest.
In my mapping profile I have the mappings defines as follows for these object types:
CreateMap<Membership, MembershipLTDTO>();
CreateMap<MembershipLTDTO, Membership>();
CreateMap<MembershipTypeDTO, MembershipType>();
CreateMap<MembershipType, MembershipTypeDTO>();
As I was about to go and do that very thing again, I was wondering if I am missing a trick with my use of Mapper, or Entity Framework that would allow it to happen more seamlessly?
A couple of things come to mind. The first thing is that the call to context.Memberships.Update(foundmembership); isn't required here so long as you haven't disabled tracking in the DbContext. Calling SaveChanges will build an UPDATE SQL statement for whatever values change (if any) where Update will attempt to overwrite the entitiy(ies).
The issue you are likely encountering is common when dealing with references, and I would recommend a different approach because of this. To outline this, lets look at Membership Types. These would typically be a known list that we want to associate to new and existing memberships. We're not going to ever expect to create a new membership type as part of an operation where we create or update a membership, just add or remove associations to existing memberships.
The problem with using Automapper for this is when we want to associate another membership type in our passed in DTO. Say we have existing data that had a membership associated with Membership Type #1, and we want to add MemberShip Type #2. We load the original entity types to copy values across, eager loading membership types so we get the membership and Type #1, so far so good. However, when we call Mapper.Map() it sees a MemberShip Type #2 in the DTO, so it will add a new entity with ID #2 into the collection of our loaded Membership's Types collection. From here, one of three things can happen:
1) The DbContext was already tracking an instance with ID #2 and
will complain when Update tries to associate another entity reference
with ID #2.
2) The DbContext isn't tracking an instance, and attempts to add #2
as a new entity.
2.1) The database is set up for an Identity column, and the new
membership type gets inserted with the next available ID. (I.e. #16)
2.2) The database is not set up for an Identity column and the
`SaveChanges` raises a duplicate constraint error.
The issue here is that Automapper doesn't have knowledge that any new Membership Type should be retrieved from the DbContext.
Using Automapper's Map method can be used to update child collections, though it should only be used to update references that are actual children of the top-level entity. For instance if you have a Customer and a collection of Contacts where updating the customer you want to update, add, or remove contact detail records because those child records are owned by, and explicitly associated to their customer. Automapper can add to or remove from the collection, and update existing items. For references like many-to-many/many-to-one we cannot rely on that since we will want to associate existing entities, not add/remove them.
In this case, the recommendation would be to tell Automapper to ignore the Membership Types collection, then handle these afterwards.
_mapper.Map(sentmembership, foundmembership, typeof(MembershipLTDTO), typeof(Membership));
var memberShipTypeIds = sentmembership.MembershipTypes.Select(x => x.MembershipTypeId).ToList();
var existingMembershipTypeIds = foundmembership.MembershipTypes.Select(x => x.MembershipTypeId).ToList();
var idsToAdd = membershipTypeIds.Except(existingMembershipTypeIds).ToList();
var idsToRemove = existingMembershipTypeIds.Except(membershipTypeIds).ToList();
if(idsToRemove.Any())
{
var membershipTypesToRemove = foundmembership.MembershipTypes.Where(x => idsToRemove.Contains(x.MembershipTypeId)).ToList();
foreach (var membershipType in membershipTypesToRemove)
foundmembership.MembershipTypes.Remove(membershipType;
}
if(idsToAdd.Any())
{
var membershipTypesToAdd = context.MembershipTypes.Where(x => idsToRemove.Contains(x.MembershipTypeId)).ToList();
foundmembership.MembershipTypes.AddRange(membershipTypesToAdd); // if declared as List, otherwise foreach and add them.
}
context.SaveChanges();
For items being removed, we find those entities in the loaded data state and remove them from the collection. For new items being added, we go to the context, fetch them all, and add them to the loaded data state's collection.
Notwithstanding marking Steve Py's solution as the answer, because it is a solution that works, though not as 'elegant' as I would have liked.
I was pointed in another direction however by the comment from
Lucian Bargaoanu, which, though a little cryptic, after some digging I found could be made to work.
To do this I had to add 'AutoMapper.Collection' and 'AutoMapper.Collection.EntityFrameworkCore' to my solution. There was a bit of jiggery pokery around setting it up as the example [here][2], didn't match up with my set up. I used this in my program.cs:
// Auto Mapper Configurations
var mappingConfig = new MapperConfiguration(mc =>
{
mc.AddProfile(new MappingProfile());
mc.AddCollectionMappers();
});
I also had to modify my mapping profile for the object - DTO mapping to this:
//Membership Types
CreateMap<MembershipTypeDTO, MembershipType>().EqualityComparison((mtdto, mt) => mtdto.Id == mt.Id);
Which is used to tell AutoMapper which fields to use for an equality.
I took out the context.Memberships.Update as recommended by Steve Py and it works.
Posted on behalf of the question asker

Problem with EF Core updating nested entities when using automapper

I am maintaining an application which uses EF Core to persist data to a SQL database.
I am trying to implement a new feature which requires me to retrieve an object from the database (Lets pretend its an order) manipulate it and some of the order lines which are attached to it and save it back into the database. Which wouldn't be a problem but I have inherited some of this code so need to try to stick to the existing way of doing things.
The basic process for data access is :
UI -> API -> Service -> Repository -> DataContext
The methods in the repo follow this pattern (Though I have simplified it for the purposes of this question)
public Order GetOrder(int id)
{
return _context.Orders.Include(o=>o.OrderLines).FirstOrDefault(x=>x.Id == id);
}
The service is where business logic and mapping to DTOs are applied, this is what the GetOrder method would look like :
public OrderDTO GetOrder(int id)
{
var ord = _repo.GetOrder(id);
return _mapper.Map<OrderDto>(ord);
}
So to retrieve and manipulate an order my code would look something like this
public void ManipulateAnOrder()
{
// Get the order DTO from the service
var order = _service.GetOrder(3);
// Manipulate the order
order.UpdatedBy = "Daneel Olivaw";
order.OrderLines.ForEach(ol=>ol.UpdatedBy = "Daneel Olivaw");
_service.SaveOrder(order);
}
And the method in the service which allows this to be saved back to the DB would look something like this:
public void SaveOrder(OrderDTO order)
{
// Get the original item from the database
var original = _repo.GetOrder(order.Id);
// Merge the original and the new DTO together
_mapper.Map(order, original);
_repo.Save(original);
}
Finally the repositories save method looks like this
public void Save(Order order){
_context.Update(order)
_context.SaveChanges();
}
The problem that I am encountering is using this method of mapping the Entities from the context into DTOs and back again causes the nested objects (in this instance the OrderLines) to be changed (or recreated) by AutoMapper in such a way that EF no longer recognises them as being the entities that it has just given to us.
This results in errors when updating along the lines of
InvalidOperationException the instance of ProductLine cannot be tracked because another instance with the same key value for {'Id'} is already being tracked.
Now to me, its not that there is ANOTHER instance of the object being tracked, its the same one, but I understand that the mapping process has broken that link and EF can no longer determine that they are the same object.
So, I have been looking for ways to rectify this, There are two ways that have jumped out at me as being promising,
the answer mentioned here EF & Automapper. Update nested collections
Automapper.Collection
Automapper.collection seems to be the better route, but I cant find a good working example of it in use, and the implementation that I have done doesn't seem to work.
So, I'm looking for advice from anyone who has either used automapper collections before successfully or anyone that has any suggestions as to how best to approach this.
Edit, I have knocked up a quick console app as an example, Note that when I say quick I mean... Horrible there is no DI or anything like that, I have done away with the repositories and services to keep it simple.
I have also left in a commented out mapper profile which does work, but isn't ideal.. You will see what I mean when you look at it.
Repo is here https://github.com/DavidDBD/AutomapperExample
Ok, after examining every scenario and counting on the fact that i did what you're trying to do in my previous project and it worked out of the box.
Updating your EntityFramework Core nuget packages to the latest stable version (3.1.8) solved the issue without modifying your code.
AutoMapper in fact "has broken that link" and the mapped entities you are trying to save are a set of new objects, not previously tracked by your DbContext. If the mapped entities were the same objects, you wouldn't have get this error.
In fact, it has nothing to do with AutoMapper and the mapping process, but how the DbContext is being used and how the entity states are being managed.
In your ManipulateAnOrder method after getting the mapped entities -
var order = _service.GetOrder(3);
your DbContext instance is still alive and at the repository layer it is tracking the entities you just retrieved, while you are modifying the mapped entities -
order.UpdatedBy = "Daneel Olivaw";
order.OrderLines.ForEach(ol=>ol.UpdatedBy = "Daneel Olivaw");
Then, when you are trying to save the modified entities -
_service.SaveOrder(order);
this mapped entities reach the repository layer and DbContext tries to add them to its tracking list, but finds that it already has entities of same type with same Ids in the list (the previously fetched ones). EF can track only one instance of a specific type with a specific key. Hence, the complaining message.
One way to solve this, is when fetching the Order, tell EF not to track it, like at your repository layer -
public Order GetOrder(int id, bool tracking = true) // optional parameter
{
if(!tracking)
{
return _context.Orders.Include(o=>o.OrderLines).AsNoTracking().FirstOrDefault(x=>x.Id == id);
}
return _context.Orders.Include(o=>o.OrderLines).FirstOrDefault(x=>x.Id == id);
}
(or you can add a separate method for handling NoTracking calls) and then at your Service layer -
var order = _repo.GetOrder(id, false); // for this operation tracking is false

EF resolve navigation property after manual attach

I'm using Entity Framework. I've attached a POCO object representing an entity in the DB to my dbcontext using:
var entity = new MyEntity() { ID = 1, AnotherItemID = 10 };
context.Set<T>().Attach(entity);
So far so good. I can access the set and work with the entity I've added. It's added in the Unchanged state. However, it is only a POCO and not a Proxy. Therefore, when I try to access a navigation property, e.g. myEntity.AnotherItem, I just get a null back.
Does anyone know if there is a way to have EF resolve navigation properties for POCO classes attached in this way? Or of a way to cast the POCO to a proxy class?
Thanks
Update
There are two ways to solve this (of course there may be others too!). One is the Explicit Loading option in the answer below. The other way, which allows lazy loading to work, is to use the DBSet Create method rather than the POCO new keyword when creating entities to be attached. More info about that here:
EF4.3 Code-First, MVC, Lazy Loading After Attaching in POST Action
You can use Explicity Loading:
//When you want to load a reference navigation property
context.Entry(entity).Reference(p => p.AnotherItem).Load();
//When you want to load a collection navigation property
context.Entry(post).Collection(p => p.Items).Load();

Track changes to collections using Entity Framework change tracker

For auditing/history purposes, I am using the Entity Framework change tracker to determine, before writing changes, what has changed and serialize the changes. I can get the changed entities by calling this.ChangeTracker.Entries() in my DbContext derivative and looking at the values for anything marked EntityState.Added, EntityState.Deleted, or EntityState.Modified. This all works great.
My problem is that this method does not work to track changes to collections of EF objects (for instance, an ICollection<Person> property on a PersonGroup object).
I'm sure the EF context must track this somehow -- how else would the database update work, after all? But is it available to me?
What you're looking for is relationship change tracking. You can find it in ObjectStateManager of the underlying ObjectContext, here is how you get all added relationships:
//you need to call DetectChanges
((IObjectContextAdapter)context).ObjectContext.DetectChanges();
var addedRelations = ((IObjectContextAdapter)context).ObjectContext
.ObjectStateManager.GetObjectStateEntries(EntityState.Added)
.Where(e=>e.IsRelationship).ToList();
It turns out you can get at the relationships with this code (assuming it's running inside your DbContext derivative):
((IObjectContextAdapter) this).ObjectContext.ObjectStateManager
.GetObjectStateEntries(EntityState.Added)
.Where(e => e.IsRelationship)
.Select(r => new {EntityKeyInfo = r.CurrentValues[0],
CollectionMemberKeyInfo = r.CurrentValues[1], r.State});
Obviously you can tweak this based on what you need and it's up to do you something useful with it. The first two CurrentValues entries represent EntityKey objects which will allow you to get the IDs of the entities in question.
If you want to deal with deleted entities this won't work and you need to use reflection. Instead of CurrentValues[0] and CurrentValues[1] you can look at the internal properties Key0 and Key1, which are defined in an internal class you can't access at compile time. This will work: r.GetType().GetProperty("Key0", BindingFlags.Instance | BindingFlags.NonPublic).Invoke(r, new object[0]). Note that this is probably not an intended use and could blow up whenever.

EF 4.1 DBContext and Navigation Properties

We've been using EF STEs for a while, but our application has grown quite a bit and we decided to sue the new 4.1 DbContext so we can "evolve" a separate business layer on top of our data layer without having to use different types for it.
In the elementary evaluation for the DbContext way of doing things, I am facing a little problem.
I am used to query and preload required related data like:
return context.Orders.Include("Detail").SingleOrDefault(ord => ord.ID == ID);
And then send the returned object to the UI for modification, and when returned from the UI save the changes to the database.
From what I read so far, doing the "change saving" in DbContext is easily done using code like this:
context.Entry(order).State = EntityState.Modified;
The problem with this code is that it actually marks all properties in the object as modified, a thing that's not allowed for some properties in my model (a business rule).
I resorted to the following solution (which seems to require a lot of code for a relatively small requirement! BTW, changing a modified property state to Unchanged is not supported):
context.Orders.Attach(order);
DbEntityEntry<Order> ordEntity = context.Entry(order);
string[] arr =
{
ordEntity.Property(ord => ord.ID).Name,
ordEntity.Property(ord => ord.ClientID).Name,
};
foreach (string prop in ordEntity.OriginalValues.PropertyNames)
{
if (!arr.Contains(prop))
{
ordEntity.Property(prop).IsModified = true;
}
}
context.SaveChanges();
The problem I am facing with this code is that the "Attach" statement is throwing an exception saying that there is some sort of conflict in the navigation properties in the attached object, even if no changes were made to anything at all! (saving the object exactly as it was retrieved from the database).
The error message is something like:
"Conflicting changes to the role 'Detail' of the relationship 'OrdersDatamodel.FK_Order_Detail' have been detected."
The questions are:
Is there a more "elegant" way for preventing the modification of certain object properties?
Does anybody know what's going on with the exception raised when attaching the object to the context?
Thanks.
From what I read so far, doing the "change saving" in DbContext is easily done using code like this:
context.Entry(order).State = EntityState.Modified;
You rarely need to explicitly set the state. When you modify properties, assuming they are virtual, the state will automatically change to Modified without you having to set it. Otherwise, DetectChanges will pick this up during your call to SaveChanges.

Categories