WebApi: mistmatch between ids - c#

Given the following route
/api/Person/15
And we do a PUT to this route with the body:
{
id: 8,
name: 'Joosh'
}
The route segment value is 15 but the [FromBody] id is 8.
Right now there is something like the following in our controllers:
public Model Put(string id, [FromBody] Model model)
{
if (id != model.Id)
throw new Exception("Id mismatch!");
// ... Do normal stuff
}
Is there a "default" or DRY-ish method for doing this without assuming that it will always be as simple as parameter ID and Model.Id property?

You can achieve via custom model validation
[HttpPut("api/Person/{id}")]
public IActionResult Put(string id, [FromBody]Person person)
{
// ... Do normal stuff
return Ok();
}
public class Person
{
[ValidateId]
public string Id { get; set; }
public string Name { get; set; }
}
public sealed class ValidateId : ValidationAttribute
{
protected override ValidationResult IsValid(object id, ValidationContext validationContext)
{
var httpContextAccessor = (IHttpContextAccessor)validationContext.GetService(typeof(IHttpContextAccessor));
var routeData = httpContextAccessor.HttpContext.GetRouteData();
var idFromUrl = routeData.Values["id"];
if (id.Equals(idFromUrl))
{
return ValidationResult.Success;
}
else
{
return new ValidationResult("Id mismatch!");
}
}
}
// In the Startup class add the IHttpContextAccessor
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddHttpContextAccessor();
// ...
}

Is there a "default" or DRY-ish method for doing this without assuming that it will always be as simple as parameter ID and Model.Id property?
Custom validation logic can be implemented in an ActionFilter. Because the ActionFilter is processed after the model binding in the action execution, the model and action parameters can be used in an ActionFilter without having to read from the Request Body, or the URL. You could refer to the below working demo:
Customize ValidationFilter
public class ValidationFilter: ActionFilterAttribute
{
private readonly ILogger _logger;
public ValidationFilter(ILoggerFactory loggerFactory)
{
_logger = loggerFactory.CreateLogger("ValidatePayloadTypeFilter");
}
public override void OnActionExecuting(ActionExecutingContext context)
{
var carDto = context.ActionArguments["car"] as Car;
var id = context.ActionArguments["id"];
if (Convert.ToInt32(id)!=carDto.Id)
{
context.HttpContext.Response.StatusCode = 400;
context.Result = new ContentResult()
{
Content = "Id mismatch!"
};
return;
}
base.OnActionExecuting(context);
}
}
Register this action filter in the ConfigureServices method
services.AddScoped<ValidationFilter>();
Call this action filter as a service
public class Car
{
public int Id { get; set; }
public string CarName { get; set; }
}
[ServiceFilter(typeof(ValidationFilter))]
[HttpPut("{id}")]
public Car Put(int id, [FromBody] Car car)
{
// the stuff you want
}
Reference:
https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/filters?view=aspnetcore-2.2#action-filters
https://code-maze.com/action-filters-aspnetcore/#comments

You can create your own CustomValidation and compare the values of id and model.id.
Check this link
example of custom model validation.

Related

How can I make an asp.net controller auto bind properties for a custom type?

I have a method on a dotnet controller like this
[HttpGet()]
public async Task<IActionResult> GetCar(
[FromQuery] GetCarFilter filters
)
Using a GetCarFilter class that looks like this
public class GetCarFilter
{
public GetCarFilter()
{
}
public string? SearchTerm { get; set; }
public int NumberOfWheels { get; set; }
public bool HasHeadLights { get; set; }
public ObjectId ModelId { get; set; }
}
Using a querystring like this
http://somedomain.com/cars?NumberOfWheels=4&HasHeadlights=true?ModelId=623c79ac554f9d15425f93c2
Dotnet will automagically pull out values from the querystring and convert them to ints and bools for some properties by not the ObjectId (which is from the MongoDB C# driver)
I have tried creating a custom type converter and adding it to the property like so
[TypeConverter(typeof(MyObjectIdConverter))]
public ObjectId ModelId { get; set; }
But it never calls my code. Somehow dotnet can take the querystring values for NumberOfWheels and HasHeadLights and can convert them to an int and bool but will not do the same for my ModelId. Is there a way I can tell dotnet this is an ObjectId and this is how you convert it from a string?
try this
.....
public string modelId { get; set; }
public ObjectId ModelId { get { return new ObjectId(modelId ); } }
Found that asp.net core is using model binders and you can build your own.
Just create a class the implements IModelBinder
public class ObjectIdModelBinder : IModelBinder
{
public ObjectIdModelBinder()
{
}
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
var modelName = bindingContext.ModelName;
// Try to fetch the value of the argument by name
var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);
if (valueProviderResult == ValueProviderResult.None)
{
return Task.CompletedTask;
}
bindingContext.ModelState.SetModelValue(modelName, valueProviderResult);
var value = valueProviderResult.FirstValue;
// Check if the argument value is null or empty
if (string.IsNullOrEmpty(value))
{
return Task.CompletedTask;
}
if (!ObjectId.TryParse(value, out var id))
{
// Not a valid object id
bindingContext.ModelState.TryAddModelError(
modelName, $"{modelName} must be a valid Mongo Object Id string.");
return Task.CompletedTask;
}
bindingContext.Result = ModelBindingResult.Success(id);
return Task.CompletedTask;
}
}
Then register it with MVC/WebAPI
services.AddControllers(config =>
{
config.ModelBinderProviders.Insert(0, new ObjectIdModelBinderProvider());
});
Be sure to insert instead of add because by default asp.net will place a catchall binder at the end of the list. If you add yours will be placed behind this catchall and never be called.
After doing this any controller parameter marked with a FromBody, FromQueryString, FromRoute, FromHeader, FromForm will be automatically converted. It even lets you provide validation errors in case your conversion fails!

