使用DTO时进行修补



我正在研究asp.net核心webAPi和EF核心,并希望实现"更新";操作(部分编辑实体)。我搜索了正确的方法来处理这个问题,并发现我应该使用jsonPatch。问题是我通过API只公开DTO,如果我使用jsonPatch,比如:

public AccountDto Patch(int id, [FromBody]JsonPatchDocument<AccountDto> patch)

然后我需要在DTO上应用补丁,如果不创建新的实体,我就无法在模型实体上应用它。

我也读过关于小田的书。Delta,但它仍然不能在asp.net核心上工作,此外,我不认为它有一个用于处理dto的内置解决方案(我发现这个例子可以在Odata for core可用时提供帮助)

所以,就目前而言,我应该使用POST并在查询中发送带有更改属性列表的DTO吗(正如我在这里看到的),或者,还有更优雅的解决方案吗?

谢谢!

现在我看到使用autoMapper我可以只做

CreateMap<JsonPatchDocument<AccountDTO>, JsonPatchDocument<Account>>();
        CreateMap<Operation<AccountDTO>, Operation<Account>>();

它就像一个符咒:)

最终,

我刚刚从JsonPatchDocument中删除了该类型,并看到它可以在没有类型的情况下工作。。。

[HttpPatch("{id}")]
    public AccountDTO Patch(int id, [FromBody]JsonPatchDocument patch)
    {
        return _mapper.Map<AccountDTO>(_accountBlService.EditAccount(id, patch));
    }

然后,在BL层,

public Account EditAccount(int id, JsonPatchDocument patch)
    {
        var account = _context.Accounts.Single(a => a.AccountId == id);
        var uneditablePaths = new List<string> { "/accountId" };
        if (patch.Operations.Any(operation => uneditablePaths.Contains(operation.path)))
        {
            throw new UnauthorizedAccessException();
        }
        patch.ApplyTo(account);            
        return account;
    }

仅将DTO用作端点的"外部契约",检查DTO和补丁文档上的一切是否正常,使用这些操作构建替换操作的字典以执行、使用这些操作(属性、值)构建和扩展对象,使用自定义自动映射器匿名映射器并求解。。

我将导出一些如何在一个更复杂的示例上完成的代码

控制器操作。。。

[HttpPatch("{id}", Name = nameof(PatchDepartment))]
[HttpCacheFactory(0, ViewModelType = typeof(Department))]
public async Task<IActionResult> PatchDepartment(int id, [FromBody] JsonPatchDocument<DepartmentForUpdateDto> patch) // The patch operation is on the dto and not directly the entity to avoid exposing entity implementation details.
{
    if (!ModelState.IsValid) return BadRequest(ModelState);
    var dto = new DepartmentForUpdateDto();
    patch.ApplyTo(dto, ModelState);                                                       // Patch a temporal DepartmentForUpdateDto dto "contract", passing a model state to catch errors like trying to update properties that doesn't exist.
    if (!ModelState.IsValid) return BadRequest(ModelState);
    TryValidateModel(dto);
    if (!ModelState.IsValid) return BadRequest(ModelState);
    var result = await _mediator.Send(new EditDepartmentCommand(id, patch.Operations.Where(o => o.OperationType == OperationType.Replace).ToDictionary(r => r.path, r => r.value))).ConfigureAwait(false);
    if (result.IsFailure && result.Value == StatusCodes.Status400BadRequest) return StatusCode(StatusCodes.Status404NotFound, result.Error);
    if (result.IsFailure && result.Value == StatusCodes.Status404NotFound) return StatusCode(StatusCodes.Status404NotFound, result.Error);
    if (result.IsFailure) return StatusCode(StatusCodes.Status500InternalServerError, result.Error);             // StatusCodes.Status500InternalServerError will be triggered by DbUpdateConcurrencyException.
    return NoContent();
}

Mediator命令和命令处理程序

