Home > Software design >  Better(Cleaner) way to Update Record using Entity Framework Core
Better(Cleaner) way to Update Record using Entity Framework Core

Time:09-14

I have gone through some solutions provided here on SO like this. Basically I have a User class that has a lot of properties.

public class Users
{
    [Key]
    public Guid Id { get; set; }
    [MaxLength(200)]
    public string Username { get; set; }
    public bool Enabled { get; set; }
    public string Name { get; set; }
    //...Many more properties
}

And UpdateUser() method to update the user. My issue is the method is long and doesn't look clean because of the many properties that are being updated.

public async Task<UpdateUserResponse> UpdateUser(UpdateUserRequest userRequest)
{
    var user = await _userRepository.GetByIdAsync(userRequest.Id);

    if (user == null)
        throw new HttpException(HttpStatusCode.BadRequest, $"User does not exist");
    
    user.EmailAddress = userRequest.EmailAddress;
    user.Enabled = userRequest.Enabled;
    user.Name = userRequest.Name;
    user.SurName = userRequest.SurName;
    //...many more properties which make this method to be long
    
    // I attempted to do something like this which gave the error
    // var usrObj = JsonConvert.DeserializeObject<Users>(JsonConvert.SerializeObject(userRequest));
    // var res = await _userRepository.UpdateAsync(usrObj);
        
    context.Entry(user).State = EntityState.Modified;

    var result = await context.SaveChangesAsync();
    //......

    return response;
}

I tried to deserialize the userRequest object into type User but in the UpdateAsync() method, Entity Framework Core complains with the following message, which makes sense:

The instance of entity type 'Users' cannot be tracked because another instance with the same key value for {'Id'} is already being tracked.

I'm looking for a cleaner way of doing this.

CodePudding user response:

First off, when using load/modify/save pattern, do not call

context.Entry(user).State = EntityState.Modified;

or

context.Update(user);

These force updating all properties of the entity into database and are applicable for disconnected entity scenarios when you don't have fresh loaded from database (and tracked by the context change tracker) entity instance.

When you having it (as with the code in question), all you need it is to set some properties, and then EF Core change tracker will determine automatically which ones to update in the database (may not issue update command at all if all property values are the same as the original ones).

With that being said, the concrete question is how to set target properties in interest without writing a lot of code. EF Core provides out of the box EntityEntry.CurrentValues property and method called SetValues, which along with entity class instance accepts also a dictionary or any object, as soon as the property names and types match. Something similar to AutoMapper default mapping. But note that this works only for primitive (data) properties, navigation properties need manual update.

So the code in question could be simplified as

public async Task<UpdateUserResponse> UpdateUser(UpdateUserRequest userRequest)
{
    var user = await _userRepository.GetByIdAsync(userRequest.Id);

    if (user == null)
        throw new HttpException(HttpStatusCode.BadRequest, $"User does not exist");
    
    // Assuming GetByIdAsync method returns tracked entity instances
    // If not, you'd need to modify it or use another one which does that
    // The next call is all you need to update matching properties
    context.Entry(user).CurrentValues.SetValues(userRequest);

    var result = await context.SaveChangesAsync();
    //......

    return response;
}

In case you need more flexibility (have non matching source properties, or don`t want to apply all source properties, or want to handle navigation properties etc.), you'd better use some 3rd party library like AutoMapper.

CodePudding user response:

You just need to use AsNoTracking() after your where condition

Example:

var blogs = context.Blogs.Where(<your condition>)
    .AsNoTracking().
    .ToList();

https://docs.microsoft.com/en-us/ef/core/querying/tracking

https://www.c-sharpcorner.com/UploadFile/ff2f08/entity-framework-and-asnotracking/#:~:text=The AsNoTracking() extension method,are returned by the query.

  • Related