Closed ahedreville closed 5 years ago
Hi,
could I suggest another approach? When patching a resource I tend to:
1) get the matching entity ("originalEntity") from your repository. This can be used to check if it exists in the first place. If it doesn't, return 404.
2) map that entity to a DTO ("mappedDto") with AutoMapper.
3) apply the patch document to that mappedDto instance.
4) validate, and if validation checks out, map the patched DTO back into the entity you got in step 1 (with AutoMapper, eg: _mapper.Map(mappedDto, originalEntity).
That should solve your issue if I'm not mistaken.
KR, Kevin
Hi Kevin, thanks a lot for your reply, deeply appreciated. Indeed your proposed path is the right path to follow. I follow it to simplify my controllers, and it works like a charm.
I had build a custom validation attribute to validate the incoming patch document before the action controller get started.
[AttributeUsage(AttributeTargets.Parameter)]
public sealed class ValidJsonPatchBindingAttribute : ValidationAttribute
{
private const string DefaultErrorMessage = "{0}";
public ValidJsonPatchBindingAttribute()
: base(DefaultErrorMessage)
{
}
public override string FormatErrorMessage(string errorMessage) => string.Format(CultureInfo.InvariantCulture, ErrorMessageString, errorMessage);
protected override ValidationResult IsValid(object patch, ValidationContext context)
{
if (patch == null) return ValidationResult.Success;
string logErrorMessage = null;
Action<JsonPatchError> logErrorAction = e => { logErrorMessage = e.ErrorMessage; };
var dtoType = patch.GetType().GetGenericArguments().First(); // Get the type behind the generic JsonPatchDocument<T>.
var dto = Activator.CreateInstance(dtoType); // Create a new object of the same T underlying type.
var applyTo = patch.GetType().GetMethod("ApplyTo", new[] { dtoType, logErrorAction.GetType() }); // Get the ApplyTo(TModel objectToApplyTo, Action<JsonPatchError> logErrorAction) method from JsonPatchDocument<T>
applyTo.Invoke(patch, new[] { dto, logErrorAction }); // Patch a temporal dto "contract" to catch errors like trying to update properties that doesn't exist.
if (logErrorMessage != null) return new ValidationResult(FormatErrorMessage(logErrorMessage)); // If error found on the pach document itself return.
var validator = context.GetService<IModelValidator>(); // Get the model validator to validate the patched mmodel.
return validator.ValidateModel(out var results, dto)
?
ValidationResult.Success :
new ValidationResult(FormatErrorMessage(results.First().ErrorMessage)); // If error found on the model return it.
}
}
Using on the controller action as below gets my controllers simplified.
[HttpPatch("{id}", Name = nameof(PatchDepartment))]
public async Task<IActionResult> PatchDepartment([Range(1, int.MaxValue)] int id, [FromBody, ValidJsonPatchBinding] JsonPatchDocument<DepartmentForUpdateDto> patch)
The validation attribute is using the following registered ModelValidator that handles System.ComponentModel.DataAnnotations validations and FluentValidation validations too.
public class ModelValidator : IModelValidator
{
private readonly IServiceProvider _provider;
public ModelValidator(IServiceProvider provider)
{
_provider = provider;
}
public bool ValidateModel(out List<ValidationResult> results, params object[] models)
{
results = new List<ValidationResult>();
foreach (var model in models)
{
if (!Validator.TryValidateObject(model, new ValidationContext(model), results, true)) return false; // If the data annotations validation process founds an error ends processing.
var genericType = typeof(IValidator<>).MakeGenericType(model.GetType());
if (!(_provider.GetService(genericType) is IValidator validator)) continue; // Gets a valid fluent validator registered to process if there is any.
if (!(validator.Validate(model) is FluentValidation.Results.ValidationResult result) || result.IsValid) continue;
results.AddRange(result.Errors.Select(e => new ValidationResult(e.ErrorMessage, new[] { e.PropertyName }))); // If the fluent validation process founds and error ends processing.
return false;
}
return true; // All models nas been validated successfully.
}
}
public interface IModelValidator
{
bool ValidateModel(out List<ValidationResult> results, params object[] models);
}
I Just share this code just in case meany reading this thread would benefit.
Thanks for that! :)
Hi Kevin, let me introduce my case. I have a DepartmentForUpdateDto that we could think on it as a "contract" for the API endpoint of my controller.
I am using it like this..
Ok. but my intention is to get "finally" applied the operations to the entity Department which is different type
Moreover, since has protected properties can not be updated.
For sure this are not issues, but having those additional overload functionalities of: 1) able to apply the operations to a different type, and 2) able to set value on protected properties will be very helpful
BR PS: The path I am following to workaround those limitations is
a) get the operations as dictionary
patch.Operations.Where(o => o.OperationType == OperationType.Replace).ToDictionary(r => r.path, r => r.value)
b) build and expando object with the operations
dynamic data = command.Operations.Aggregate(new ExpandoObject() as IDictionary<string, object>, (a, p) => { a.Add(p.Key.Replace("/", ""), p.Value); return a; });
c) Use automaper to map the properties to the entity