Json.net and Web Api Conditional Validation on model properties - asp.net-core

I know I can validate a model object by implementing IValidateObject but unfortunately this doesn't give you the nice errors that state the line and the type that has failed json validation when converting the json request to the object when your controller is decorated with the FromBody attribute.
My question is, is it possible to validate an objects properties conditionally in an ApiController and get the nice error format you get for free? As an arbitrary example, say the Account class below needed to validate Roles had more than one item if IsAdmin was true?
public class Account
{
[JsonRequired]
public bool IsAdmin { get; set; }
public IList<string> Roles { get; set; }
}

is it possible to validate an objects properties conditionally in an ApiController and get the nice error format you get for free? As an arbitrary example, say the Account class below needed to validate Roles had more than one item if IsAdmin was true?
Try this:
1.Controller(be sure to add [ApiController] otherwise you need to judge the ModelState):
[Route("api/[controller]")]
[ApiController]
public class ValuesController : Controller
{
[HttpPost]
public IActionResult Post([FromBody]Account account)
{
return Ok(account);
}
}
2.Model:
public class Account: IValidatableObject
{
[JsonRequired]
public bool IsAdmin { get; set; }
public IList<string> Roles { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
var count = 0;
if (IsAdmin == true)
{
if (Roles != null)
{
foreach (var role in Roles)
{
count++;
}
}
}
if (count <= 1)
{
yield return new ValidationResult(
$"Roles had more than one item", new[] { "Roles" });
}
}
}

Related

How do you centralise ASP.NET Core Web API attribute validation on multiple DTOs with similar properties?

Is there a way to centralise the model validation of the same property name across multiple DTOs?
For example, if I have the following classes to be used as the request body in a Web API action.
public class RegisterRequest
{
[Required]
[EmailAddress]
public string EmailAddress { get; set; } = null!;
[Required]
[MinLength(8)]
[RegularExpression(UserSettings.PasswordRegex)]
public string Password { get; set; } = null!;
[Required]
[MaxLength(100)]
public string DisplayName { get; set; } = null!;
}
public class UserProfileRequest
{
[Required]
public int UserId { get; set; }
[Required]
[MaxLength(100)]
public string DisplayName { get; set; } = null!;
[Range(3, 3)]
public string? CCN3 { get; set; }
}
Can I centralise the attribute validation on DisplayName, duplicating the attributes goes against single responsibility principle. I believe I could achieve the centralised validation using an IFilterFactory and dropping the usage of attributes.
I opted to use a custom ActionFilterAttribute to achieve centralisation of the validation. The example below is for validating the country code (CCN3).
CountryCodeValidationAttribute.cs - custom attribute to be applied to properties (contains no logic)
[AttributeUsage(AttributeTargets.Property)]
public class CountryCodeValidationAttribute : Attribute
{
}
CountryCodeValidationActionFilter.cs - custom action filter that supports dependency injection and looks for the custom attribute on the properties. In my case I'm returning the standard invalid model bad request response.
public class CountryCodeValidationActionFilter : ActionFilterAttribute
{
private readonly ICountryService countryService;
private readonly IOptions<ApiBehaviorOptions> apiBehaviorOptions;
public CountryCodeValidationActionFilter(
ICountryService countryService,
IOptions<ApiBehaviorOptions> apiBehaviorOptions)
{
this.countryService = countryService;
this.apiBehaviorOptions = apiBehaviorOptions;
}
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
var actionArguments = context.ActionArguments;
foreach (var actionArgument in actionArguments)
{
if (actionArgument.Value == null) continue;
var propertiesWithAttributes = actionArgument.Value
.GetType()
.GetProperties()
.Where(x => x.GetCustomAttributes(true).Any(y => y.GetType() == typeof(CountryCodeValidationAttribute)))
.ToList();
foreach (var property in propertiesWithAttributes)
{
var value = property.GetValue(actionArgument.Value)?.ToString();
if (value != null && await countryService.GetCountryAsync(value) != null) await next();
else
{
context.ModelState.AddModelError(property.Name, "Must be a valid country code");
context.Result = apiBehaviorOptions.Value.InvalidModelStateResponseFactory(context);
}
}
}
await base.OnActionExecutionAsync(context, next);
}
}
Program.cs - register the custom action filter.
builder.Services.AddMvc(options =>
{
options.Filters.Add(typeof(CountryCodeValidationActionFilter));
});
UserProfile.cs - apply the [CountryCodeValidation] attribute to the CountryCode property.
public class UserProfile
{
[Required]
[MaxLength(100)]
public string DisplayName { get; set; } = null!;
[CountryCodeValidation]
public string? CountryCode { get; set; }
}
I can take this same approach and apply it to the DisplayName property to create a centralised validation for it 👍.

