I have the following asp.net WebApi2 route using .NET 4.6 that illustrates the problem I am having:
[Route("books/{id}")]
[HttpGet]
public JsonResponse GetBooks(string id, [FromUri]DescriptorModel model)
With the following model:
public class DescriptorModel
{
public bool Fiction { get; set; } = false;
// other properties with default arguments here
}
I am trying to allow Fiction property to be set to a default value (if not specified during the get request).
When I specify the Fiction property explicitly it works correctly:
curl -X GET --header 'Accept: application/json' 'http://127.0.0.1:11000/api/v1/books/516.375/?Fiction=false'
However, when doing the following test (omitting the property with the default argument):
curl -X GET --header 'Accept: application/json' 'http://127.0.0.1:11000/api/v1/books/516.375'
The value of "model" is bound as null which is not what I am looking for. My question is how to simply allow models defined with default values to be instantiated as such during/after the model binding process but prior to the controller's "GetBooks" action method being called.
NOTE. the reason I use models with GET requests is that documenting in swagger is much easier as then my GET/POST actions can reuse the same models in many case via inheritance.
Since you are using id as FromUri, the only way you can use a model with get is to use url with a query string
[Route("~/GetBooks/{id?}")]
[HttpGet]
public IActionResult GetBooks(string id, [FromQuery] DescriptorModel model)
in this case you url should be
'http://127.0.0.1:11000/api/v1/books/?Name=name&&fiction=true'
//or if fiction==false just
'http://127.0.0.1:11000/api/v1/books/?Name=name'
//or if want to use id
'http://127.0.0.1:11000/api/v1/books/123/?Name=name&&fiction=true'
using model your way will be working only with [FromForm] or [FromBody].
To use it as MVC recomends try this
[Route("books/{id}/{param1}/{param2}/{fiction?}")]
[HttpGet]
public JsonResponse GetBooks(string id, string param1, string param2, bool fiction)
By the way, you don't need to make bool false as default since it is false by default any way
if you want to use ID and DescriptorModel from uri you can do this only if you add Id to DescriptorModel too
[Route("books/{id}/{param1}/{param2}/{fiction?}")]
[HttpGet]
public JsonResponse GetBooks(DescriptorModel model)
UPDATE
If your mvc doesnt support [FromQuery], you can use RequestQuery inside of action like this
var value= context.Request.Query["value"];
but is better to update to MVC 6.
I wasn't able to figure out how to do this via model-binding but I was able to use Action Filters to accomplish the same thing.
Here's the code I used (note it only supports one null model per action but this could easily be fixed if needed):
public class NullModelActionFilter : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext context)
{
object value = null;
string modelName = string.Empty;
// are there any null models?
if (context.ActionArguments.ContainsValue(null))
{
// Yes => iterate over all arguments to find them.
foreach (var arg in context.ActionArguments)
{
// Is the argument null?
if (arg.Value == null)
{
// Search the parameter bindings to find the matching argument....
foreach (var parameter in context.ActionDescriptor.ActionBinding.ParameterBindings)
{
// Did we find a match?
if (parameter.Descriptor.ParameterName == arg.Key)
{
// Yes => Does the type have the 'Default' attribute?
var type = parameter.Descriptor.ParameterType;
if (type.GetCustomAttributes(typeof(DefaultAttribute), false).Length > 0)
{
// Yes => need to instantiate it
modelName = arg.Key;
var constructor = parameter.Descriptor.ParameterType.GetConstructor(new Type[0]);
value = constructor.Invoke(null);
// update the model state
context.ModelState.Add(arg.Key, new ModelState { Value = new ValueProviderResult(value, value.ToString(), CultureInfo.InvariantCulture) });
}
}
}
}
}
// update the action arguments
context.ActionArguments[modelName] = value;
}
}
}
I created a DefaultAttribute class like so:
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class DefaultAttribute : Attribute
{
}
I then added that attribute to my descriptor class:
[Default]
public class DescriptorModel
{
public bool Fiction { get; set; } = false;
// other properties with default arguments here
}
And finally registered the action filter in
public void Configure(IAppBuilder appBuilder)
{
var config = new HttpConfiguration();
// lots of configuration here omitted
config.Filters.Add(new NullModelActionFilter());
appBuilder.UseWebApi(config);
}
I definitely consider this a hack (I think I really should be doing this via model binding) but it accomplishes what I needed to do with the constraints that I was given of ASP.NET (not Core) / WebApi2 / .NET Framework so hopefully some else will benefit from this.
Related
I'm trying to make a post request from my Angular frontend to the .net Core 3.1 backend, but in the controller method the argument object only gets by default 0 and null values;
let response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(obj)
});
[ApiController]
[Route("[controller]")]
[ApiExplorerSettings(IgnoreApi = true)]
public class RequestController : ControllerBase
{
[HttpPost]
public async Task<IAResponseViewModel> PostAnswer([FromBody] IAResponseViewModel something)
{
var temp = something.ToString();
if (!ModelState.IsValid)
{
Console.WriteLine();
}
return something;
}
}
public class IAResponseViewModel
{
public string AdministrationId { get; }
public int RoundUsesItemId { get; }
public int ResponseOptionId { get; }
}
The JSON object I see being submitted
{AdministrationId: "12345678-00e8-4edb-898b-03ee7ff517bf", RoundUsesItemId: 527, ResponseOptionId: 41}
When inspecting the controller method the 3 values of the IAResponseViewModel are null or 0
When I change the argument to object I get something with a value of
ValueKind = Object : "{"AdministrationId":"12345678-00e8-4edb-898b-03ee7ff517bf","RoundUsesItemId":523,"ResponseOptionId":35}"
I've tried with and without the [FromBody] attribute, changing the casing of the properties of the controller method's argument and the frontend argument, copy pasting the viewmodel attributes on to the submitted object keys, wrapping the posted object in a 'something' object. the ModelState.IsValid attribute shows as true.
I've red other answers such as Asp.net core MVC post parameter always null &
https://github.com/dotnet/aspnetcore/issues/2202 and others but couldn't find an answer that helped.
Why isn't the model binding working and how do I populate the viewmodel class I'm using with the json data?
From a comment on my original question:
Could it be because the properties in IAResponseViewModel are 'get only'?
Indeed this was the problem. Giving the properties (default) set methods fixed the problem.
Kestrel only allows 30MB of request body in the POST request. I am changing the request body limit on a particular action method using [RequestSizeLimit(100000000)]
I would like to pass "100000000" value through configuration. Is there a way to do it?
Action
[HttpPost]
[RequestSizeLimit(100000000)]
public IActionResult MyAction(MyViewModel data)
{
}
This works fine but I am not able to pass it through configuration/appsettings
RequestSizeLimit is an attribute that implements IFilterFactory and returns RequestSizeLimitFilter in CreateInstance() method. You can implement similar filter that will read this limit from configuration:
public class RequestSizeLimitFromConfigAttribute : Attribute, IFilterFactory
{
private string _configurationKey;
public RequestSizeLimitFromConfigAttribute(string configurationKey)
{
_configurationKey = configurationKey;
}
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
{
var filter = serviceProvider.GetRequiredService<RequestSizeLimitFilter>();
var config = serviceProvider.GetRequiredService<IConfiguration>();
filter.Bytes = config.GetValue<int>(_configurationKey);
return filter;
}
...
}
My project is using the following code to generate MVC routed URLs for the given routeName and routeValues:
public class RouteBuilder
{
public string GetUrl(string routeName, object routeValues)
{
return RouteTable.Routes.GetVirtualPath(
new RequestContext(new MockHttpContextBase(string.Empty), new RouteData()),
routeName,
new RouteValueDictionary(routeValues)
)?.VirtualPath;
}
}
For example, given the following route definition:
public MyController : Controller
{
[Route("Foo", "{bar}/{quux}")]
public IActionResult MyControllerMethod(MyModel model) { }
}
we call:
// yields "zod/baz"
var routedUrl = RouteBuilder.GetUrl("Foo", new
{
bar = "zod",
quux = "baz",
});
The problem comes in with the route values object passed to GetUrl. Out of necessity it can be any type of object, including an anonymous one as demonstrated in the example. But this is problematic because if there are any typos in the object's definition, the route template won't match and the URL won't be built:
// yields null!
var routedUrl = RouteBuilder.GetUrl("Foo", new
{
baz = "zod", // oops, typo in 1st param name!
quux = "baz",
});
This is even more of an issue because it can only be caught at runtime.
A potential solution is this:
public class MyControllerMethodRouteParameters
{
string bar { get; set; }
string quux { get; set; }
}
public MyController : Controller
{
[Route("Foo", "{" + nameof(MyControllerMethodRouteParameters.bar) + "}/{" + nameof(MyControllerMethodRouteParameters.quux ) + "}")]
public IActionResult MyControllerMethod(MyModel model) { }
}
public class RouteBuilder
{
private string GetUrl(string routeName, object routeValues) { /* as before */ }
public string GetUrl(MyControllerMethodRouteParameters params)
{
return GetUrl(GetRouteNameFor(params.GetType(), params));
}
}
But it has a large drawback in terms of developer effort: when adding a new controller method, you also have to remember to add a route parameters class, a GetUrl overload for it, and a routename => route parameters type mapping. Easy to forget to do, and it also feels like unnecessary repetition since all of the necessary parameters and their types are already defined in the RouteAttribute.
So my idea was to generate the needed code via a T4 template, by reflecting over the assembly, grabbing all RouteAttributes and pulling the route name and template out of them, then parsing the template for the property names of the route parameters class. It's this last hurdle that I've fallen at, because while I know I can probably write a regex(es) to match and extract the route params from the template, I would prefer to use existing functionality to do this.
Problem is, I can't find anything that seems like it'll do this for me. The closest appears to be the internal class System.Web.Mvc.Routing.InlineRouteTemplateParser but my experiments with it were not particularly fruitful. Is there anything I can or should be using to achieve this, or should I just give in to the dark regex god?
I am having trouble understanding the model binding process in Asp.Net core 2. I have a very simple API that has a model. It has some basic validation on it. Whenever a user posts an incorrect model, I am trying to return a 422 unprocessableentity along with the error messages from the modelstate.
The 2 issues I am trying to understand are as follows:
If I post a request without an ID, a default ID of 0 is being created circumventing the required attribute. I am assuming this is C# functionality for providing default values to fields. Is there a way to circumvent this?
The other problem is that if I place a breakpoint in my post action and send a bad request, it does not even go into the method. It sends back a 400 bad request by using the validation attributes. How does this work? Does the request halt as soon as it tries to model bind to an invalid property (i.e. Name length > 10)? What I need it to do is send back a 422 unprocessable entity with the same error message instead of 400.
Does ASP.NET not even go into the method if the model state validation fails based on the validation attributes? What would be a better way to solve this issue to return a 422 error code?
Below is the code for my various classes (I used the API template when creating the project):
Startup.cs - Only thing I added here was the singleton instance of my in-memory context
public void ConfigureServices(IServiceCollection services)
{
//services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddMvc();
services.AddSingleton<IItemRepository, ItemRepository>();
}
IItemRepository.cs - My interface for DI
public interface IItemRepository
{
List<ItemModel> Items { get; set; }
void AddValue(ItemModel itemModel);
}
ItemRepository.cs - Concrete implementation
public class ItemRepository : IItemRepository
{
public List<ItemModel> Items { get; set; } = new List<ItemModel>();
public ItemRepository()
{
Items.AddRange(
new List<ItemModel> {
new ItemModel {Id = 1, Name = "Test1" },
new ItemModel {Id = 2, Name = "Test2" }
}
);
}
public void AddValue(ItemModel itemModel)
{
Items.Add(itemModel);
}
}
ItemModel.cs - My model class for user input
public class ItemModel
{
[Required]
public int Id { get; set; }
[MaxLength(10)]
public string Name { get; set; }
}
ValuesController.cs
[Route("api/[controller]")]
[ApiController]
public class ValuesController : Controller
{
private IItemRepository _context;
public ValuesController(IItemRepository context)
{
_context = context;
}
// GET api/values
[HttpGet]
public ActionResult<IEnumerable<string>> Get()
{
return Ok(_context.Items);
}
// GET api/values/5
[HttpGet("{id}", Name = "GetSingle")]
public ActionResult<string> Get(int id)
{
return Ok(_context.Items.Where(x => x.Id == id));
}
// Problem here - placing a breakpoint in below method does not do anytthing as it will return a 400 bad request instead of 422
[HttpPost]
public ActionResult Post([FromBody] ItemModel itemModel)
{
if (!ModelState.IsValid)
{
return new UnprocessableEntityObjectResult(ModelState);
}
ItemModel addNew = new ItemModel { Id = itemModel.Id, Name = itemModel.Name };
_context.AddValue(addNew);
return Ok(addNew);
}
}
For your first issue, if you don't want to make the property nullable, you can also put a range attribute [Range(1, int.MaxValue)], but 0 will not be a valid value in this case.
For your second issue, if you still want the automatic model validation from ApiControllerAttribute but want a 422 response code instead of 400 you can use the InvalidModelStateResponseFactory configuration option.
services.Configure<ApiBehaviorOptions>(options =>
{
options.InvalidModelStateResponseFactory = ctx =>
new UnprocessableEntityObjectResult(ctx.ModelState);
});
If I post a request without an ID, a default ID of 0 is being created
circumventing the required attribute. I am assuming this is C#
functionality for providing default values to fields. Is there a way
to circumvent this?
As #StephenMuecke answered here, you need to change your model to
public class ItemModel
{
[Required]
public int? Id { get; set; }
[MaxLength(10)]
public string Name { get; set; }
}
The other problem is that if I place a breakpoint in my post action
and send a bad request, it does not even go into the method. It sends
back a 400 bad request by using the validation attributes. How does
this work? Does the request halt as soon as it tries to model bind to
an invalid property (i.e. Name length > 10)? What I need it to do is
send back a 422 unprocessable entity with the same error message
instead of 400.
This is because you applied the ApiControllerAttribute to the Controller. From the documentation:
Validation errors automatically trigger an HTTP 400 response. The following code becomes unnecessary in your actions:
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
You can either remove the attribute, or, as the same link explains, add this to the startup configuration:
services.Configure<ApiBehaviorOptions>(options =>
{
options.SuppressModelStateInvalidFilter = true;
})
Your first issue can be solved by making the property nullable. As commented by Stepen Muecke.
Also take a look here, perhaps the BindRequired attribute can help. The article also describes how to tweak behaviour.
For your second issue, this is new (breaking) behaviour by Asp.Net Core 2.1. New is the automatic 400 response. That explains why your breakpoint isn't hit. You can suppress this as follows:
services.Configure<ApiBehaviorOptions>(options =>
{
options.SuppressModelStateInvalidFilter = true;
});
I have the following class:
public class GetLogsRequestDto
{
public LogLevel Level { get; set; }
public LogSortOrder SortOrder { get; set; }
}
I have a Web API Controller (LogsController) with the following 2 actions:
async Task<IHttpActionResult> Get( [FromUri]int id )
async Task<IHttpActionResult> Get( [FromUri]GetLogsRequestDto dto )
The first for retrieving a specific log, and the second for retrieving a list of logs. When I make a GET request for a specific log via: /logs/123, it calls the 1st action correctly, and likewise if I make a GET request for /logs it calls the 2nd action correctly (the properties defined in that class are optional and don't need to always be provided).
However, I wanted to change the first GET method so it uses a class instead of the int id parameter, like this (note it's specifying a different (singular) type to the 2nd action above):
async Task<IHttpActionResult> Get( [FromUri]GetLogRequestDto dto )
This GetLogRequestDto class looks like this:
public class GetLogRequestDto
{
[Required]
[Range( 100, int.MaxValue )]
public int Id { get; set; }
}
My reasoning behind this approach was so that I can have validation of the model go through my standard ModelStateValidationActionFilter, and also put any specific validation attributes inside this class, rather than when using the 'int id' parameter approach, then having to perform validation.
When I implement this approach though and attempt to call /logs/1, I get the following error:
Multiple actions were found that match the request
It's not differentiating between the 2 different types used as params in these 2 methods.
The default route I have configured is:
config.Routes.MapHttpRoute(
name: "controller-id",
routeTemplate: "{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
I can't figure out why there is a problem - why it works one way but not the other.
Using a complex type for handling a single basic type parameter (that is also part of the route) in GET requests is not a great idea.
By using this approach the framework will not be able to bind your route parameter to that complex type (the route definition requires an id parameter that must be a simple type).
I strongly suggest you to revert your changes and make the id parameter again an int.
As an alternative approach you may follow this great post and implement an action filter that may validate your method parameters decorated by validation attributes even if they are simple types.
Here it is an excerpt from Mark Vincze's blog post representing the action filter attribute used to validate action parameters:
public class ValidateActionParametersAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
var descriptor = context.ActionDescriptor as ControllerActionDescriptor;
if (descriptor != null)
{
var parameters = descriptor.MethodInfo.GetParameters();
foreach (var parameter in parameters)
{
var argument = context.ActionArguments[parameter.Name];
EvaluateValidationAttributes(parameter, argument, context.ModelState);
}
}
base.OnActionExecuting(context);
}
private void EvaluateValidationAttributes(ParameterInfo parameter, object argument, ModelStateDictionary modelState)
{
var validationAttributes = parameter.CustomAttributes;
foreach (var attributeData in validationAttributes)
{
var attributeInstance = CustomAttributeExtensions.GetCustomAttribute(parameter, attributeData.AttributeType);
var validationAttribute = attributeInstance as ValidationAttribute;
if (validationAttribute != null)
{
var isValid = validationAttribute.IsValid(argument);
if (!isValid)
{
modelState.AddModelError(parameter.Name, validationAttribute.FormatErrorMessage(parameter.Name));
}
}
}
}
}