Edit/Update EF Core 中的集合:来自 DTO 还是存储库?
Edit/Update collections in EF Core: from DTO or repository?
我有以下 类:Company
、Country
、Person
。
class Country
{
public int Id { get; set; }
public string Name { get; set; }
public Person President { get; set; }
ICollection<Company> Companies { get; set; }
ICollection<Person> People { get; set; }
}
class Company
{
public int Id { get; set; }
public string Name { get; set; }
public string Code { get; set; }
public Country Country { get; set; }
ICollection<Person> People { get; set; }
}
class Person
{
public int Id { get; set; }
public string Name { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
我有以下 DTO(ViewModel) 类:CompanyDTO
、CountryDTO
、PersonDTO
:
class CountryDTO
{
public int Id { get; set; }
public string Name { get; set; }
public string PresidentName { get; set; }
int[] CompaniesIds { get; set; }
int[] PeopleIds { get; set; }
string[] PeopleNames { get; set; }
}
class CompanyDTO
{
public int Id { get; set; }
public string Name { get; set; }
public string Code { get; set; }
public string CountryName { get; set; }
int[] PeopleIds { get; set; }
string[] PeopleNames { get; set; }
}
class PersonDTO
{
public int Id { get; set; }
public string Name { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
我用AutoMapper,所以我用
CreateMap<Company, CompanyDTO>()
.ForMember(p => p.CountryName, o => o.MapFrom(p => p.Country.Name))
.ForMember(p => p.PeopleIds, o => o.MapFrom(p => p.People.Select(s => s.Id).ToArray()))
.ForMember(p => p.PeopleNames, o => o.MapFrom(p => p.People.Select(s => s.Name).ToArray()))
.ReverseMap();
我在 EntityFramework(核心)存储库“正常”(业务)类 中使用,例如 Country
;我在 Views 中仅使用 DTO 作为模型,例如 CountryDTO
.
更新现有集合时出现问题。
不清楚如何创建将被更新的业务对象:来自存储库或来自 DTO (ViewModel) .如果我们从 DTO 获取,EF 将(尝试)复制子集合。确定。
如果我们从存储库中获取,我们应该逐个字段手动更新所有对象属性,因此这样的映射根本没有用。
/// CompaniesController
/// Saving a Company being in edit mode
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, CompanyDTO companyDto)
{
if (id != companyDto.Id) { return NotFound(); }
if (ModelState.IsValid) {
try {
//
// 1) How to get the edited country
// a) from repository or
// b) from the mapper?
//
var spec = new CompanyWithPeopleSpecification(id);
var company = await _repository.SingleOrDefaultAsync(spec);
// or
var company = _mapper.Map<Company>(companyDto);
//
// 2) How to update the changed People's collection?
//
//
// if we got from _repository, we should
//
// a) update all value fields from DTO
company.Code = companyDto.Code;
company.Name = companyDto.Name;
// b) filter the collection items to remove and to add
var existingPeopleIds = company.People.Select(x => x.Id).ToList();
var peopleIdsToRemove = existingPeopleIds.Except(companyDto.PeopleIds).ToArray();
var peopleIdsToAdd = companyDto.PeopleIds.Except(existingPeopleIds).ToArray();
foreach (var personId in peopleIdsToRemove)
company.People.Remove(company.People.Single(s => s.Id == personId));
var specPeopleToAdd = new PeopleFromIdsSpecification(peopleIdsToAdd);
var peopleToAdd = await _repository.ListAsync(specPeopleToAdd);
foreach (var person in peopleToAdd)
company.People.Add(person);
//
// if we got from _mapper, we should
//
// a) update all object fields from repository
company.Country = _repository.GetById<Country>(companyDto.CountryId)
// b) the added to collection exising in DB objects will be duplicated
foreach(var personId in companyDto.PeopleIds)
company.People.Add(_repository.GetById<Person>(personId));
await _repository.UpdateAsync(company);
}
catch (DbUpdateConcurrencyException) {
if (!CompanyExists(companyDto.Id)) {
return NotFound();
}
else { throw; }
}
return RedirectToAction(nameof(Index));
}
ViewData["Countries"] = new SelectList(_repository.List<Country>(), "Id", "Name", companyDto.CountryId);
ViewData["AvailablePeople"] = new MultiSelectList(_repository.List<Person>(),
"Id", "Name", companyDto.PeopleIds);
return View(companyDto);
}
来自存储库。
对于问题的第一部分,Automapper 有一个 Map
调用,可以将值复制到现有实体。
var company = await _repository.SingleAsync(spec);
mapper.Map(companyDto, company);
ReverseMap
应该负责将详细信息返回到实体中,尽管它不会做 add/remove 关联实体之类的事情。为此,您对 Add/Remove 的 ID 的实现与我使用的方法一致。
在更新数据和关联时,通常建议尽可能以原子方式拆分操作;例如,要有操作来添加或删除国家关联,而不是让客户端更改整个公司对象图,然后通过“UpdateCompany”之类的方法提交更改。全部或单一更新操作可能涉及相当多的工作,因此我将其保留在绝对需要的地方。
根据 Steve 的建议,我构建了一个扩展方法,可以将存储库中的任何现有集合更新为 id 数组:
public static class BusinessExtensions
{
/// <summary>
/// Updates a business objects collection (usually from repository)
/// to correspond to the given list of ID's (usually updated in the View)
/// </summary>
/// <typeparam name="T">Any entity having an Id (IdEntity)</typeparam>
/// <param name="collectionToUpdate">reposotory collection to be updated</param>
/// <param name="updatedIds">list of new id's to syncronise with the repository collection</param>
/// <param name="Spec">function transforming a list of ids into a list of business objects from repository</param>
public static void SyncCollection<T>(this ICollection<T> collectionToUpdate, int[] updatedIds, Func<int[], IEnumerable<T>> Spec)
where T : IdEntity
{
var existingEntityIds = collectionToUpdate.Select(x => x.Id).ToList();
var entityIdsToRemove = existingEntityIds.Except(updatedIds).ToArray();
var entityIdsToAdd = updatedIds.Except(existingEntityIds).ToArray();
var entitiesToRemove = Spec(entityIdsToRemove);
var entitiesToAdd = Spec(entityIdsToAdd);
foreach (var entity in entitiesToRemove)
collectionToUpdate.Remove(entity);
foreach (var entity in entitiesToAdd)
collectionToUpdate.Add(entity);
}
}
控制器中的用法
var spec = new CompanyWithPeopleSpecification(id);
var company = await _repository.SingleOrDefaultAsync(spec);
_mapper.Map(comapnyDto, company;
company.People.SyncCollection(companyDto.PeopleIds,
ids => _repository.List(new PeopleFromIdsSpecification(ids)));
我有以下 类:Company
、Country
、Person
。
class Country
{
public int Id { get; set; }
public string Name { get; set; }
public Person President { get; set; }
ICollection<Company> Companies { get; set; }
ICollection<Person> People { get; set; }
}
class Company
{
public int Id { get; set; }
public string Name { get; set; }
public string Code { get; set; }
public Country Country { get; set; }
ICollection<Person> People { get; set; }
}
class Person
{
public int Id { get; set; }
public string Name { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
我有以下 DTO(ViewModel) 类:CompanyDTO
、CountryDTO
、PersonDTO
:
class CountryDTO
{
public int Id { get; set; }
public string Name { get; set; }
public string PresidentName { get; set; }
int[] CompaniesIds { get; set; }
int[] PeopleIds { get; set; }
string[] PeopleNames { get; set; }
}
class CompanyDTO
{
public int Id { get; set; }
public string Name { get; set; }
public string Code { get; set; }
public string CountryName { get; set; }
int[] PeopleIds { get; set; }
string[] PeopleNames { get; set; }
}
class PersonDTO
{
public int Id { get; set; }
public string Name { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
我用AutoMapper,所以我用
CreateMap<Company, CompanyDTO>()
.ForMember(p => p.CountryName, o => o.MapFrom(p => p.Country.Name))
.ForMember(p => p.PeopleIds, o => o.MapFrom(p => p.People.Select(s => s.Id).ToArray()))
.ForMember(p => p.PeopleNames, o => o.MapFrom(p => p.People.Select(s => s.Name).ToArray()))
.ReverseMap();
我在 EntityFramework(核心)存储库“正常”(业务)类 中使用,例如 Country
;我在 Views 中仅使用 DTO 作为模型,例如 CountryDTO
.
更新现有集合时出现问题。
不清楚如何创建将被更新的业务对象:来自存储库或来自 DTO (ViewModel) .如果我们从 DTO 获取,EF 将(尝试)复制子集合。确定。
如果我们从存储库中获取,我们应该逐个字段手动更新所有对象属性,因此这样的映射根本没有用。
/// CompaniesController
/// Saving a Company being in edit mode
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, CompanyDTO companyDto)
{
if (id != companyDto.Id) { return NotFound(); }
if (ModelState.IsValid) {
try {
//
// 1) How to get the edited country
// a) from repository or
// b) from the mapper?
//
var spec = new CompanyWithPeopleSpecification(id);
var company = await _repository.SingleOrDefaultAsync(spec);
// or
var company = _mapper.Map<Company>(companyDto);
//
// 2) How to update the changed People's collection?
//
//
// if we got from _repository, we should
//
// a) update all value fields from DTO
company.Code = companyDto.Code;
company.Name = companyDto.Name;
// b) filter the collection items to remove and to add
var existingPeopleIds = company.People.Select(x => x.Id).ToList();
var peopleIdsToRemove = existingPeopleIds.Except(companyDto.PeopleIds).ToArray();
var peopleIdsToAdd = companyDto.PeopleIds.Except(existingPeopleIds).ToArray();
foreach (var personId in peopleIdsToRemove)
company.People.Remove(company.People.Single(s => s.Id == personId));
var specPeopleToAdd = new PeopleFromIdsSpecification(peopleIdsToAdd);
var peopleToAdd = await _repository.ListAsync(specPeopleToAdd);
foreach (var person in peopleToAdd)
company.People.Add(person);
//
// if we got from _mapper, we should
//
// a) update all object fields from repository
company.Country = _repository.GetById<Country>(companyDto.CountryId)
// b) the added to collection exising in DB objects will be duplicated
foreach(var personId in companyDto.PeopleIds)
company.People.Add(_repository.GetById<Person>(personId));
await _repository.UpdateAsync(company);
}
catch (DbUpdateConcurrencyException) {
if (!CompanyExists(companyDto.Id)) {
return NotFound();
}
else { throw; }
}
return RedirectToAction(nameof(Index));
}
ViewData["Countries"] = new SelectList(_repository.List<Country>(), "Id", "Name", companyDto.CountryId);
ViewData["AvailablePeople"] = new MultiSelectList(_repository.List<Person>(),
"Id", "Name", companyDto.PeopleIds);
return View(companyDto);
}
来自存储库。
对于问题的第一部分,Automapper 有一个 Map
调用,可以将值复制到现有实体。
var company = await _repository.SingleAsync(spec);
mapper.Map(companyDto, company);
ReverseMap
应该负责将详细信息返回到实体中,尽管它不会做 add/remove 关联实体之类的事情。为此,您对 Add/Remove 的 ID 的实现与我使用的方法一致。
在更新数据和关联时,通常建议尽可能以原子方式拆分操作;例如,要有操作来添加或删除国家关联,而不是让客户端更改整个公司对象图,然后通过“UpdateCompany”之类的方法提交更改。全部或单一更新操作可能涉及相当多的工作,因此我将其保留在绝对需要的地方。
根据 Steve 的建议,我构建了一个扩展方法,可以将存储库中的任何现有集合更新为 id 数组:
public static class BusinessExtensions
{
/// <summary>
/// Updates a business objects collection (usually from repository)
/// to correspond to the given list of ID's (usually updated in the View)
/// </summary>
/// <typeparam name="T">Any entity having an Id (IdEntity)</typeparam>
/// <param name="collectionToUpdate">reposotory collection to be updated</param>
/// <param name="updatedIds">list of new id's to syncronise with the repository collection</param>
/// <param name="Spec">function transforming a list of ids into a list of business objects from repository</param>
public static void SyncCollection<T>(this ICollection<T> collectionToUpdate, int[] updatedIds, Func<int[], IEnumerable<T>> Spec)
where T : IdEntity
{
var existingEntityIds = collectionToUpdate.Select(x => x.Id).ToList();
var entityIdsToRemove = existingEntityIds.Except(updatedIds).ToArray();
var entityIdsToAdd = updatedIds.Except(existingEntityIds).ToArray();
var entitiesToRemove = Spec(entityIdsToRemove);
var entitiesToAdd = Spec(entityIdsToAdd);
foreach (var entity in entitiesToRemove)
collectionToUpdate.Remove(entity);
foreach (var entity in entitiesToAdd)
collectionToUpdate.Add(entity);
}
}
控制器中的用法
var spec = new CompanyWithPeopleSpecification(id);
var company = await _repository.SingleOrDefaultAsync(spec);
_mapper.Map(comapnyDto, company;
company.People.SyncCollection(companyDto.PeopleIds,
ids => _repository.List(new PeopleFromIdsSpecification(ids)));