ASP.NET Core Webapi (3.x) Custom ModelBinder Always Returns Empty Object

I've been attempting to create a custom model binder and am running into an issue where the object after going through the binder is ALWAYS empty.
I have a good reason for using a custom model binder. For the purposes of this question, assume I HAVE to use a custom model binder. In every case where i've used a custom binder provider with either stock or custom modelbinders I've run into this issue, so I've dumbed this down a LOT to demonstrate, but it happens in this specific example as well, and I really need to know why.
I have a simple controller action and DTO class:
[HttpPost]
[Route("{id}")]
public async Task<ActionResult<QueryServicesDto>> UpdateQueryService(int id,
[FromBody] QueryServicesDtoLight dto)
{
}
public class QueryServicesDtoLight
{
public long QueryServicesId { get; set; }
public DateTime? CreationDate { get; set; }
public long? ModifiedDate { get; set; }
public long? PropagationDate { get; set; }
public int? CabinetListNumber { get; set; }
public string GameCode { get; set; }
public string Status { get; set; }
}
Can anyone tell me why when posting valid JSON to this action with no custom binder providert I get a DTO with the proper values, but if I inject the custom modelBinderProvider below I get a newed up model with no values?
public class QueryServiceModelBinderBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context.Metadata.ModelType== typeof(QueryServicesDtoLight))
{
var propertyBinders = new Dictionary<ModelMetadata, IModelBinder>();
for (var i = 0; i < context.Metadata.Properties.Count; i++)
{
ModelMetadata theProp = context.Metadata.Properties[i];
var binder = context.CreateBinder(theProp);
propertyBinders.Add(theProp, binder);
}
var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>();
return new ComplexTypeModelBinder(propertyBinders, loggerFactory);
}
else
{
return null;
}
}
}
Binder provider is added like this::
services.AddControllersWithViews(o=>o.ModelBinderProviders.Insert(0, new QueryServiceModelBinderBinderProvider())).AddNewtonsoftJson();
Sample JSON Data
{"queryServicesId":14,"creationDate":"2021-03-08T17:06:36.053","modifiedDate":16176433093000000,"propagationDate":0,"cabinetListNumber":996,"gameCode":"PGA2006","status":"AC"}

Mapping Id to Model for controller API

