Manually bind form data in ASP.NET Core - c#

I have a web api with some actions that should receive both data and files. To do so, I am accepting multipart/form-data instead of JSON and binding to the model using [FromForm]:
[Authorize(Policy = "MyCustomPolicy")]
public async Task<IActionResult> Create([FromForm]MyCustomDto myDto)
{
// Do some stuff here
}
public class MyCustomDto
{
public int Id { get; set; }
public string Title { get; set; }
public IEnumerable<IFormFile> Attachments { get; set; }
}
This works fine and is correctly bound to the DTO.
The Policy is checked and enforced using a AuthorizationHandler, which also works fine.
In said AuthorizationHandler, I need to access some stuff passed to the controller, such as IDs in the route or DTOs.
Accessing route data is quite easy with authContext.RouteData.Values["nameOfIdField"].
For the body, however, I have created a helper extension method that reads the body stream and deserializes it:
public static async Task<T> DeserializeBody<T>(this AuthorizationFilterContext context, string name)
{
var content = string.Empty;
using(var reader = new StreamReader(context.HttpContext.Request.Body, System.Text.Encoding.UTF8, true, 1024, true))
{
content = await reader.ReadToEndAsync();
reader.BaseStream.Seek(0, SeekOrigin.Begin);
}
if (string.IsNullOrWhiteSpace(content))
{
return default(T);
}
return JsonConvert.DeserializeObject<T>(content);
}
Again, this also works quite fine. However, now I am having trouble with DTOs that are not passed as JSON but - as said in the beginning - as form data.
The body's content is not a JSON that can be serialized easily:
-----------------------------7e2b13b820d4a
Content-Disposition: form-data; name="id"
232
-----------------------------7e2b13b820d4a
Content-Disposition: form-data; name="title"
test
-----------------------------7e2b13b820d4a
Content-Disposition: form-data; name="attachments"; filename="C:\Temp\Test.jpg"
Content-Type: image/jpeg
Is there any way to bind this to my DTO easily, without having to manually parse it?

You're better off handling this situation in your action method:
public class SomeController : Controller
{
private readonly IAuthorizationService _authorizationService;
public SomeController(IAuthorizationService authorizationService)
{
_authorizationService = authorizationService;
}
public async Task<IActionResult> Create([FromForm]MyCustomDto myDto)
{
var authorizationResult = await _authorizationService.AuthorizeAsync(User, myDto, "MyCustomPolicy");
if (!authorizationResult.Succeeded)
return User.Identity.IsAuthenticated ? Forbid() : (IActionResult)Challenge();
// Do some stuff here
}
And you define your authorization handler like this:
public class MyCustomDtoAuthorizationHandler : AuthorizationHandler<MyCustomDtoRequirement, MyCustomDto>
{
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, MyCustomDtoRequirement requirement, MyCustomDto resource)
{
// your authorization logic based on the resource argument...
}
}
The big problem with choosing the AuthorizeFilter way is that authorization filters are executed before model binding takes place. (Just take a look at the source of the ResourceInvoker class.) You would need to bind your model manually to access the required information for authorization in it. Then the framework would do its job resulting in model binding being done two times and thus, in performance degradation. And this should and could be avoided as it was described earlier.
Update
I've just noticed I accidentally left an important piece of code out of the action method. Corrected.

In said AuthorizationHandler, I need to access some stuff passed to the controller, such as IDs in the route or DTOs
Please don't do that. You are basically replicating the logic already in place on how to parse your parameters from the request.
The basic handlers are for basic cases: For example, only authenticated members of the "BookClub" role should be able to access the BooksController methods. That's great.
As soon as you find yourself needing information from the message itself, please don't go and do all of that parsing by hand. Let ASP do it's thing and parse the message as per your given constraints and then when the message is complete, invoke your authorization logic on the objects you got.
Microsoft Example

Related

Return String or 404 exception in asp .net web api core 3 [duplicate]