Require not null collection elements in when validating model in aspnet core

I have example mode class:
public class Data
{
[Required]
[MinLength(1)]
public List<Foo> Foos{ get; set; }
}
which is passed to controller:
[ApiController]
public class FooController : ControllerBase
{
public async Task<IActionResult> Post([FromBody] Data data)
{
// Do stuff
}
}
I have nullable annotations enabled in my project configuration.
How can I force model validator to reject request with null foos array? For example following request is accepted by controller:
{
foos: [null]
}
I would like such request to return bad request status code.
When using Data Annotations the child classes are not validated. I recommend Fluent Validation for the use case presented.
The use is simple, just define a rule
public class DataValidator : AbstractValidator<Data>
{
public DataValidator()
{
// RuleFor(r => r.Foos).NotEmpty(); // <-- not needed as Nullable Annotations are enabled
RuleForEach(r => r.Foos).NotNull();
}
}
And register FluentValidation in configure services method
services.AddControllers()
.AddFluentValidation(configuration => configuration.RegisterValidatorsFromAssemblyContaining<Startup>());
You could custom ValidationAttribute to custom the rule:
Custom ValidationAttribute:
public class ValidArray : ValidationAttribute
{
protected override ValidationResult
IsValid(object value, ValidationContext validationContext)
{
var model = (Data)validationContext.ObjectInstance;
if(Extension.IsNullOrEmpty<Foo>(model.Foos))
{
return new ValidationResult
("Array cannot be null");
}
else
{
return ValidationResult.Success;
}
}
}
Custom IsNullOrEmpty:
public static class Extension
{
public static bool IsNullOrEmpty<T>(List<T> array) where T : class
{
if (array == null || array.Count == 0)
return true;
else
return array.All(item => item == null);
}
}
Model:
public class Data
{
//[Required]
[MinLength(1)]
[ValidArray]
public List<Foo> Foos { get; set; }
}
public class Foo
{
public int Id { get; set; }
public int Count { get; set; }
}
Result:

Can you use ModelBinding with a GET request?

I'm curious if it's possible to bind a query string that is passed in with a GET request to a Model.
For example, if the GET url was https://localhost:1234/Users/Get?age=30&status=created
Would it be possible on the GET action to bind the query parameters to a Model like the following:
[HttpGet]
public async Task<JsonResult> Get(UserFilter filter)
{
var age = filter.age;
var status = filter.status;
}
public class UserFilter
{
public int age { get; set; }
public string status { get; set; }
}
I am currently using ASP.NET MVC and I have done quite a bit of searching but the only things I can find are related to ASP.NET Web API. They suggest using the [FromUri] attribute but that is not available in MVC.
I just tested the this, and it does work (at least in .net core 3.1)
[HttpGet("test")]
public IActionResult TestException([FromQuery]Test test)
{
return Ok();
}
public class Test
{
public int Id { get; set; }
public string Yes { get; set; }
}
You can can create an ActionFilterAttribute where you will parse the query parameters, and bind them to a model. And then you can decorate your controller method with that attribute.
For example
public class UserFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
var controller = actionContext.ControllerContext.Controller as CustomApiController;
var queryParams = actionContext.Request.GetQueryNameValuePairs();
var ageParam = queryParams.SingleOrDefault(a => a.Key == "age");
var statusParam = queryParams.SingleOrDefault(a => a.Key == "status");
controller.UserFilter = new UserFilter {
Age = int.Parse(ageParam.Value),
Status = statusParam.Value
};
}
}
The CustomApiController (inherits from your current controller) and has a UserFilter property so you can keep the value there. You can also add null checks in case some of the query parameters are not sent with the request..
Finally you can decorate your controller method
[HttpGet]
[UserFilter]
public async Task<JsonResult> Get()
{
var age = UserFilter.age;
var status = UserFilter.status;
}

Custom validation - Required only when method is put

