I know that this question had been asked already, but those answers didn't work for me. So, I beg not to close this question! 😎
The goal is simple:
What I have:
• Client relates to table in database.
• ClientDto is the same as Client, but without Comment property (to emulate situation when user must not see this field).
public class Client
{
public string TaskNumber { get; set; }
public DateTime CrmRegDate { get; set; }
public string Comment { get; set; }
}
public class ClientDto
{
public string TaskNumber { get; set; }
public DateTime CrmRegDate { get; set; }
}
CREATE TABLE dbo.client
(
task_number varchar(50) NOT NULL,
crm_reg_date date NOT NULL,
comment varchar(3000) NULL
);
-- Seeding
INSERT INTO dbo.client VALUES
('1001246', '2010-09-14', 'comment 1'),
('1001364', '2010-09-14', 'comment 2'),
('1002489', '2010-09-22', 'comment 3');
public class ClientsController : Controller
{
private readonly ILogger logger;
private readonly TestContext db;
private IMapper mapper;
public ClientsController(TestContext db, IMapper mapper, ILogger<ClientsController> logger)
{
this.db = db;
this.logger = logger;
this.mapper = mapper;
}
public IActionResult Index()
{
var clients = db.Clients;
var clientsDto = mapper.ProjectTo<ClientDto>(clients);
return View(model: clientsDto);
}
public async Task<IActionResult> Edit(string taskNumber)
{
var client = await db.Clients.FindAsync(taskNumber);
return View(model: client);
}
[HttpPost]
public async Task<IActionResult> Edit(ClientDto clientDto)
{
// For now it's empty - it'll be used for saving entity
}
}
builder.Services
.AddAutoMapper(config =>
{
config.CreateMap<Client, ClientDto>();
config.CreateMap<ClientDto, Client>();
}
internal static class EntititesExtensions
{
internal static string ToJson<TEntity>(this EntityEntry<TEntity> entry) where TEntity: class
{
var states = from member in entry.Members
select new
{
State = Enum.GetName(member.EntityEntry.State),
Name = member.Metadata.Name,
Value = member.CurrentValue
};
var json = JsonSerializer.SerializeToNode(states);
return json.ToJsonString(new JsonSerializerOptions { WriteIndented = true });
}
}
I need to map only changed properties from ClientDto to Client in Edit(ClientDto clientDto).
Just map ClientDto to Client:
[HttpPost]
public async Task<IActionResult> Edit(ClientDto clientDto)
{
var client = mapper.Map<Client>(clientDto);
logger.LogInformation(db.Entry(client).ToJson());
db.Update(client);
await db.SaveChangesAsync();
return View(viewName: "Success");
}
The problem with this code is that absolutely new Client entity gets created which properties will be filled by ClientDto. This means that Comment property will be NULL and it will make NULL the column comment in database. In general case, all hidden properties (i.e. absent in DTO) will have their default values. Not good.
Output:
[
{
"State": "Detached",
"Name": "TaskNumber",
"Value": "1001246"
},
{
"State": "Detached",
"Name": "Comment",
"Value": null
},
{
"State": "Detached",
"Name": "CrmRegDate",
"Value": "2010-09-15T00:00:00"
}
]
I tried to use the solution from the answer I mentioned above:
public static IMappingExpression<TSource, TDestination> MapOnlyIfChanged<TSource, TDestination>(this IMappingExpression<TSource, TDestination> map)
{
map.ForAllMembers(source =>
{
source.Condition((sourceObject, destObject, sourceProperty, destProperty) =>
{
if (sourceProperty == null)
return !(destProperty == null);
return !sourceProperty.Equals(destProperty);
});
});
return map;
}
In configuration:
builder.Services
.AddAutoMapper(config =>
{
config.CreateMap<Client, ClientDto>();
config.CreateMap<ClientDto, Client>().MapOnlyIfChanged();
});
Running same code as in solution 1, we get the same output (Comment is null):
[
{
"State": "Detached",
"Name": "TaskNumber",
"Value": "1001246"
},
{
"State": "Detached",
"Name": "Comment",
"Value": null
},
{
"State": "Detached",
"Name": "CrmRegDate",
"Value": "2010-09-15T00:00:00"
}
]
Not good.
Let's take another route:
Map in order to overwrite the values from ClientDto to Client from database.builder.Services
.AddAutoMapper(config =>
{
config.CreateMap<Client, ClientDto>();
config.CreateMap<ClientDto, Client>()
.ForMember(
dest => dest.TaskNumber,
opt => opt.Condition((src, dest) => src.TaskNumber != dest.TaskNumber))
.ForMember(
dest => dest.TaskNumber,
opt => opt.Condition((src, dest) => src.CrmRegDate != dest.CrmRegDate));
});
[HttpPost]
public async Task<IActionResult> Edit(ClientDto clientDto)
{
var dbClient = await db.Clients.FindAsync(clientDto.TaskNumber);
logger.LogInformation(db.Entry(dbClient).ToJson());
mapper.Map(
source: clientDto,
destination: dbClient,
sourceType: typeof(ClientDto),
destinationType: typeof(Client)
);
logger.LogInformation(db.Entry(dbClient).ToJson());
db.Update(dbClient);
await db.SaveChangesAsync();
return View(viewName: "Success");
}
This finally works, but it has problem - it still modifies all properties. Here's the output:
[
{
"State": "Modified",
"Name": "TaskNumber",
"Value": "1001246"
},
{
"State": "Modified",
"Name": "Comment",
"Value": "comment 1"
},
{
"State": "Modified",
"Name": "CrmRegDate",
"Value": "2010-09-15T00:00:00"
}
]
So, how to make Automapper update only modified properties? 😐
You do not need to call context.Update() explicitly.
When loading entity, EF remember every original values for every mapped property.
Then when you change property, EF will compare current properties with original and create appropriate update SQL only for changed properties.
For further reading: Change Tracking in EF Core
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With