public class MoveProductParam { public ProductId {get; set; } public CategoryId {get; set; } } //... if(!dbContext.Products.Any(x => x.Id == par.ProductId)) return BadRequest("Product not found"); if(!dbContext.Categories.Any(x => x.Id == par.CategoryId )) return BadRequest("Category not found");
public class MoveProductParam { [EntityId(typeof(Product))] public ProductId {get; set; } [EntityId(typeof(Category))] public CategoryId {get; set; } }
IsValid
method of the ValidationAttribute
overloaded. We need a second overload to reach the IOC container. For simplicity, I did not introduce an additional interface and just try to get a delegate that by type and Id will return a boolean value: is there such an entity or not. public class EntityIdAttribute: ValidationAttribute { private readonly Type _entityType; public EntityIdAttribute(Type entityType) { _entityType = entityType; } protected override ValidationResult IsValid(object value, ValidationContext validationContext) { var checker = validationContext .GetService(typeof(Func<Type, object, bool>)) as Func<Type, object, bool> ?? throw new InvalidOperationException( "You must register Func<Type, object, bool>"); return checker(_entityType, value) ? ValidationResult.Success : new ValidationResult($"Entity with id {value} is not found"); } }
services.AddScoped<Func<Type, object, bool>>(x => { bool Func(Type t, object o) { var dbContext = x.GetService<DbContext>(); return dbContext.Find(t, o) != null; }; return Func; });
public class MoveProductParam { [EntityId(typeof(Product))] public ProductId {get; set; } [EntityId(typeof(Category))] public CategoryId {get; set; } } [Validate] public class ProductController: Controller { public IActionResult Move(MoveProductParam par) { // . // , ProductId CategoryId // } }
NRE
or ArgumentException
if you use defensive programming. This error will be very easy to diagnose and fix. If the MoveProductParam
used in more than one place, the validation will apply wherever this parameter is used. You will not forget to add verification again.ModelBinder
. So that you can: public class MoveProductParam { [ModelBinder(typeof(EntityModelBinder))] public Product Product{get; set; } [ModelBinder(typeof(EntityModelBinder))] public Category Category{get; set; } }
Find
method cannot pass a string from the query. You need to know the type of primary key. Most likely this information is available from the context of the database, but I did not dig so deeply, so I pulled out the property type Id
. That was quicker added TypeAccessor
from FastMember
. public class EntityModelBinder: IModelBinder { private readonly Func<Type, object, object> _getter; public EntityModelBinder(Func<Type, object, object> getter) { _getter = getter; } public Task BindModelAsync(ModelBindingContext bindingContext) { var value = bindingContext.ActionContext .HttpContext .Request .Query[bindingContext.ModelName] .FirstOrDefault(); if (value == null) { bindingContext.ModelState .AddModelError(bindingContext.ModelName, "Id for \"bindingContext.ModelName\" is null"); return Task.CompletedTask; } try { var typeAccessor = TypeAccessor .Create(bindingContext.ModelType); // Id . // EF, PK // var id = Convert.ChangeType(value, typeAccessor.GetMembers() .First(y => y.Name == "Id") .Type); var result = _getter(bindingContext.ModelType, id); bindingContext.Result = ModelBindingResult.Success(result); } catch (Exception e) { bindingContext.ModelState.AddModelError( bindingContext.ModelName, e.Message); } return Task.CompletedTask; } // . // services.AddScoped<Func<Type, object, object>>(x => { object Func(Type t, object o) { var typeAccessor = TypeAccessor.Create(t); var dbContext = x.GetService<DemoContext>(); return dbContext.Find(t, o); }; return Func; });
IModelBinderProvider
to assign this binder to all Entity from the context.Source: https://habr.com/ru/post/346850/
All Articles