symfony's paramconverter equivalent in ASP.NET Web API - c#

I'm pretty new in .Net community, please show mercy haha. I got two questions:
is there any similar implementation of symfony's #paramconverter annotation in ASP.NET (such as a converter attributes)?
e.g. the request url, no matter GET/POST/PUT/DELETE, the api request with an id of certain entity and we are able to convert/bind such id into a entity object(say productId in int and convert to Product object)
Expected PseudoCode:
[Route("products/{productId}/comments")]
[ProductConverter]
public HttpResponseMessage getProductComments(Product product) {
// product here not only with `productId` field, but already converted/bind into `Product`, through repository/data store
....
}
Is this good practise in .Net? I ask for this implementation because I think this pattern able to reduce duplicate code as API requests mainly rely on object id. I can even throw an exception if such Product object not found by such string.

It looks like ModelBinder is equiuvalent to symphony's parmconverter. You can read more about it here: http://www.asp.net/web-api/overview/formats-and-model-binding/parameter-binding-in-aspnet-web-api
Here is sample:
First you have to implement IModelBinder. That's very basic implementation of it:
public class ProductBinder : IModelBinder
{
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
if (bindingContext.ModelType != typeof(Product))
{
return false;
}
var id = (int)bindingContext.ValueProvider.GetValue("productId").ConvertTo(typeof(int));
// Create instance of your object
bindingContext.Model = new Product { Id = id };
return true;
}
}
Next you have to configure ASP.NET WebApi to use that binder. In WebApiConfig.cs file (or any other where you configure WebAPI) add the following line:
config.BindParameter(typeof(Product), new ProductBinder());
The final step is to create correct controller method. It's important to provide correct route parameter in order to correct binding.
[Route("products/{productId}/comments")]
public HttpResponseMessage getProductComments(Product product) {
}
I don't think that this is bad practice or good practices. As always it depends. For sure it reduce code duplication and introduce some order to your code. If the object with that id doesn't exists you can even try tweak this binder to return response 404 (not found)
Edit:
Using IModelBinder with dependency injection is a bit tricky but still possible. You have to write additional extension method:
public static void BindParameter(this HttpConfiguration config, Type type, Type binderType)
{
config.Services.Insert(typeof(ModelBinderProvider), 0, new SimpleModelBinderProvider(type, () => (IModelBinder)config.DependencyResolver.GetService(binderType)));
config.ParameterBindingRules.Insert(0, type, param => param.BindWithModelBinding());
}
It's based on orginal method found there. The only difference is that it expect type of the binder instead of instance. So you just call:
config.BindParameter(typeof(Product), typeof(ProductBinder));

Related

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 Can I Send Data to the Action Filter after Execution of the controller in ASP.NET MVC?

The following is my action in the controller. I need to pass ProductId and Quantity to the filter SessionCheck. Is there any other way besides TempData?
[SessionCheck]
[HttpPost]
public ActionResult AddToCart(int ProductId, int Quantity)
{
try
{
//my code here
return RedirectToAction("Cart");
}
catch (Exception error)
{
throw error;
}
}
And the following is my action filter:
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
// my code here
}
Good question. So far as I'm aware, it's not possible to call the parameters directly from your filter—even though you can retrieve metadata about them via the ActionDescriptor.GetParameters() method.
Source Values
You can, however, access these values directly from their source collection using either RequestContext.RouteData, or the RequestContext.HttpContext’s Request property, which can retrieve data from the Form, QueryString, or other request collections. All of these are properties off of the ActionExecutedContext.
Example
So, for example, if your values are being retrieved from the form collection—which I assume might be the case, since this is an [HttpPost] action—your code might look something like:
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
var request = filterContext.RequestContext.HttpContext.Request;
Int32.TryParse(request.Form.Get("ProductId"), out var productId);
Int32.TryParse(request.Form.Get("Quantity"), out var quantity);
}
Validation
Keep in mind that, technically, your ActionFilterAttribute can be applied to any number of actions, so you should be aware of that and not assume that these parameters will be available. You can use the ActionDescriptor.ActionName property to validate the context, if necessary:
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
if (filterContext.ActionDescriptor.ActionName.Equals(nameof(MyController.AddToCart))
{
//Retrieve values
}
}
Or, alternatively, you can use the ActionDescriptor.GetParameters() method mentioned above to simply evaluate if the parameters exist, regardless of what the name of the action is.
Caveats
There are some limitations to this approach. Most notably:
It won't be able to retrieve the values of internal calls to the action, and
It won't execute any model binding, which could be an issue for more sophisticated objects.
Other Frameworks
You specify ASP.NET MVC. For anyone reading this who's using ASP.NET Core, the class libraries are a bit different, and offer some additional functionality (such as TryGetValue() for calls to the HttpRequest methods). In addition, it also provides access to the BoundProperties collection, which may provide additional options—though I haven't dug into those data yet to confirm.

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.

How do I retrieve body values from an HTTP POST request in an ASP.NET Web API ValueProvider?

I want to send a HTTP POST request with the body containing information that makes up a simple blog post, nothing fancy.
I've read here that when you want to bind a complex type (i.e. a type that is not string, int etc) in Web API, a good approach is to create a custom model binder.
I have a custom model binder (BlogPostModelBinder) that in turn uses a custom Value Provider (BlogPostValueProvider). What I don't understand is that how and where shall I be able to retrieve the data from the request body in the BlogPostValueProvider?
Inside the model binder this is what I thought would be the right way to for example retrieve the title.
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
...
var title= bindingContext.ValueProvider.GetValue("Title");
...
}
while the BlogPostValueProvider looks like this:
public class BlogPostValueProvider : IValueProvider
{
public BlogPostValueProvider(HttpActionContext actionContext)
{
// I can find request header information in the actionContext, but not the body.
}
public ValueProviderResult GetValue(string key)
{
// In some way return the value from the body with the given key.
}
}
This might be solvable in an easier way, but since i'm exploring Web API it would be nice to get it to work.
My problem is simply that I cannot find where the request body is stored.
Thanks for any guidance!
Here's a blog post about this from Rick Strahl. His post almost answers your question. To adapt his code to your needs, you would do the following.
Within your value provider's constructor, read the request body like this.
Task<string> content = actionContext.Request.Content.ReadAsStringAsync();
string body = content.Result;
I needed to do this in an ActionFilterAttribute for logging, and the solution I found was to use the ActionArguments in the actionContext as in
public class ExternalApiTraceAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
...
var externalApiAudit = new ExternalApiAudit()
{
Method = actionContext.Request.Method.ToString(),
RequestPath = actionContext.Request.RequestUri.AbsolutePath,
IpAddresss = ((HttpContextWrapper)actionContext.Request.Properties["MS_HttpContext"]).Request.UserHostAddress,
DateOccurred = DateTime.UtcNow,
Arguments = Serialize(actionContext.ActionArguments)
};
To build on Alex's answer, after you get the Raw body, you can then use Newtonsoft to deserealize it (without doing any of that complicated stuff in the blog post):
Task<string> content = actionContext.Request.Content.ReadAsStringAsync();
string body = content.Result;
var deserializedObject = JsonConvert.DeserializeObject<YourClassHere>(body.ToString());
https://www.newtonsoft.com/json/help/html/deserializeobject.htm

Categories

Resources