I have two API endpoints, post and put:
[HttpPost]
[Route("projects")]
public IHttpActionResult Create([FromBody] ProjectDTO projectDto)
{
if (ModelState.IsValid)
{
var project = MappingConfig.Map<ProjectDTO, Project>(projectDto);
_projectService.Create(project);
return Ok("Project successfully created.");
}
else
{
return BadRequest(ModelState);
}
}
[HttpPut]
[Route("projects")]
public IHttpActionResult Edit([FromBody] ProjectDTO projectDto)
{
if (ModelState.IsValid)
{
var project = _projectService.GetById(projectDto.ProjectId);
if (project == null)
return NotFound();
project = Mapper.Map(projectDto, project);
_projectService.Update(project);
return Ok("Project successfully edited.");
}
else
{
return BadRequest(ModelState);
}
}
DTO looks like this:
public class ProjectDTO
{
public int ProjectId { get; set; }
[Required(ErrorMessage = "Name field is required.")]
public string Name { get; set; }
[Required(ErrorMessage = "IsInternal field is required.")]
public bool IsInternal { get; set; }
}
I'm trying to validate field ProjectId. ProjectId field should be required only in HttpPut method when I'm editing my entity.
Is it possible to make custom validation RequiredIfPut or something like that where that field will be required only when editing, but not when creating?
Here is what you can do using custom validation attribute:
public class RequiredWhenPutAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (System.Web.HttpContext.Current.Request.HttpMethod == "PUT")
{
var obj = (ProjectDTO)validationContext.ObjectInstance;
if (obj.ProjectId == null)
{
return new ValidationResult("Project Id is Required");
}
}
else
{
return ValidationResult.Success;
}
}
}
public class ProjectDTO
{
[RequiredWhenPut]
public int? ProjectId { get; set; }
}
Update:
In response to your comment, in order to make the solution more general, you can add a ParentDto class from which other classes are inherited and the shared property needs to be in the ParentDto class, as the following:
public class RequiredWhenPutAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (HttpContext.Current.Request.HttpMethod == "PUT")
{
var obj = (ParentDto)validationContext.ObjectInstance;
if (obj.Id == null)
{
return new ValidationResult(ErrorMessage);
}
}
else
{
return ValidationResult.Success;
}
}
}
public class ParentDto
{
[RequiredWhenPut(ErrorMessage = "Id is required")]
public int? Id { get; set; }
}
public class UserDTO : ParentDto
{
// properties
}
public class ProjectTypeDTO : ParentDto
{
// properties
}
public class ProjectDTO : ParentDto
{
// properties
}
That's one of the reasons, why I use different classes for both case (e.g. ProjectUpdateRequestDto and ProjectCreateRequestDto). Maybe both can be derived from a common base class, but even if not it makes it a lot easier to distinguish between both scenarios.
Also security could be a problem, cause if you use the same class it could be possible that the create requests already contains an id and if your create method simply maps the DTO to an database entity you could overwrite existing data. This means you have to be careful and think about such scenarios. If your create DTO class doesn't have that property it can't be set from the mapper and can't be malused.

how to pass ParentController Property to custom action filter?

I have created a Custom Action Filter with 3 properties as below:
public class TrackUser : ActionFilterAttribute, IActionFilter
{
public string BaseUrl { get; set; }
public string Service { get; set; }
public HealthUtil.PageCode Pagecode { get; set; }
public void OnActionExecuting(ActionExecutingContext filterContext)
{
// Some logic to execute
}
}
I'm trying to use this Customer Action Filter with my action as below:
[TrackUser(BaseUrl =baseUrl, Service =service1, Pagecode =HealthUtil.PageCode.HealthHome)]
public ActionResult AddLead(leadViewModel leaddata)
{
}
The property baseUrl is defined in ParentController class as below:
public static string baseUrl
{
get { return "http://localhost:52985/api/"; }
//set { baseUrl = value; }
}
Issue is I can't pass baseUrl to action filter. I m getting the following error message:
An attribute argument must be a constant expression, typeof expression or array creation expression of an attribute parameter type
Please guide and help me solving this issue.
EDIT
baseUrl in ParentController has to be a property since later code will read it from App.Config.
You can define a method in your ParentController like so:
public class ParentController : Controller
{
public string GetUrl()
{
return ConfigurationManager.AppSettings["AppUrl"];
}
}
Then in your filter class you can call the parent controller's method to get the url like so:
public class TrackUser : ActionFilterAttribute, IActionFilter
{
public string BaseUrl { get; set; }
public string Service { get; set; }
public void OnActionExecuting(ActionExecutingContext filterContext)
{
// Some logic to execute
var controller = filterContext.Controller as ParentController;
if (controller != null)
{
var url = controller.GetUrl();
// Use it here
}
}
}
Hope this helps, follow up question welcome!
Try changing the definition of the baseUrl property as below in the ParentConroller class.
public const string baseUrl = "http://localhost:52985/api/";
Add below code to OnActionExecuting method of TrackUser class
public void OnActionExecuting(ActionExecutingContext filterContext)
{
//get hold of the controller
var controller = filterContext.Controller as ParentController;
//assign the BaseUrl property from tye ParentController and use it as required
BaseUrl = controller.BaseUrl;
// Some logic to execute
}
In the ParentController' class Change thebaseUrl` as below,
//get the URL from App.Config and assign to baseUrl
public string baseUrl = "http://localhost:52985/api/";
Update the AddLead Action attribute by removing the BaseUrl property, as it is handled in the TrackUser Action filter class
[TrackUser(Service = service1, Pagecode = HealthUtil.PageCode.HealthHome)]
public ActionResult AddLead(leadViewModel leaddata)
{
//do something with the action
}`

Categories