I'm using asp.net core on a project. (I'm fairly new to it)
I have a User Model. the code below is a simplified version:
public class User
{
public int id { get; set; }
// attribute declaration
public ICollection<User> friends { get; set; }
}
I'm using automapper service to map my api to this Model:
public class UserResource
{
public UserResource()
{
this.friendsId = new List<int>();
}
public int id { get; set; }
// attribute declaration
public ICollection<int> friendsId { get; set; }
}
consider a post request to UserController with the following body:
{
"id" : 1
"friendsId": [2,3,4],
}
I want to map integers in friendsId to id of each user in friends collection. but I can't figure out what to do. here's what I've got:
CreateMap<UserResource,User>()
.ForMember(u => u.friends,opt => opt.MapFrom(????);
is this the right approach? if so how should I implement it?
or should I change my database model to this:
public class User
{
public int id { get; set; }
// attribute declaration
public ICollection<int> friendsId { get; set; }
}
Thank you in advance.
You'll need to implement a custom value resolver. These can be injected into, so you can access things like your context inside:
public class FriendsResolver : IValueResolver<UserResource, User, ICollection<User>>
{
private readonly ApplicationDbContext _context;
public FriendsResolver(ApplicationDbContext context)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
}
public ICollection<User> Resolve(UserResource source, User destination, ICollection<User> destMember, ResolutionContext context)
{
var existingFriendIds = destMember.Select(x => x.Id);
var newFriendIds = source.friendsId.Except(existingFriendIds);
var removedFriendIds = existingFriendIds.Except(source.Friends);
destMember.RemoveAll(x => removedFriendIds.Contains(x.Id);
destMember.AddRange(_context.Users.Where(x => newFriendIds.Contains(x.Id).ToList());
return destMember;
}
}
Not sure if that's going to actually work as-is, as I just threw it together here, but it should be enough to get your going. The general idea is that you inject whatever you need into the value resolver and then use that to create the actual stuff you need to return. In this case, that means querying your context for the User entities with those ids. Then, in your CreateMap:
.ForMember(dest => dest.friends, opts => opts.ResolveUsing<FriendsResolver>());
This only covers one side of the relationship, though, so if you need to map the other way, you may need a custom resolver for that path as well. Here, I don't think you actually do. You should be able to just get by with:
.ForMember(dest => dest.friendsId, opts => opts.MapFrom(src => src.friends.Select(x => x.Id));
This would help
CreateMap<UserResource,User>()
.ForMember(u => u.friends,opt => opt.MapFrom(t => new User {FriendsId = t.friendsId);
public class User
{
...
public ICollection<User> friends { get; set; }
}
Where friends is ICollection<User> whereas UserResource class has ICollection<int>. There is type mismatch here. You need to map ICollection to ICollection that is why I casted new User ...

Create a Parent with existing children in EntityFramework core

I am building a Web API and have two models: Task and Feature:
public class Feature
{
[Key]
public long FeatureId { get; set; }
public string Analyst_comment { get; set; }
public virtual ICollection<User_Task> Tasks { get; set; }
public Feature()
{
}
}
public class User_Task
{
[Key]
public long TaskId { get; set; }
public string What { get; set; }
[ForeignKey("FeatureId")]
public long? FeatureId { get; set; }
public User_Task()
{
}
}
I create Tasks first and then create a Feature that combines few of them. Task creation is successful, however while creating a Feature with existing Tasks, my controller throws an error saying the task already exists:
My FeatureController has following method:
//Create
[HttpPost]
public IActionResult Create([FromBody] Feature item)
{
if (item == null)
{
return BadRequest();
}
** It basically expects that I am creating a Feature with brand new tasks, so I guess I will need some logic here to tell EF Core that incoming tasks with this feature already exist **
_featureRepository.Add(item);
return CreatedAtRoute("GetFeature", new { id = item.FeatureId }, item);
}
How to tell EF core that incoming Feature has Tasks that already exist and it just needs to update the references instead of creating new ones?
My context:
public class WebAPIDataContext : DbContext
{
public WebAPIDataContext(DbContextOptions<WebAPIDataContext> options)
: base(options)
{
}
public DbSet<User_Task> User_Tasks { get; set; }
public DbSet<Feature> Features { get; set; }
}
And repo:
public void Add(Feature item)
{
_context.Features.Add(item);
_context.SaveChanges();
}
When calling Add on a DBSet with a model that was not loaded from EF, it thinks it is untracked and will always assume it is new.
Instead, you need to load the existing record from the dbcontext and map the properties from the data passed into the API to the existing record. Typically that is a manual map from parameter object to domain. Then if you return an object back, you would map that new domain object to a DTO. You can use services like AutoMapper to map the domain to a DTO. When you're done mapping, you only need to call SaveChanges.
Generally speaking, loading the record and mapping the fields is a good thing for the security of your API. You wouldn't want to assume that the passed in data is pristine and honest. When you give the calling code access to all the properties of the entity, you may not be expecting them to change all the fields, and some of those fields could be sensitive.

User registration and authentication

I develop a web application. It has a three-tier architecture (data access layer, a business logic layer and the presentation layer). A data access layer is implemented with a NHibernate ORM (S#arp Architecture). I have the following table:
public partial class Role {
public Role()
{
this.Users = new Iesi.Collections.Generic.HashedSet<User>();
}
public virtual long Id
{
get;
set;
}
public virtual string Name
{
get;
set;
}
public virtual Iesi.Collections.Generic.ISet<User> Users
{
get;
set;
}
}
public partial class User {
public User()
{
this.Drivers = new Iesi.Collections.Generic.HashedSet<Driver>();
this.UserPhotos = new Iesi.Collections.Generic.HashedSet<UserPhoto>();
this.Roles = new Iesi.Collections.Generic.HashedSet<Role>();
}
public virtual long Id
{
get;
set;
}
public virtual string Login
{
get;
set;
}
public virtual string Email
{
get;
set;
}
public virtual string Salt
{
get;
set;
}
public virtual string Hash
{
get;
set;
}
public virtual string Name
{
get;
set;
}
public virtual Iesi.Collections.Generic.ISet<Driver> Drivers
{
get;
set;
}
public virtual University University
{
get;
set;
}
public virtual Iesi.Collections.Generic.ISet<UserPhoto> UserPhotos
{
get;
set;
}
public virtual Iesi.Collections.Generic.ISet<Role> Roles
{
get;
set;
}
}
public partial class Driver {
public Driver()
{
this.Trips = new Iesi.Collections.Generic.HashedSet<Trip>();
}
public virtual long Id
{
get;
set;
}
public virtual Car Car
{
get;
set;
}
public virtual User User
{
get;
set;
}
public virtual Iesi.Collections.Generic.ISet<Trip> Trips
{
get;
set;
}
}
There is a User in the system. Table Driver inherits the user table. Each user in the system may have several roles.
I want to implement a few things.
1) User registration. Is it a correct way to implement this features?
[Authorize]
public class AccountController : Controller
{
private readonly IUserTasks userTasks;
public AccountController(IUserTasks userTasks)
{
this.userTasks = userTasks;
}
// GET: /Account/Register
[AllowAnonymous]
public ActionResult Register()
{
return View();
}
// POST: /Account/Register
[HttpPost]
[AllowAnonymous]
public ActionResult Register(RegisterViewModel model)
{
if (ModelState.IsValid)
{
var userToCreate = this.userTasks.Create(model);
return View(customerToCreate);
}
return View(model);
}
}
2) User authentication. Is it a correct way to implement this features?
// GET: /Account/Login
[AllowAnonymous]
public ActionResult Login(string returnUrl)
{
ViewBag.ReturnUrl = returnUrl;
return View();
}
// POST: /Account/Login
[HttpPost]
[AllowAnonymous]
public ActionResult Login(LoginViewModel model, string returnUrl)
{
if (ModelState.IsValid)
{
var user = userTasks.Find(model.UserName, model.Password);
if (user != null)
{
///......
return RedirectToLocal(returnUrl);
}
else
{
ModelState.AddModelError("", "Invalid username or password.");
}
}
return View(model);
}
3) User authorization. User authorization. I want to write some attribute, for example,
[Authorize(Roles = "Admin")]
public string Get(int id)
{
return "value";
}
I do not know how to do it.
In many examples a new library Microsoft.AspNet.Identity is used. Should I use it? There is a implementation NHibernate.AspNet.Identity. However, I do not understand what benefit I get from this.
Also I do not know how to implement user authentication.
I'll be glad if you tell me a vector for further research.
You have 2 choices. Either:
you use OWIN as explained in Setting up Forms Authentication for multiple Web Apps in MVC 5 based on OWIN
you use traditional form-based authentication. See here: MVC 5 External authentication with authentication mode=Forms
As for authorization, Microsoft provides something called claims-based authorization which lets you define user and resource claims and define authorization constraints based on them. Have a look here: Using Claim-Based Authorization
Alternatively, you could look into XACML, the eXtensible Access Control Markup Language. That will require additional libraries outside the .NET framework though. XACML gives you policy-based, fine-grained authorization.
HTH