Giter Site home page Giter Site logo

Child objects with identifying-relationship keys are deleted and then re-added to collections, causing DB delete/insert on SaveChanges about automapper.collection HOT 13 CLOSED

automapper avatar automapper commented on July 24, 2024
Child objects with identifying-relationship keys are deleted and then re-added to collections, causing DB delete/insert on SaveChanges

from automapper.collection.

Comments (13)

TylerCarlson1 avatar TylerCarlson1 commented on July 24, 2024

When you say non-identifying do you mean it doesn't have any Key properties at all? If it doesn't then there's no way for AM.Collections to figure out what is equal to what and defaults to AutoMapper's collection mapping.

If this is not the case can you provide a gist of an example of it failing? Or at least show your Entities and Dtos. Hopefully you aren't creating the mapper before every persist call.

from automapper.collection.

alewkowicz avatar alewkowicz commented on July 24, 2024

When I say "Non-identifying" I'm referring to the primary key of the child object not containing a foreign key to the parent object. It does have a primary key of its own. It also has a foreign key to the parent, but that property isn't part of its primary key.

It's the children with the identifying relationship (where the primary key of the child includes the identifier of the parent) that has the problem.

I will put together a simplified example and post it. Thanks!

from automapper.collection.

TylerCarlson1 avatar TylerCarlson1 commented on July 24, 2024

My best guess is that you don't have the complete Key set in property map for the Identifying relationship.
ID is mapped but ParentID isn't, because you can get away with it in non-identifying relationships. The thing with AM.Collections.EF is that if the complete key doesn't have property maps it will say it can't generate an equivalency and default back to AM collection functionality. You either have to map the ParentID or explicitly make an equality expression for that map using just ID and not ParentID.

If this isn't the case a complete example would really help.

from automapper.collection.

alewkowicz avatar alewkowicz commented on July 24, 2024

When you say "map the parent ID" you mean that my mapping in the AutoMapper profile has to explicitly handle (with a ForMember) the parent ID property?

