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