14

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

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

然后我需要在 DTO 上应用补丁,并且我不能将它应用到模型实体上,而无需创建新实体。

我还阅读了有关 Odata.Delta 的信息,但它仍然无法在 asp.net 核心上运行,此外 - 我认为它没有用于使用 dto 的内置解决方案(我发现这个示例可以帮助核心的 Odata 将能得到的)

所以,现在 - 我应该使用 POST 并在查询中发送带有更改属性列表的 DTO(正如我在这里看到的),还是 - 有更优雅的解决方案?

谢谢!

4

4 回答 4

9

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

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

它就像一个魅力:)

于 2016-07-31T19:02:12.363 回答
4

最终,

我只是从 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;
    }
于 2016-07-23T21:55:12.683 回答
4

仅将 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();
}

MediatR 命令和命令处理程序

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;
}
于 2019-02-16T22:39:07.613 回答
1

这适用于任何<T>

CreateMap(typeof(JsonPatchDocument<>), typeof(JsonPatchDocument<>));
CreateMap(typeof(Operation<>), typeof(Operation<>));
于 2019-08-20T15:36:36.210 回答