public sealed class EditDepartmentCommand : IRequest<Result<int>>
{
    public int Id { get; }
    public IDictionary<string, object> Operations { get; }
    public EditDepartmentCommand(int id, IDictionary<string, object> operations) // (*) We avoid coupling this command to a JsonPatchDocument<DepartmentForUpdateDto> "contract" passing a dictionary with replace operations.
    {
        Id = id;
        Operations = operations;
    }
}
public sealed class EditDepartmentHandler : BaseHandler, IRequestHandler<EditDepartmentCommand, Result<int>>
{
    private readonly IUnitOfWork _unitOfWork;
    private readonly IAnonymousMapper _mapper;
    public EditDepartmentHandler(IUnitOfWork unitOfWork, IAnonymousMapper mapper)
    {
        _mapper = mapper;
        _unitOfWork = unitOfWork;
    }
    public async Task<Result<int>> Handle(EditDepartmentCommand command, CancellationToken token)
    {
        using (var repository = _unitOfWork.GetRepository<Department>())
        {
            var department = await repository.FindAsync(command.Id, true, token).ConfigureAwait(false);
            if (department == null) return Result.Fail($"{nameof(EditDepartmentHandler)} failed on edit {nameof(Department)} '{command.Id}'.", StatusCodes.Status404NotFound);   // We could perform a upserting but such operations will require to have guids as primary keys.
            dynamic data = command.Operations.Aggregate(new ExpandoObject() as IDictionary<string, object>, (a, p) => { a.Add(p.Key.Replace("/", ""), p.Value); return a; });    // Use an expando object to build such as and "anonymous" object.
            _mapper.Map(data, department);                                                                                                                                       //  (*) Update entity with expando properties and his projections, using auto mapper Map(source, destination) overload.
            ValidateModel(department, out var results);
            if (results.Count != 0)
                return Result.Fail($"{nameof(EditDepartmentHandler)} failed on edit {nameof(Department)} '{command.Id}' '{results.First().ErrorMessage}'.", StatusCodes.Status400BadRequest);
            var success = await repository.UpdateAsync(department, token: token).ConfigureAwait(false) &&                                                                        // Since the entity has been tracked by the context when was issued FindAsync
                          await _unitOfWork.SaveChangesAsync().ConfigureAwait(false) >= 0;                                                                                       // now any changes projected by auto mapper will be persisted by SaveChangesAsync.
            return success ?
                Result.Ok(StatusCodes.Status204NoContent) :
                Result.Fail<int>($"{nameof(EditDepartmentHandler)} failed on edit {nameof(Department)} '{command.Id}'.");
        }
    }
}
public abstract class BaseHandler
{
    public void ValidateModel(object model, out ICollection<ValidationResult> results)
    {
        results = new List<ValidationResult>();
        Validator.TryValidateObject(model, new ValidationContext(model), results, true);
    }
}

匿名映射器

public interface IAnonymousMapper : IMapper
{
}

public class AnonymousMapper : IAnonymousMapper
{
    private readonly IMapper _mapper = Create();
    private static IMapper Create()
    {
        var config = new MapperConfiguration(cfg =>
        {
            cfg.ValidateInlineMaps = false;
            cfg.CreateMissingTypeMaps = true;
            //cfg.SourceMemberNamingConvention = 
           // cfg.DestinationMemberNamingConvention = new PascalCaseNamingConvention();
        });
        return config.CreateMapper();
    }
    public TDestination Map<TDestination>(object source) => _mapper.Map<TDestination>(source);
    public TDestination Map<TDestination>(object source, Action<IMappingOperationOptions> opts) => _mapper.Map<TDestination>(source, opts);
    public TDestination Map<TSource, TDestination>(TSource source) => _mapper.Map<TSource, TDestination>(source);
    public TDestination Map<TSource, TDestination>(TSource source, Action<IMappingOperationOptions<TSource, TDestination>> opts) => _mapper.Map(source, opts);
    public TDestination Map<TSource, TDestination>(TSource source, TDestination destination) => _mapper.Map(source, destination);
    public TDestination Map<TSource, TDestination>(TSource source, TDestination destination, Action<IMappingOperationOptions<TSource, TDestination>> opts) => _mapper.Map(source, destination, opts);
    public object Map(object source, Type sourceType, Type destinationType) => _mapper.Map(source, sourceType, destinationType);
    public object Map(object source, Type sourceType, Type destinationType, Action<IMappingOperationOptions> opts) => _mapper.Map(source, sourceType, destinationType, opts);
    public object Map(object source, object destination, Type sourceType, Type destinationType) => _mapper.Map(source, destination, sourceType, destinationType);
    public object Map(object source, object destination, Type sourceType, Type destinationType, Action<IMappingOperationOptions> opts) => _mapper.Map(source, destination, sourceType, destinationType);
    public IQueryable<TDestination> ProjectTo<TDestination>(IQueryable source, object parameters = null, params Expression<Func<TDestination, object>>[] membersToExpand) => _mapper.ProjectTo(source, parameters, membersToExpand);
    public IQueryable<TDestination> ProjectTo<TDestination>(IQueryable source, IDictionary<string, object> parameters, params string[] membersToExpand) => _mapper.ProjectTo<TDestination>(source, parameters, membersToExpand);
    public IConfigurationProvider ConfigurationProvider => _mapper.ConfigurationProvider;
    public Func<Type, object> ServiceCtor => _mapper.ServiceCtor;
}

这适用于任何<T>

CreateMap(typeof(JsonPatchDocument<>), typeof(JsonPatchDocument<>));
CreateMap(typeof(Operation<>), typeof(Operation<>));

最新更新