我正在研究asp.net核心webAPi和EF核心,并希望实现“更新”操作(部分编辑实体)。 我搜索了正确的方法来处理它,并看到我应该使用jsonPatch。问题是我通过我的API公开DTO,如果我使用jsonPatch:
public AccountDto Patch(int id, [FromBody]JsonPatchDocument<AccountDto> patch)
然后我需要在DTO上应用补丁,我不能在模型实体上应用它,而不创建新的实体。
我也读过Odata.Delta,但它仍然无法在asp.net核心上运行,而且 - 我认为它没有内置的解决方案来处理dto(我发现this example可以当Odata for core可用时提供帮助)
所以,现在 - 我应该使用POST并在查询中发送带有已更改属性列表的DTO(我看到here),或者 - 还有更优雅的解决方案吗?
谢谢!
答案 0 :(得分:4)
现在我看到使用autoMapper我可以做到
CreateMap<JsonPatchDocument<AccountDTO>, JsonPatchDocument<Account>>();
CreateMap<Operation<AccountDTO>, Operation<Account>>();
它就像一个魅力:)
答案 1 :(得分:1)
最终,
我只是从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;
}
答案 2 :(得分:0)
仅将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命令和CommandHandler
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;
}
答案 3 :(得分:0)
这非常适合任何<T>
CreateMap(typeof(JsonPatchDocument<>), typeof(JsonPatchDocument<>));
CreateMap(typeof(Operation<>), typeof(Operation<>));