13

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.

3
  • In our system, we use Auotmapper. Automapper maps our 'Front-end model' (Model received from WebApp and used for transferring data, to our Domain Model. Commented Sep 14, 2022 at 9:17
  • Disposing of your context, for example, by placing it in a using statement after every read/write, would eliminate the Tracking issue. Unit of Work approach Commented Sep 14, 2022 at 9:19
  • 1
    Try removing the entitystate = modified line. Commented Sep 14, 2022 at 9:20

3 Answers 3

30

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.

Sign up to request clarification or add additional context in comments.

1 Comment

this was super useful thank you!
1

//no tracker //straight to the db

await _context. .Where(r => r.Id == Id).ExecuteUpdateAsync();

Comments

-1

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

Example:

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

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

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

2 Comments

This has the opposite effect. EF no longer knows what has changed
Above is correct as no tracking is often just used for performance when u dont want the entity tracked

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.