WebApi
WebApi copied to clipboard
Default Attribute Routing for unbound functions isn't working.
Unless I am doing something wrong, Default Attribute Routing for unbound functions isn't working.
Assemblies affected
Microsoft.AspNetCore.OData (7.4.1)
Reproduce steps
- Specify an unbound function in your Edm
public class FooModelBuilder : ODataConventionModelBuilder
{
public PropertyModelBuilder(IServiceProvider serviceProvider)
: base(serviceProvider) { }
public override IEdmModel GetEdmModel()
{
Namespace = "Bar";
// Add a global function
var funcSayAnything = this.Function("SayAnything");
funcSayAnything.Namespace = "Bar";
funcSayAnything.Parameter<string>("value");
funcSayAnything.Parameter<int>("repeat");
funcSayAnything.Returns<IActionResult>();
return base.GetEdmModel();
}
}
- Create a controller class with appropriate method and Attribute
public class GlobalFunctionsController : ODataController
{
// THIS DOESN'T WORK, BUT AT LEAST NO ERRORS OCCUR. You just get a 404 Error from the
// browser/Postman.
[ODataRoute("SayAnything", RouteName = "odata")]
// THIS DOESN'T WORK EITHER. The moment you run the application, it produces this error:
// ~
// InvalidOperationException: The path template 'SayAnything({value},{repeat})' on the action
// 'SayAnything' in controller 'GlobalFunctions' is not a valid OData path template. The
// request URI is not valid. The segment 'SayAnything' cannot include key predicates, however
// it may end with empty parenthesis.
// ~
// [ODataRoute("SayAnything({value},{repeat})", RouteName = "odata")]
// Same error occurs. Thought I'd try it though since, in spite of all the samples people publish
// online, parameters to resource endpoints that return an entity by key value must be
// named "keySomeId"
// [ODataRoute("SayAnything({keyValue},{keyRepeat})", RouteName = "odata")]
[HttpGet]
public IActionResult SayAnything([FromODataUri] string value, [FromODataUri] int repeat)
{
// Breakpoint anywhere. Function never gets called.
string result = string.Join(" ... ", Enumerable.Range(1, repeat).Select(i => value));
return Content(result, "text/plain; charset=utf-8");
}
}
- Configure routing and Edm use in Startup:
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapODataRoute("odata", "odata",
b =>
{
b.AddService(Microsoft.OData.ServiceLifetime.Singleton,
sp => new FooModelBuilder(app.ApplicationServices).GetEdmModel());
b.AddService<ODataDeserializerProvider>(Microsoft.OData.ServiceLifetime.Singleton,
sp => new EntityReferenceODataDeserializerProvider(sp));
b.AddService<IEnumerable<IODataRoutingConvention>>(Microsoft.OData.ServiceLifetime.Singleton,
sp => ODataRoutingConventions.CreateDefaultWithAttributeRouting("odata", endpoints.ServiceProvider));
});
});
}
Expected result
A request to the following endpoint should return something: (https://localhost:44308/odata/Bar.SayAnything('Bueller?',4))
I also tried (https://localhost:44308/odata/SayAnything('Bueller?', 4))
Neither Uri works.
Actual result
The controller method is never called (break point is never hit) and the API returns a 404 Not Found error.
@mitselplik
Some hints maybe help you:
-
Doesn't config the function to return the IActionResult. you can return any built-in type. for example
funcSayAnything.Returns<string>();
-
functionimport or unbound function call, the syntax is that : To invoke a function through a function import the client issues a GET request to a URL identifying the function import and passing parameter values using inline parameter syntax. The canonical URL for a function import is the service root, followed by the name of the function import. So, you don't need to call an unbound with the namespace.
-
The attribute on the method should same as the function import call. Below is an example:Be noted, the parameter is key={parameterName}.
[HttpGet]
[ODataRoute("SayAnything(value={anyValueNameHere},repeat={anyRepeatNameHere})")]
public IActionResult SayAnything([FromODataUri] string anyValueNameHere, [FromODataUri] int anyRepeatNameHere)
{
// Breakpoint anywhere. Function never gets called.
string result = string.Join(" ... ", Enumerable.Range(1, anyRepeatNameHere).Select(i => anyValueNameHere));
return Content(result, "text/plain; charset=utf-8");
}
- The parameter
value
string used in your example has a question mark, '?', you should escape it in the request path.
There's a blog for your reference: https://devblogs.microsoft.com/odata/tutorial-sample-functions-actions-in-web-api-v2-2-for-odata-v4-0-type-scenario/
Hey @xuzhg, I read through this post and produced a github repo to reproduce the routing returns 404. I'm really interested in applying a bound function to AspNetCore 3.1 with endpoint routing enabled.
The blog reference https://devblogs.microsoft.com/odata/tutorial-sample-functions-actions-in-web-api-v2-2-for-odata-v4-0-type-scenario/ makes sense for Web Api 2 / MVC.
I've also looked at the Sample for 3.1 AspNetCore for the controller.
How does the routing work in the sample? The only hint I can see is here:
b.AddService<IEnumerable<IODataRoutingConvention>>(Microsoft.OData.ServiceLifetime.Singleton,
sp => ODataRoutingConventions.CreateDefaultWithAttributeRouting("nullPrefix", endpoints.ServiceProvider));
Local debugging AspNetCore3xEndpointSample
I've tried running the AspNetCore3xEndpointSample locally but I cannot invoke a function based on how to invoke an unbound function blog.
Based on the Endpoint sample, I would expected http://localhost:5000/Default.CalcByRating(name='b')
to return 200 OK, not 404 actual response.
Trial and Error Test Results
After multiple code revisions on the AspNetCore3xEndpointSample, Applying [ODataRoute("CalcByRating(name={name})")]
on the controller enabled correct routing. I could request at url: http://localhost:5000/CalcByRating(name='b')
But I believe the correct odata url should be : http://localhost:5000/Default.CalcByRating(name='b')
, This does not work with [ODataRoute("Default.CalcByRating(name={name})")]
, an ODataUnrecognizedPathException is thrown using console application debugging.
Exception thrown: 'Microsoft.OData.UriParser.ODataUnrecognizedPathException' in Microsoft.AspNetCore.OData.dll ("Resource not found for the segment 'Default.CalcByRating'.") | Microsoft.OData.UriParser.ODataUnrecognizedPathException
Summary Asks
- Can you explain how to use the convention routing without
[ODataRoute("CalcByRating(name={name})")]
added? - Given configured IEdmModel with multiple parameters with
IsOptional = true
, is it possible to provide the controller action a single dictionaryODataActionParameters parameters
without explicitly naming parameters with:[ODataRoute("CalcByRating(name={name})")]
?
@stack111
Same again, how to invoke a function depends on the function type.
- If it's bound function, it can call from the bound resource using qualified or unqualified function name with the arguments.
- If it's unbound function, it can call from the service root directly using the unbound function name.
So, http://localhost:5000/Default.CalcByRating(name='b')
doesn't make sense, because from OData, there's no URL convention starting with a namespace function call.
For your question:
-
Can you explain how to use the convention routing without [ODataRoute("CalcByRating(name={name})")] added?
Build the
CalcByRating
as bound function, for example: https://github.com/OData/AspNetCoreOData/blob/master/sample/ODataRoutingSample/Models/EdmModelBuilder.cs#L20 where,GetWholeSalary
is bound to COLLECTION of Product. In order to invoke this function, you should have:- A controller named by an entity set name whose type is "Product". For example: ProductsController
- A action whose name is same as the "GetWholeSalary" with [HttpGet], and with correct parameters. See https://github.com/OData/AspNetCoreOData/blob/master/sample/ODataRoutingSample/Controllers/ProductsController.cs#L131
To invoke the function, you can:
GET ~/odata/Products/Namespace.GetWholeSalary(maxSalary=2,minSalary=2...) GET ~/odata/Products/GetWholeSalary(maxSalary=2,minSalary=2...)
- Given configured IEdmModel with multiple parameters with IsOptional = true, is it possible to provide the controller action a single dictionary ODataActionParameters parameters without explicitly naming parameters with: [ODataRoute("CalcByRating(name={name})")]?
You can config the method as "Edm action". just call. To invoke an action:
- HttpPost method
- the http request uri only contains the action name
- put all arguments into the request body as JSON
- the method definition in the controller is same as the above for the function.
- the method parameter now can use
ODataActionParameters
to hold all arguments.
Thank you @xuzhg, I appreciate writing the detailed explanation, this certain helps me understand OData configuration better.
To summarize, does this mean an unbound action always requires an explicit route on the controller method? This is the only way I could get an unbound action to work and be recognized as an OData path in 8.x.
[HttpPost($"/{EntityDataModel.RoutePrefix}/{nameof(UnboundAction)}")]
public IActionResult UnboundAction(ODataActionParameters op)
{
return Ok();
}
Closing this issue due to inactivity. If this issue still persists, feel free to create a new issue.