I'm struggling to add OData v4 query support to a method on a controller that inherits from ApiController rather than ODataController. While I have a working OData model in the solution there are some endpoints that don't really belong in the model but the power of the query would be useful.
I've seen some articles suggesting I can just return an IQueryable and use EnableQuery.
Here is my sample code:
public class TestController : ApiController
{
[HttpGet]
[EnableQuery]
public IQueryable<TestObject> Events()
{
var result = new[] { new TestObject { Id = "1" } }.AsQueryable();
return result;
}
}
public class TestObject
{
public string Id { get; set; }
}
All I get back when a call /Test/Events is a 406 Not Acceptable, something that I've run into a lot dealing with OData that usually means I've return something the OData framework doesn't like. I've never been able to get anymore info out of the framework (a major failing I think) as to why it doesn't like it and only solved them by trial and error in the past.
Has anyone got this setup working and if so how?
Or any suggestions on debugging the 406 response?
EDIT:
OK so it turned out the culprit was so code in the start up that was registering a custom ODataMediaTypeFormatter and in the process was clearing all other formatters.
After removing the offending code it worked.
Really really wish WebApi would record somewhere what caused the 406 error when it spits one out.
You can apply the "query" magic manually, without [EnableQuery].
Create a ODataQueryContext, passing in your model (you'll need to
save it in global and make it available), and the entity type your are
querying against.
Create an HttpRequestMessage using Get and the current url
(Request.RequestUri.AbsoluteUri).
Create a new ODataQueryOptions<yourEntity>, passing in the context and
request message you created.
Call that object's ApplyTo using your entity.AsQueryable(), casting
the result as an IQueryable<yourEntity>.
Further manipulate needed and return. Keep in mind that return type does not need to be IQueryable<yourEntity>, or even based upon "yourEntity".
Here is a similar implementation that does this for an ODataController method ( as a base ODataQueryOptions comes for free):
public IEnumerable<Aggregate> GetOrders(ODataQueryOptions<ReportingOrder> opts, [FromUri(Name="$groupby")]string groupby, [FromUri(Name="$aggregates")]string aggregates = null)
{
var url = opts.Request.RequestUri.AbsoluteUri;
int? top = null;
if (opts.Top != null)
{
top = int.Parse(opts.Top.RawValue);
var topStr = string.Format("$top={0}", top.Value);
url = url.Replace(topStr, "");
var req = new HttpRequestMessage(HttpMethod.Get, url);
opts = new ODataQueryOptions<ReportingOrder>(opts.Context, req);
}
var query = opts.ApplyTo(db.ReportingOrders.AsQueryable()) as IQueryable<ReportingOrder>;
var results = query.GroupBy(groupby, aggregates, top);
return results;
}
Related
firstly I can get around this issue by creating multiple API controllers in my web application. However, I wondered if it is possible to do the following:
I am trying to have two PUT methods in my API controller and just attempt to call them by api//Method-name/id using POSTMAN, I cannot get a response however when using what I assume would be the route for the HTTP put request. I have done some research and I'm not sure if this is possible or if the information is too outdated but I have found some information linking to a need of adding some routing code to my web application config? But I don't know the correct way to put this in the startup.cs file as there do not seem to be methods for Mapping HTTP routes?
Below is my code: Please note I'm not sure what other detail to add other than the controller and the POSTMAN url im trying (https://MY.I.P:44388/api/Data/Reject/1143) please let me know if I require more.
public class DataController : ControllerBase
{
// PUT api/<DataController>/Accept
[HttpPut("{id}")]
public void Accept(int id)
{
try
{
using (RecoDBEntities Entities = new RecoDBEntities())
{
List<recovery_jobs> entity = new List<recovery_jobs>();
entity = Entities.recovery_jobs.Where(e => e.DOCKETNO.ToString().Trim() == id.ToString().Trim()).ToList();
recovery_jobs job = entity[0];
job.STATUS = "ONROUTE";
Entities.SaveChanges();
}
}
catch (System.Exception)
{
throw;
}
}
// PUT api/<DataController>/Reject
[HttpPut("{id}")]
public void Reject(int id)
{
try
{
using (RecoDBEntities Entities = new RecoDBEntities())
{
List<recovery_jobs> entity = new List<recovery_jobs>();
entity = Entities.recovery_jobs.Where(e => e.DOCKETNO.ToString().Trim() == id.ToString().Trim()).ToList();
recovery_jobs job = entity[0];
job.STATUS = "REJECTED";
Entities.SaveChanges();
}
}
catch (System.Exception)
{
throw;
}
}
// DELETE api/<DataController>/5
[HttpDelete("{id}")]
public void Delete(int id)
{
}
}
The error I receive in POSTMAN is a 404
Moving comment to answer:
Not by method name alone but you can do it by the route parameter. E.g.
[Route(“accept/{id})]
Also, as mentioned by Dan, you should still use the HttpPut attribute along with this.
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) { }
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¬e=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¬e=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.
I am trying to post to a Web API method from a client, as follows:
// Create list of messages that will be sent
IEnumerable<IMessageApiEntity> messages = new List<IMessageApiEntity>();
// Add messages to the list here.
// They are all different types that implement the IMessageApiEntity interface.
// Create http client
HttpClient client = new HttpClient {BaseAddress = new Uri(ConfigurationManager.AppSettings["WebApiUrl"])};
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
// Post to web api
HttpResponseMessage response = client.PostAsJsonAsync("Communications/Messages", messages).Result;
// Read results
IEnumerable<ApiResponse<IMessageApiEntity>> results = response.Content.ReadAsAsync<IEnumerable<ApiResponse<IMessageApiEntity>>>().Result;
My Web API controller action looks like this:
public HttpResponseMessage Post([FromBody]IEnumerable<IMessageApiEntity> messages)
{
// Do stuff
}
The problem I am having is that messages is always empty (but not null) when coming into the web API controller action. I have verified in the debugger that the messages object on the client side does have items in it right before being posted.
I suspect it might have something to do with the interface type not being converted to a concrete type when trying to pass the objects, but I don't know how to go about making it work. How can I achieve this?
I figured out how to do it without a custom model binder. Posting the answer in case anyone else has this issue...
Client:
// Create list of messages that will be sent
IEnumerable<IMessageApiEntity> messages = new List<IMessageApiEntity>();
// Add messages to the list here.
// They are all different types that implement the IMessageApiEntity interface.
// Create http client
HttpClient client = new HttpClient {BaseAddress = new Uri(ConfigurationManager.AppSettings["WebApiUrl"])};
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
// Post to web api (this is the part that changed)
JsonMediaTypeFormatter json = new JsonMediaTypeFormatter
{
SerializerSettings =
{
TypeNameHandling = TypeNameHandling.All
}
};
HttpResponseMessage response = client.PostAsync("Communications/Messages", messages, json).Result;
// Read results
IEnumerable<ApiResponse<IMessageApiEntity>> results = response.Content.ReadAsAsync<IEnumerable<ApiResponse<IMessageApiEntity>>>().Result;
Add to Register method in WebApiConfig.cs:
config.Formatters.JsonFormatter.SerializerSettings.TypeNameHandling = TypeNameHandling.Auto;
The key is to send the type as part of the json and turn on automatic type name handling, so that web API can figure out what type it is.
Why you use interface type in method? Looks like web API, has no idea what kind of instance should be used for materialize messages argument. Seems that you have to write custom model binder for this action.
I had a similar problem a few weeks ago with .NET Core WebAPI.
The proposed solution of adding the line below did not work for me:
config.Formatters.JsonFormatter.SerializerSettings.TypeNameHandling = TypeNameHandling.Auto;
I ended up creating a generic object that can carry my IEnumerable, where T is my intended class
[Serializable]
public class GenericListContainer<T> where T : class
{
#region Constructors
public GenericListContainer()
{
}
public GenericListContainer(IEnumerable<T> list)
{
List = list;
}
#endregion
#region Properties
public IEnumerable<T> List { get; set; }
#endregion
}
Then I changed my webapi method to :
[Route("api/system-users/save-collection-async")]
[HttpPost]
[ProducesResponseType(typeof(string), 200)]
public async Task<IActionResult> SaveSystemUserCollectionAsync([FromBody] GenericListContainer<SystemUser> dto)
{
var response = await _systemUserService.SaveSystemUserCollectionAsync(dto.List);
return Ok(response);
}
This method returns the saved user's id (Guid in my case).
Hope this helps someone else!
Here is my issue. I am using ASP.NET Web API 2.0 and the QueryableAttribute to take advantage of some of the OData filtering functionality.
public class VideoController : ApiController
{
[HttpGet]
[Route("activevideos")]
[Queryable]
public IEnumerable<Video> GetActiveVideos(ODataQueryOptions<Video> options)
{
return new GetvContext().Videos.Where(c => c.IsActive);
}
}
Now, I have a class that I have been using to modify the response object and contained entities. This was working fine before I started using the QueryableAttribute. Before this, I was returning a List from the previous method instead of IEnumerable.
public class TestMessageProcessHandler : MessageProcessingHandler
{
protected override HttpResponseMessage ProcessResponse(
HttpResponseMessage response, CancellationToken cancellationToken)
{
var content = ((ObjectContent)(response.Content)).Value;
// Do something here with the content. This used to be a List<Video>
// but now the object it is of type:
// System.Data.Entity.Infrastructure.DbQuery<System.Web.Http.OData.Query.Expressions.SelectExpandBinder.SelectSome<Content.Api.Video>>
}
}
I need to be able to get the entity from this and I am not sure how to get from type:
System.Data.Entity.Infrastructure.DbQuery<System.Web.Http.OData.Query.Expressions.SelectExpandBinder.SelectSome<Content.Api.Video>> to something like List<Video> so I can modify the Video object.
Remove the [Queryable] attribute and manage the querying of the data yourself - something like this:
public class VideoController : ApiController
{
[HttpGet]
[Route("activevideos")]
public IList<Video> GetActiveVideos(ODataQueryOptions<Video> options)
{
var s = new ODataQuerySettings() { PageSize = 1 };
var result = options.ApplyTo(
new GetvContext().Videos.Where(c => c.IsActive), s)
.ToList();
return result;
}
}
Can't you just do a ToList() - the DbQuery implements IQueryable...
e.g.
var content = ((ObjectContent)(response.Content)).Value;
var queryable = content as IQueryable<Video>;
var list = queryable.ToList();
Most probably you are getting this problem because you specified which fields should be selected by your OData query (for example, $select=Nr,Date). If you won't do this, after applying query options OData won't return a SelectSome objects, it will return the same objects you are querying, in your case Video. So you can easily modify these objects server-side before returning them to client.
And if you need to exclude properties from Video object which you don't need, instead of $select query option you can use a DTO instead of your Video object and include in this DTO only those fields you need.
Yes, this is a workaround, and hopefully there will be better means to do deal with server-side logic in the future versions of this library