如何使用 Automapper 和 Entity Framework 仅映射已更改的属性
How to map only changed properties using Automapper and Entity Framework
我已经知道这个问题 had been asked,但这些答案对我不起作用。所以,我请求不要关闭这个问题!
目标很简单:
- 使用 Entity Framework 从数据库中获取实体。
- 将此实体传递给 MVC 视图。
- 编辑页面上的实体。
- 保存实体。
我有什么:
实体
• Client
与数据库中的 table 相关。
• ClientDto
与 Client
相同,但没有 Comment
属性(模拟用户不得看到此字段的情况)。
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; }
}
数据库table
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');
MVC 控制器
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>();
}
助手(输出实体条目为JSON)
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 });
}
}
问题
我只需要在 Edit(ClientDto clientDto)
中将更改的属性从 ClientDto
映射到 Client
。
使用了哪些解决方案
解决方案 1
只需将 ClientDto
映射到 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");
}
此代码的问题是创建了绝对新的 Client
实体,其属性将由 ClientDto
填充。这意味着 Comment
属性 将是 NULL
,这将使 NULL
成为数据库中的列 comment
。在一般情况下,所有隐藏属性(即 DTO 中不存在的属性)都将具有其默认值。 不好。
输出:
[
{
"State": "Detached",
"Name": "TaskNumber",
"Value": "1001246"
},
{
"State": "Detached",
"Name": "Comment",
"Value": null
},
{
"State": "Detached",
"Name": "CrmRegDate",
"Value": "2010-09-15T00:00:00"
}
]
解决方案 2
我尝试使用我上面提到的答案中的solution:
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;
}
配置中:
builder.Services
.AddAutoMapper(config =>
{
config.CreateMap<Client, ClientDto>();
config.CreateMap<ClientDto, Client>().MapOnlyIfChanged();
});
运行 与解决方案 1 相同的代码,我们得到相同的输出(Comment
是 null
):
[
{
"State": "Detached",
"Name": "TaskNumber",
"Value": "1001246"
},
{
"State": "Detached",
"Name": "Comment",
"Value": null
},
{
"State": "Detached",
"Name": "CrmRegDate",
"Value": "2010-09-15T00:00:00"
}
]
不好.
解决方案 3
我们走另一条路:
- 从数据库中获取实体。
- 使用非泛型
Map
以覆盖数据库中从 ClientDto
到 Client
的值。
- 调整映射配置以跳过值相等的属性。
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");
}
这个 终于可以工作了 ,但是它有问题 - 它仍然修改 所有 属性。这是输出:
[
{
"State": "Modified",
"Name": "TaskNumber",
"Value": "1001246"
},
{
"State": "Modified",
"Name": "Comment",
"Value": "comment 1"
},
{
"State": "Modified",
"Name": "CrmRegDate",
"Value": "2010-09-15T00:00:00"
}
]
那么,如何让Automapper只更新修改过的属性呢?
您不需要显式调用 context.Update()
。
加载实体时,EF 会记住每个映射的每个原始值 属性。
然后,当您更改 属性 时,EF 会将当前属性与原始属性进行比较,并仅为更改后的属性创建适当的更新 SQL。
我已经知道这个问题 had been asked,但这些答案对我不起作用。所以,我请求不要关闭这个问题!
目标很简单:
- 使用 Entity Framework 从数据库中获取实体。
- 将此实体传递给 MVC 视图。
- 编辑页面上的实体。
- 保存实体。
我有什么:
实体
• Client
与数据库中的 table 相关。
• ClientDto
与 Client
相同,但没有 Comment
属性(模拟用户不得看到此字段的情况)。
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; }
}
数据库table
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');
MVC 控制器
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>();
}
助手(输出实体条目为JSON)
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 });
}
}
问题
我只需要在 Edit(ClientDto clientDto)
中将更改的属性从 ClientDto
映射到 Client
。
使用了哪些解决方案
解决方案 1
只需将 ClientDto
映射到 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");
}
此代码的问题是创建了绝对新的 Client
实体,其属性将由 ClientDto
填充。这意味着 Comment
属性 将是 NULL
,这将使 NULL
成为数据库中的列 comment
。在一般情况下,所有隐藏属性(即 DTO 中不存在的属性)都将具有其默认值。 不好。
输出:
[
{
"State": "Detached",
"Name": "TaskNumber",
"Value": "1001246"
},
{
"State": "Detached",
"Name": "Comment",
"Value": null
},
{
"State": "Detached",
"Name": "CrmRegDate",
"Value": "2010-09-15T00:00:00"
}
]
解决方案 2
我尝试使用我上面提到的答案中的solution:
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;
}
配置中:
builder.Services
.AddAutoMapper(config =>
{
config.CreateMap<Client, ClientDto>();
config.CreateMap<ClientDto, Client>().MapOnlyIfChanged();
});
运行 与解决方案 1 相同的代码,我们得到相同的输出(Comment
是 null
):
[
{
"State": "Detached",
"Name": "TaskNumber",
"Value": "1001246"
},
{
"State": "Detached",
"Name": "Comment",
"Value": null
},
{
"State": "Detached",
"Name": "CrmRegDate",
"Value": "2010-09-15T00:00:00"
}
]
不好.
解决方案 3
我们走另一条路:
- 从数据库中获取实体。
- 使用非泛型
Map
以覆盖数据库中从ClientDto
到Client
的值。 - 调整映射配置以跳过值相等的属性。
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");
}
这个 终于可以工作了 ,但是它有问题 - 它仍然修改 所有 属性。这是输出:
[
{
"State": "Modified",
"Name": "TaskNumber",
"Value": "1001246"
},
{
"State": "Modified",
"Name": "Comment",
"Value": "comment 1"
},
{
"State": "Modified",
"Name": "CrmRegDate",
"Value": "2010-09-15T00:00:00"
}
]
那么,如何让Automapper只更新修改过的属性呢?
您不需要显式调用 context.Update()
。
加载实体时,EF 会记住每个映射的每个原始值 属性。
然后,当您更改 属性 时,EF 会将当前属性与原始属性进行比较,并仅为更改后的属性创建适当的更新 SQL。