Robert McCoy
Robert McCoy

Reputation: 671

EntityFramework Core - Copying an entity and placing it back into the database

Is there a best practice for doing a copy of an entity, making some changes to it based on user input, and then re-inserting it into the database?

Some other Stackoverflow threads have mentioned that EF will handle inserting new objects for you even if the same primary key exists in the database, but I'm not quite sure that's how EF Core is handling it. Whenever I try and copy an object I get an error of

Cannot insert explicit value for identity column in table when IDENTITY_INSERT is set to OFF

Basically I just need a clean way to copy an object, make some changes to it based on user input, and then insert that copy back into the database, and have the Id auto-increment properly. Is there a best practice or simple way of doing this without having to manually set properties to null or empty?

EDIT: Example code for retrieving the object from the database:

    public Incident GetIncidentByIdForCloning(int id)
    {
        try
        {
            return _context.Incident.Single(i => i.IncidentId == id);
        }
        catch
        {
            return null;
        }
    }

Code after retrieving object (As some fields are auto-generated like RowVersion which is a Timestamp):

public IActionResult Clone([FromBody]Incident Incident)
    {
        var incidentToCopy = _incidentService.IncidentRepository.GetIncidentByIdForCloning(Incident.IncidentId);
        incidentToCopy.IncidentTrackingRefId = _incidentService.IncidentRepository.GetNextIdForIncidentCategoryAndType(
            Incident.IncidentCategoryLookupTableId, Incident.IncidentTypeLookupTableId).GetValueOrDefault(0);
        incidentToCopy.RowVersion = null;
        incidentToCopy.IncidentId = 0; //This will fail with or without this line, this was more of a test to see if manually setting would default the insert operation, such as creating a brand new object would normally do.
        incidentToCopy.IncidentCategoryLookupTableId = Incident.IncidentCategoryLookupTableId;
        incidentToCopy.IncidentTypeLookupTableId = Incident.IncidentTypeLookupTableId;
        var newIncident = _incidentService.IncidentRepository.CreateIncident(incidentToCopy);
...

I realize I could just make an entirely new object and do left-hand copying, but that seems terribly inefficient and I want to know if EF Core offers better solutions.

Upvotes: 12

Views: 14888

Answers (4)

user692942
user692942

Reputation: 16672

In the end, we went with a generic approach based on this answer so we could use it all over. The saveChanges flag defaults to true but allows control of switching it off and on if building up a complex ORM structure before calling SaveChangeAsync().

public async Task<T> CloneAsync(T source, bool saveChanges = true)
{
    var destination = await _dbContext.Set<T>()
        .AsNoTracking()
        .FirstOrDefaultAsync(r => r.Id == source.Id);

    destination.Id = 0;
    // Additional values you might need to set in your implementation (not required).
    destination.Identifier = Guid.NewGuid();

    // Call the repository AddAsync() method.
    destination = await AddAsync(destination, saveChanges);
    return destination;
}

Upvotes: 0

dkmann
dkmann

Reputation: 621

In your IncidentRepository class try getting the Incident by using AsNoTracking and it should get tracked as a new entity when it is added.

public void Clone(int id)
{
    // Prevent tracking changes to the object.
    var incident = _context.AsNoTracking().SingleOrDefault(i => i.Id == id);

    // Setting back to 0 should treat the object Id as unset.
    incident.Id = 0;

    // Add the Incident while it is untracked will treat it as a new entity.
    _context.Incidents.Add(incident);
    _context.SaveChanges();
}

Upvotes: 7

GlennSills
GlennSills

Reputation: 4177

I believe what is happening here is the following:

When you retrieve a value from the database, it gets stored in something like

context.ChangeTracker.Entries<Incident>

Which is a collection of Incident entries being tracked. When you change the id property of the incident object you've retrieved you are sort of abusing the ChangeTracker in the name of efficiency. The ChangeTracker does not believe you have created a new object. You might be able to try something like finding the entry in the ChangeTracker and set it's state to detached then after you've set the id to 0 add the object back to the context.DbSet but at that point you have probably made things way more complicated than simply copying the object.

Upvotes: 0

Robert McCoy
Robert McCoy

Reputation: 671

So I went through the "Possible duplicate" thread a bit more than I did when I initially stumbled upon it before creating this one, and there was a not-so-highly upvoted solution that I overlooked that essentially just grabs all of the values at once when retrieving the object from the database - and it doesn't retrieve a reference to that object in the process. My code now looks something like this:

try
{
    var incidentToCopy = _context.Incident.Single(i => i.IncidentId == id);
    return (Incident) _context.Entry(incidentToCopy).CurrentValues.ToObject();
}

Upvotes: 12

Related Questions