ASP.NET Core API controllers typically return explicit types (and do so by default if you create a new project), something like:
[Route("api/[controller]")]
public class ThingsController : Controller
{
// GET api/things
[HttpGet]
public async Task<IEnumerable<Thing>> GetAsync()
{
//...
}
// GET api/things/5
[HttpGet("{id}")]
public async Task<Thing> GetAsync(int id)
{
Thing thingFromDB = await GetThingFromDBAsync();
if(thingFromDB == null)
return null; // This returns HTTP 204
// Process thingFromDB, blah blah blah
return thing;
}
// POST api/things
[HttpPost]
public void Post([FromBody]Thing thing)
{
//..
}
//... and so on...
}
The problem is that return null; - it returns an HTTP 204: success, no content.
This is then regarded by a lot of client side Javascript components as success, so there's code like:
const response = await fetch('.../api/things/5', {method: 'GET' ...});
if(response.ok)
return await response.json(); // Error, no content!
A search online (such as this question and this answer) points to helpful return NotFound(); extension methods for the controller, but all these return IActionResult, which isn't compatible with my Task<Thing> return type. That design pattern looks like this:
// GET api/things/5
[HttpGet("{id}")]
public async Task<IActionResult> GetAsync(int id)
{
var thingFromDB = await GetThingFromDBAsync();
if (thingFromDB == null)
return NotFound();
// Process thingFromDB, blah blah blah
return Ok(thing);
}
That works, but to use it the return type of GetAsync must be changed to Task<IActionResult> - the explicit typing is lost, and either all the return types on the controller have to change (i.e. not use explicit typing at all) or there will be a mix where some actions deal with explicit types while others. In addition unit tests now need to make assumptions about the serialisation and explicitly deserialise the content of the IActionResult where before they had a concrete type.
There are loads of ways around this, but it appears to be a confusing mishmash that could easily be designed out, so the real question is: what is the correct way intended by the ASP.NET Core designers?
It seems that the possible options are:
Have a weird (messy to test) mix of explicit types and IActionResult depending on expected type.
Forget about explicit types, they're not really supported by Core MVC, always use IActionResult (in which case why are they present at all?)
Write an implementation of HttpResponseException and use it like ArgumentOutOfRangeException (see this answer for an implementation). However, that does require using exceptions for program flow, which is generally a bad idea and also deprecated by the MVC Core team.
Write an implementation of HttpNoContentOutputFormatter that returns 404 for GET requests.
Something else I'm missing in how Core MVC is supposed to work?
Or is there a reason why 204 is correct and 404 wrong for a failed GET request?
These all involve compromises and refactoring that lose something or add what seems to be unnecessary complexity at odds with the design of MVC Core. Which compromise is the correct one and why?
This is addressed in ASP.NET Core 2.1 with ActionResult<T>:
public ActionResult<Thing> Get(int id) {
Thing thing = GetThingFromDB();
if (thing == null)
return NotFound();
return thing;
}
Or even:
public ActionResult<Thing> Get(int id) =>
GetThingFromDB() ?? NotFound();
I'll update this answer with more detail once I've implemented it.
Original Answer
In ASP.NET Web API 5 there was an HttpResponseException (as pointed out by Hackerman) but it's been removed from Core and there's no middleware to handle it.
I think this change is due to .NET Core - where ASP.NET tries to do everything out of the box, ASP.NET Core only does what you specifically tell it to (which is a big part of why it's so much quicker and portable).
I can't find a an existing library that does this, so I've written it myself. First we need a custom exception to check for:
public class StatusCodeException : Exception
{
public StatusCodeException(HttpStatusCode statusCode)
{
StatusCode = statusCode;
}
public HttpStatusCode StatusCode { get; set; }
}
Then we need a RequestDelegate handler that checks for the new exception and converts it to the HTTP response status code:
public class StatusCodeExceptionHandler
{
private readonly RequestDelegate request;
public StatusCodeExceptionHandler(RequestDelegate pipeline)
{
this.request = pipeline;
}
public Task Invoke(HttpContext context) => this.InvokeAsync(context); // Stops VS from nagging about async method without ...Async suffix.
async Task InvokeAsync(HttpContext context)
{
try
{
await this.request(context);
}
catch (StatusCodeException exception)
{
context.Response.StatusCode = (int)exception.StatusCode;
context.Response.Headers.Clear();
}
}
}
Then we register this middleware in our Startup.Configure:
public class Startup
{
...
public void Configure(IApplicationBuilder app)
{
...
app.UseMiddleware<StatusCodeExceptionHandler>();
Finally actions can throw the HTTP status code exception, while still returning an explicit type that can easily be unit tested without conversion from IActionResult:
public Thing Get(int id) {
Thing thing = GetThingFromDB();
if (thing == null)
throw new StatusCodeException(HttpStatusCode.NotFound);
return thing;
}
This keeps the explicit types for the return values and allows easy distinction between successful empty results (return null;) and an error because something can't be found (I think of it like throwing an ArgumentOutOfRangeException).
While this is a solution to the problem it still doesn't really answer my question - the designers of the Web API build support for explicit types with the expectation that they would be used, added specific handling for return null; so that it would produce a 204 rather than a 200, and then didn't add any way to deal with 404? It seems like a lot of work to add something so basic.
You can actually use IActionResult or Task<IActionResult> instead of Thing or Task<Thing> or even Task<IEnumerable<Thing>>. If you have an API that returns JSON then you can simply do the following:
[Route("api/[controller]")]
public class ThingsController : Controller
{
// GET api/things
[HttpGet]
public async Task<IActionResult> GetAsync()
{
}
// GET api/things/5
[HttpGet("{id}")]
public async Task<IActionResult> GetAsync(int id)
{
var thingFromDB = await GetThingFromDBAsync();
if (thingFromDB == null)
return NotFound();
// Process thingFromDB, blah blah blah
return Ok(thing); // This will be JSON by default
}
// POST api/things
[HttpPost]
public void Post([FromBody] Thing thing)
{
}
}
Update
It seems as though the concern is that being explicit in the return of an API is somehow helpful, while it is possible to be explicit it is in fact not very useful. If you're writing unit tests that exercise the request / response pipeline you are typically going to verify the raw return (which would most likely be JSON, i.e.; a string in C#). You could simply take the returned string and convert it back to the strongly typed equivalent for comparisons using Assert.
This seems to be the only shortcoming with using IActionResult or Task<IActionResult>. If you really, really want to be explicit and still want to set the status code there are several ways to do this - but it is frowned upon as the framework already has a built-in mechanism for this, i.e.; using the IActionResult returning method wrappers in the Controller class. You could write some custom middleware to handle this however you'd like, however.
Finally, I would like to point out that if an API call returns null according to W3 a status code of 204 is actually accurate. Why on earth would you want a 404?
204
The server has fulfilled the request but does not need to return an
entity-body, and might want to return updated metainformation. The
response MAY include new or updated metainformation in the form of
entity-headers, which if present SHOULD be associated with the
requested variant.
If the client is a user agent, it SHOULD NOT change its document view
from that which caused the request to be sent. This response is
primarily intended to allow input for actions to take place without
causing a change to the user agent's active document view, although
any new or updated metainformation SHOULD be applied to the document
currently in the user agent's active view.
The 204 response MUST NOT include a message-body, and thus is always
terminated by the first empty line after the header fields.
I think the first sentence of the second paragraph says it best, "If the client is a user agent, it SHOULD NOT change its document view from that which caused the request to be sent". This is the case with an API. As compared to a 404:
The server has not found anything matching the Request-URI. No
indication is given of whether the condition is temporary or
permanent. The 410 (Gone) status code SHOULD be used if the server
knows, through some internally configurable mechanism, that an old
resource is permanently unavailable and has no forwarding address.
This status code is commonly used when the server does not wish to
reveal exactly why the request has been refused, or when no other
response is applicable.
The primary difference being one is more applicable for an API and the other for the document view, i.e.; the page displayed.
In order to accomplish something like that(still, I think that the best approach should be using IActionResult), you can follow, where you can throw an HttpResponseException if your Thing is null:
// GET api/things/5
[HttpGet("{id}")]
public async Task<Thing> GetAsync(int id)
{
Thing thingFromDB = await GetThingFromDBAsync();
if(thingFromDB == null){
throw new HttpResponseException(HttpStatusCode.NotFound); // This returns HTTP 404
}
// Process thingFromDB, blah blah blah
return thing;
}
From ASP.NET Core 7, a action controller can return a HttpResults type. Then you can :
public async Task<Results<Ok<Product>, NotFound>> GetAsync(int id)
{
Thing thingFromDB = await GetThingFromDBAsync();
if(thingFromDB == null)
return TypedResults.NotFound();
...
return TypedResults.Ok(thingFromDB);
}
I love this syntax, because it's explicitly indicate that return the API. But actually, the openAPI specification generator don't manage this. You can follow the progress from this Github ticket :
TypedResults metadata are not inferred for API Controllers
I too looked high and low for an answer to what to do about strongly typed responses when I wanted to return an 400 response for bad data sent into the request. My project is in ASP.NET Core Web API (.NET5.0). The solution I found was basically set the status code and return default version of the object. Here is your example with the change to set the status code to 404 and return the default object when the db object is null.
[Route("api/[controller]")]
public class ThingsController : Controller
{
// GET api/things
[HttpGet]
public async Task<IEnumerable<Thing>> GetAsync()
{
//...
}
// GET api/things/5
[HttpGet("{id}")]
public async Task<Thing> GetAsync(int id)
{
Thing thingFromDB = await GetThingFromDBAsync();
if(thingFromDB == null)
{
this.Response.StatusCode = Microsoft.AspNetCore.Http.StatusCodes.Status404NotFound;
return default(Thing);
}
// Process thingFromDB, blah blah blah
return thing;
}
// POST api/things
[HttpPost]
public void Post([FromBody]Thing thing)
{
//..
}
//... and so on...
}
ASP.NET Core 3.1 introduced filter.
Filters in ASP.NET Core allow code to run before or after specific stages in the request processing pipeline.
You can define a result filter that transform null ok result to not found result :
public class NullAsNotFoundResultFilter : IResultFilter
{
public void OnResultExecuted(ResultExecutedContext context)
{ }
public void OnResultExecuting(ResultExecutingContext context)
{
if(context.Result is ObjectResult result && result.Value == null)
{
context.Result = new NotFoundResult();
}
}
}
Finally, you need to add the filter in the MVC pipeline :
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers(o => o.Filters.Add<NullAsNotFoundResultFilter>());
Had another problem with same behavior - all methods return 404. The problem was in missing code block
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});

How to have an action to support form-data and as JSON in the body?

I have a controller action:
[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> Register(RegisterViewModel model, string returnUrl = null) { }
I need to be able to call this endpoint either through a razor page or using the HttpClient's PostAsJsonAsync.
I got this working for the razor page. But when I do:
var response = await client.PostAsJsonAsync($"{api}Account/Register", new { email, password });
I get a 415 Unsupported Media Type.
How can I support both ways of submitting the data?
The new behavior of model binding designed in asp.net core is a bit inconvenient in some cases (for some developers) but I personally think it's necessary to reduce some complexity in the binding code (which can improve the performance a bit). The new behavior of model binding reflects clearly the purpose of the endpoints (MVC vs Web API). So usually your web browsers don't consume Web API directly (in case you use it, you must follow its rule, e.g: post the request body with application/json content-type).
If you look into the source code of asp.net core, you may have the best work-around or solution to customize it to support the multi-source model binding again. I did not look into any source code but I've come up with a simple solution to support model binding from either form or request body like this. Note that it will bind data from either the form or request body, not combined from both.
The idea is just based on implementing a custom IModelBinder and IModelBinderProvider. You can wrap the default one (following the decorator pattern) and add custom logic around. Otherwise you need to re-implement the whole logic.
Here is the code
//the custom IModelBinder, this supports binding data from either form or request body
public class FormBodyModelBinder : IModelBinder
{
readonly IModelBinder _overriddenModelBinder;
public FormBodyModelBinder(IModelBinder overriddenModelBinder)
{
_overriddenModelBinder = overriddenModelBinder;
}
public async Task BindModelAsync(ModelBindingContext bindingContext)
{
var httpContext = bindingContext.HttpContext;
var contentType = httpContext.Request.ContentType;
var hasFormData = httpContext.Request.HasFormContentType;
var hasJsonData = contentType?.Contains("application/json") ?? false;
var bindFromBothBodyAndForm = bindingContext.ModelMetadata is DefaultModelMetadata mmd &&
(mmd.Attributes.Attributes?.Any(e => e is FromBodyAttribute) ?? false) &&
(mmd.Attributes.Attributes?.Any(e => e is FromFormAttribute) ?? false);
if (hasFormData || !hasJsonData || !bindFromBothBodyAndForm)
{
await _overriddenModelBinder.BindModelAsync(bindingContext);
}
else //try binding model from the request body (deserialize)
{
try
{
//save the request body in HttpContext.Items to support binding multiple times
//for multiple arguments
const string BODY_KEY = "___request_body";
if (!httpContext.Items.TryGetValue(BODY_KEY, out var body) || !(body is string jsonPayload))
{
using (var streamReader = new StreamReader(httpContext.Request.Body))
{
jsonPayload = await streamReader.ReadToEndAsync();
}
httpContext.Items[BODY_KEY] = jsonPayload;
}
bindingContext.Result = ModelBindingResult.Success(JsonSerializer.Deserialize(jsonPayload, bindingContext.ModelType));
}
catch
{
bindingContext.Result = ModelBindingResult.Success(Activator.CreateInstance(bindingContext.ModelType));
}
}
}
}
//the corresponding model binder provider
public class FormBodyModelBinderProvider : IModelBinderProvider
{
readonly IModelBinderProvider _overriddenModelBinderProvider;
public FormBodyModelBinderProvider(IModelBinderProvider overriddenModelBinderProvider)
{
_overriddenModelBinderProvider = overriddenModelBinderProvider;
}
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
var modelBinder = _overriddenModelBinderProvider.GetBinder(context);
if (modelBinder == null) return null;
return new FormBodyModelBinder(modelBinder);
}
}
We write a simple extension method on MvcOptions to help configure it to override an IModelBinderProvider with the custom one to support multisource model binding.
public static class MvcOptionsExtensions
{
public static void UseFormBodyModelBinderProviderInsteadOf<T>(this MvcOptions mvcOptions) where T : IModelBinderProvider
{
var replacedModelBinderProvider = mvcOptions.ModelBinderProviders.OfType<T>().FirstOrDefault();
if (replacedModelBinderProvider != null)
{
var customProvider = new FormBodyModelBinderProvider(replacedModelBinderProvider);
mvcOptions.ModelBinderProviders.Remove(replacedModelBinderProvider);
mvcOptions.ModelBinderProviders.Add(customProvider);
}
}
}
To support binding complex model from either form or request body, we can override the ComplexTypeModelBinderProvider like this:
//in the ConfigureServices method
services.AddMvc(o => {
o.UseFormBodyModelBinderProviderInsteadOf<ComplexTypeModelBinderProvider>();
};
That should suit most of the cases in which your action's argument is of a complex type.
Note that the code in the FormBodyModelBinder requires the action arguments to be decorated with both FromFormAttribute and FromBodyAttribute. You can read the code and see where they're fit in. So you can write your own attribute to use instead. I prefer to using existing classes. However in this case, there is an important note about the order of FromFormAttribute and FromBodyAttribute. The FromFormAttribute should be placed before FromBodyAttribute. Per my test, looks like the ASP.NET Core model binding takes the first attribute as effective (seemingly ignores the others), so if FromBodyAttribute is placed first, it will take effect and may prevent all the model binders (including our custom one) from running when the content-type is not supported (returning 415 response).
The final note about this solution, it's not perfect. Once you accept to bind model from multi-sources like this, it will not handle the case of not supporting media type nicely as when using FromBodyAttribute explicitly. Because when we support multi-source model binding, the FromBodyAttribute is not used and the default check is not kicked in. We must implement that ourselves. Because of multiple binders joining in the binding process, we cannot easily decide when the request becomes not supported (so even you add a check in the FormBodyModelBinder, you can successfully set the response's status code to 415 but it may not be right and the request is still being processed in the selected action method). With this note, in case of media type not supported, you should ensure that your code in the action method is not broken by handling empty (or null) model argument.
Here is how you use the custom model binder:
[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> Register([FromForm] [FromBody] RegisterViewModel model, string returnUrl = null) { }

How to Test if Parameters from URL Route get Binded to an object correctly? (C# controller)

So far I tried searching for an answer, what I found was something query string related, custom model binders or something with the viewbag (irrelevant). Being still pretty new to C# and not very familiar with its internal workings such as this, so I don't feel I can make sense of the answers or translate them into this situation. (Like this) With this question I'm hoping to find an answer with comment or a few of explanation.
The situation:
I want to test if the model binding attributes are configured correctly. Main motivation is TDD, a specific configuration is required and needs to be test driven. Another motivation is that if someone messes with it and breaks it, the test will immediately tell where the problem is.
Example Controller with a single endpoint (the route has to be that way):
[ApiController]
[Route("/cats")]
public class CatsController : ControllerBase
{
[HttpGet]
[Route("cat-breed/{cat_breed}/cat-size/{cat_size}")]
public IActionResult GetMultipleCats([FromRoute] GetCatsRequestObject request)
{
//implementation is not important
return Ok();
}
}
A request object to encapsulate input fields (one of our standards):
public class GetCatsRequestObject
{
[FromRoute(Name = "cat_breed")] public string CatBreed { get; set; }
[FromRoute(Name = "cat_size")] public string CatSize { get; set; }
}
What I'm looking for is the test, where I somehow do a call with the url to whatever is doing model binding, and then in the controller I check if the values from the url are in the correct places in/and in my request object. Something like this:
private CatsController _controllerUnderTest = new CatsController();
[Test]
public void GivenSomeUrl_WhenControllerIsCalledWithIt_ThenParametersFromUrlGetBindedCorrectly()
{
//arrange
string urlpart = "/cats/cat-breed/British%20Shorthair/cat-size/Medium%20Large";
GetCatsRequestObject expectedResult = new GetCatsRequestObject() { CatBreed = "British Shorthair", CatSize = "Medium Large" };
// The rest of the setup I don't know how to do
//act
// somehow do a call with the url, rather than request object.
//assert
// somehow capture the model binding result and compare it with expected result.
}
Is this possible to test? How would you do this? I realize this is a lengthy post, so I thank you for taking your time to read this.

Getting 415 Unsupported media type when executing from address line in browser provifing JSON as parameter for route in .NET Core 3

I'm executing the URL
https://localhost:44310/api/Licensee/{"name":"stan"}
in address field of my browser, getting the error
"title": "Unsupported Media Type", "status": 415
which is described as
... the origin server is refusing to service the request because the payload
is in a format not supported by this method on the target resource.
The suggested troubleshot is
... due to the request's indicated Content-Type or Content-Encoding, or as a result of inspecting the data ...
I can't really control what header the browser provides. Due to intended usage, I can't rely on Postman or a web application. It needs to be exected from the URL line. The parameter will differ in structure, depending what search criteria that are applied.
The controller looks like this.
[HttpGet("{parameters}")]
public async Task<ActionResult> GetLicensee(LicenseeParameters parameters)
{
return Ok(await Licensee.GetLicenseeByParameters(parameters));
}
I considered decorating the controller with [Consumes("application/json")] but found something dicouraging it. I tried to add JSON converter as suggested here and here but couldn't really work out what option to set, fumbling according to this, not sure if I'm barking up the right tree to begin with.
services.AddControllers()
.AddJsonOptions(_ =>
{
_.JsonSerializerOptions.AllowTrailingCommas = true;
_.JsonSerializerOptions.PropertyNamingPolicy = null;
_.JsonSerializerOptions.DictionaryKeyPolicy = null;
_.JsonSerializerOptions.PropertyNameCaseInsensitive = false;
});
My backup option is to use query string specifying the desired options for a particular search. However, I'd prefer to use the object with parameters for now.
How can I resolve this (or at least troubleshoot further)?
The reason is that there might be a loooot of parameters and I don't want to refactor the controller's signature each time
Actually, you don't have to change the controller's signature each time. ASP.NET Core Model binder is able to bind an object from query string automatically. For example, assume you have a simple controller:
[HttpGet("/api/licensee")]
public IActionResult GetLicensee([FromQuery]LicenseeParameters parameters)
{
return Json(parameters);
}
The first time the DTO is:
public class LicenseeParameters
{
public string Name {get;set;}
public string Note {get;set;}
}
What you need is to send a HTTP Request as below:
GET /api/licensee?name=stan&note=it+works
And later you decide to change the LicenseeParameters:
public class LicenseeParameters
{
public string Name {get;set;}
public string Note {get;set;}
public List<SubNode> Children{get;set;} // a complex array
}
You don't have to change the controller signature. Just send a payload in this way:
GET /api/licensee?name=stan&note=it+works&children[0].nodeName=it&children[1].nodeName=minus
The conversion is : . represents property and [] represents collection or dictionary.
In case you do want to send a json string within URL, what you need is to create a custom model binder.
internal class LicenseeParametersModelBinder : IModelBinder
{
private readonly JsonSerializerOptions _jsonOpts;
public LicenseeParametersModelBinder(IOptions<JsonSerializerOptions> jsonOpts)
{
this._jsonOpts = jsonOpts.Value;
}
public Task BindModelAsync(ModelBindingContext bindingContext)
{
var name= bindingContext.FieldName;
var type = bindingContext.ModelType;
try{
var json= bindingContext.ValueProvider.GetValue(name).FirstValue;
var obj = JsonSerializer.Deserialize(json,type, _jsonOpts);
bindingContext.Result = ModelBindingResult.Success(obj);
}
catch (JsonException ex){
bindingContext.ModelState.AddModelError(name,$"{ex.Message}");
}
return Task.CompletedTask;
}
}
and register the model binder as below:
[HttpGet("/api/licensee/{parameters}")]
public IActionResult GetLicensee2([ModelBinder(typeof(LicenseeParametersModelBinder))]LicenseeParameters parameters)
{
return Json(parameters);
}
Finally, you can send a json within URL(suppose the property name is case insensive):
GET /api/licensee/{"name":"stan","note":"it works","children":[{"nodeName":"it"},{"nodeName":"minus"}]}
The above two approaches both works for me. But personally I would suggest you use the the first one as it is a built-in feature.

Use FromQuery model binder in output formatter

I have a custom OutputFormatter in my .net core project. In there I want to use some info inside the querystring of the initial request.
Inside the controller this is nicely done with the FromQuery modelbinder, giving me an object to work with. I would like to have this object (model) in my output formatter as well.
Can I somehow call FromQuery as an instance or such, so I can pass in the HttpContext or the querystring even, to get the model?
public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context)
{
// Want a model from my querystring here
}
Use HttpContext.Items. The objects placed in there will be cleaned up at the end of request. And you can pass even more with it, defaulted values or changed values (probably it can be a dangerous point if you are going to change binded object)
// GET api/values
[HttpGet]
public ActionResult Get([FromQuery] QData data)
{
HttpContext.Items["data"] = data;
.......
return Ok(....);
}
Also you can have multiple formatters for different types of requests.
public class Formatter : OutputFormatter
{
public override bool CanWriteResult(OutputFormatterCanWriteContext context)
{
return context.HttpContext.Items["data"] is QData;
}
public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context)
{
var incoming = context.HttpContext.Items["data"] as QData;
.......
}
}
The same way you can put any other object in Items and work with it in formatter. In case of other object it can be more generic and stable solution as it relies on specific structure.

Categories

Resources