Do you mean that I need to explicitly specify the properties to be used as the identifier? (I've forgotten the name of the AM.Collection class that you do this with.) But I thought AM.Collections.EF could do this automatically, based on the EF config.

Or do you mean that my EF config doesn't include the full key of the object? It does, as the gist will show.

My gist is here: https://gist.github.com/alewkowicz/a75add41d1d2165f8a100539a4700d98

from automapper.collection.

TylerCarlson1 avatar TylerCarlson1 commented on July 24, 2024

This is REALLY confusing, but I think what the issue is that what you expect to happen is incorrect.

From what I've seen in AM.Collection and EF6 in general with lists, is that Identity and Non-Identity have the exact same functionality when updating with no changes. If you don't change anything, which from your example seems like you are not, then every item should be unchanged, because well it didn't change anything.

From your Original post it seemed like the application was adding and removing items from the Identity Collection, which SHOULD NOT happen if the objects are all the same.

The only difference I've found with Identity and Non-Identity relationships is that when you actually remove an object from a collection the Identity relationship will Delete it, where as the Non-Identity will Orphan it. The Non-Identity will set the FK property to null. In your example LogId will be set to null when removed from the Log.Events list. If LogId is int and not int? then it will throw and exception saying it can't set to null.

https://stackoverflow.com/questions/11033348/is-it-possible-to-remove-child-from-collection-and-resolve-issues-on-savechanges explains the situation people usually have. And it's mostly from people having a Non-Identity relationship and expecting removing from the list to mark it as deleted.

from automapper.collection.

alewkowicz avatar alewkowicz commented on July 24, 2024

I understand the behavior described in the links. What I saw was the application adding and removing items from the child collection, which I agree should not happen given how I had things configured.

But I think I've figured it out, thanks to your remark about maybe not having the complete Key set in the property map.

The raw data I get back from the API and convert into POCOs doesn't have parent id fields. The EF POCOs I map to, do. They need them for the database.

I was handling this by adding them manually in an AfterMap call on the parent object:

            .AfterMap((apiLog, dbLog) =>
            {
                foreach (var ev in dbLog.Events)
                {
                    ev.Log = dbLog;
                    ev.LogId = dbLog.Id;
                }
           }

But I suspect that's too late in the process: the AM.Collection.EF comparison of the child object to the DB is probably happening before that parent object AfterMap.

So I added a ParentID to the DTO POCOs. The process that maps the API data to the POCOs will default it to 0. And then I created a BeforeMap call on the parent object, to populate the ParentID on the source object:

     profile.CreateMap<ApiLog.Log, Db.Log>()
            .BeforeMap((api, db) =>
            {
                if (api.Events != null)
                {
                    foreach (var e in api.Events)
                    {
                        e.Event.LogId = api.Id;
                    }
                }
            })

Once I did this, that test method I put in the gist started behaving differently. It left all the child objects unchanged. No longer adding and deleting.

So, my conclusion: if I used InsertOrUpdate, it appeared that the child objects were compared to the database before the parent object AfterMap that finished populating them, ran.

Does this make sense?

I don't want to have to pre-process the objects I've materialized from the API data before I feed them to AutoMapper. I don't want the API POCOs to have to have fields that don't come from the API data. But that's the workaround I'm facing.

Thanks.

from automapper.collection.

alewkowicz avatar alewkowicz commented on July 24, 2024

Actually, my memory from when I looked at the InsertOrUpdate<T> code was that you did the EF entity-finding BEFORE you map the child object. So it didn't matter when in the mapping I populated the target object's ParentID: you'd use the source object's values to see whether there was a matching object already.

Is there a way to configure things so that I can avoid forcing a ParentID property into my source object? That I can supply that value "on the fly" from the ResolutionContext, or something?

Thanks for all your help. I appreciate this.

from automapper.collection.

TylerCarlson1 avatar TylerCarlson1 commented on July 24, 2024

You should just be able to use AM.Collection's EqualityComparison which will overwrite what AM.Collections.EF does with the key properties.

CreateMap<EntityDto,Entity>().EqualityComparison((edto, e) => edto.ID == e.ID);

Then you don't have to pass LogID for the EntityDto from the API just so it won't add/remove. Setting LogID isn't important to set anyways, because having it in the child list EF will handle that.

If you have a convention that creates the Maps automatically and don't want to call CreateMap explicitly then you can look at GenerateEntityFrameworkPrimaryKeyPropertyMaps code and write your own to use all primary key values that have matches, instead of defaulting to AM logic when not all match.

Example of what it might look like.

public class GenerateEntityFrameworkPartialPrimaryKeyPropertyMaps<TDatabaseContext> : IGeneratePropertyMaps
        where TDatabaseContext : IObjectContextAdapter, new()
    {
        private readonly TDatabaseContext _context = new TDatabaseContext();
        private readonly MethodInfo _createObjectSetMethodInfo = typeof(ObjectContext).GetMethod("CreateObjectSet", Type.EmptyTypes);
        
        public IEnumerable<PropertyMap> GeneratePropertyMaps(TypeMap typeMap)
        {
            var propertyMaps = typeMap.GetPropertyMaps();
            try
            {
                var createObjectSetMethod = _createObjectSetMethodInfo.MakeGenericMethod(typeMap.DestinationType);
                dynamic objectSet = createObjectSetMethod.Invoke(_context.ObjectContext, null);

                IEnumerable<EdmMember> keyMembers = objectSet.EntitySet.ElementType.KeyMembers;
                var primaryKeyPropertyMatches = keyMembers.Select(m => propertyMaps.FirstOrDefault(p => p.DestinationProperty.Name == m.Name)).Where(pm => pm != null); // Added Where statement to ignore primary key values that don't have matches for partial matches

                return primaryKeyPropertyMatches;
            }
            catch (Exception)
            {
                return Enumerable.Empty<PropertyMap>();
            }
        }
    }

from automapper.collection.

alewkowicz avatar alewkowicz commented on July 24, 2024

Thank you! I will investigate, first the Id only option, then the custom object, if necessary.

from automapper.collection.

alewkowicz avatar alewkowicz commented on July 24, 2024

The ID only option works without throwing errors, but I have a question.

In the case I'm dealing with, the Event entity's primary key includes the parent ID because the provided Id value from the API data is not unique across the entire domain. It's only unique within the children of a given Log.

You suggest

  CreateMap<EntityDto,Entity>().EqualityComparison((edto, e) => edto.ID == e.ID);

... but I'm concerned I might get a hit on an Event from a different Log.

If I did, I believe the mapping of the LogId in the AfterMap for the Log entity would wind up overwriting the LogId in the child, which would result in an EF exception.

That hasn't happened yet in my testing, though. So, my question:

Are AM and AM.Collection.EF smart enough that they'll only be searching the collection of known children of the parent entity when they try to match a child using the EqualityComparison, or do they search the entire DbSet?

Thanks!

from automapper.collection.

TylerCarlson1 avatar TylerCarlson1 commented on July 24, 2024

They compare just the collection, because that's the only thing they have to compare with AM.Collection mappers. They have no access or Idea of DBSet, it's just AM.Collection.EF uses the schema to generate the equality comparison that's given to AM.Collection which has no reference to EF.

You really shouldn't have that scenario happen anyways because of how the DB should be set up. I would more than likely duplicate the Event under the Log with the same child ID, or if there's a match by ID would copy to that. Also you shouldn't have to set 'LogID' in After Map because just being part of the child collection will handle that for you. Unless there's something else in the code that requires it to work that I'm unaware of.

It's not that they are smart enough to just pick the child collection. It's actually they are stupid enough not to know that there's anything else. :)

from automapper.collection.

alewkowicz avatar alewkowicz commented on July 24, 2024

Thank you!

from automapper.collection.

alewkowicz avatar alewkowicz commented on July 24, 2024

I will close this now as the issue is resolved. Thank you again!

from automapper.collection.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.