Edit/Update EF Core 中的集合:来自 DTO 还是存储库?

Edit/Update collections in EF Core: from DTO or repository?

我有以下 类:CompanyCountryPerson

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) 类:CompanyDTOCountryDTOPersonDTO

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)));