I am new to this so i will start with the code and after that i will explain.
The problem is this
[HttpGet, ODataRoute("({key})")]
public SingleResult<Employee> GetByKey([FromODataUri] string key)
{
var result = EmployeesHolder.Employees.Where(id => id.Name == key).AsQueryable();
return SingleResult<Employee>.Create<Employee>(result);
}
[HttpGet, ODataRoute("({key})")]
public SingleResult<Employee> Get([FromODataUri] int key)
{
var result = EmployeesHolder.Employees.Where(id => id.Id == key).AsQueryable();
return SingleResult<Employee>.Create<Employee>(result);
}
I have those 2 actions but for one i want to search by a string and for the other by number (although this is not the problem). If i leave it this way it will work for the (int) case but for the string "....odata/Employees('someName')" i will get a : HTTP 404 (and it's normal) but if i try to be more specific with the method which takes a string
Code in webApiConfig.
ODataModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<Employee>("Employees");
builder.Function("GetByKeyFromConfig").Returns<SingleResult<Employee>>().Parameter<string>("Key");
Code in Controller
[ODataRoutePrefix("Employees")]
public class FooController : ODataController
{
[HttpGet, ODataRoute("GetByKeyFromConfig(Key={key})")]
public SingleResult<Employee> GetByKey([FromODataUri] string key)
{ ... }
}
i get an expcetion
{"The complex type
'System.Web.Http.SingleResult`1[[OData_Path.Employee, OData_Path,
Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]' refers to the
entity type 'OData_Path.Employee' through the property 'Queryable'."}
If i change the return type in for the method in WebApiConfig
builder.Function("GetByKeyFromConfig").Returns<Employee>().Parameter<string>("Key");
I get this which i have no idea why.
{"The path template 'Employees/GetByKeyFromConfig(Key={key})' on the
action 'GetByKey' in controller 'Foo' is not a valid OData path
template. The request URI is not valid. Since the segment 'Employees'
refers to a collection, this must be the last segment in the request
URI or it must be followed by an function or action that can be bound
to it otherwise all intermediate segments must refer to a single
resource."}
I have searched and tried to get explanation , but each time i found an answer it does not work. i am struggling for days.
After the updates taken from the 2 answers
still have the Invalid OData path template exception
WebApiConfig Code
ODataModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<Employee>("Employees").EntityType.HasKey(p => p.Name);
var employeeType = builder.EntityType<Employee>();
employeeType.Collection.Function("GetByKey").Returns<Employee>().Parameter<int>("Key");
config.EnableUnqualifiedNameCall(unqualifiedNameCall: true);
config.MapODataServiceRoute(
routeName: "ODataRoute",
routePrefix: null,
model: builder.GetEdmModel());
Controller Code
[EnableQuery, HttpGet, ODataRoute("Employees/GetByKey(Key={Key})")]
public SingleResult<Employee> GetByKey([FromODataUri] int Key)
{
var single = Employees.Where(n => n.Id == Key).AsQueryable();
return SingleResult<Employee>.Create<Employee>(single);
}
I've also tried using a specific Namespace
builder.Namespace = "NamespaceX";
[EnableQuery, HttpGet, ODataRoute("Employees/NamespaceX.GetByKey(Key={Key})")]
And
[EnableQuery, HttpGet, ODataRoute("Employees/NamespaceX.GetByKey")]
While you can solve your problem with OData functions, a cleaner solution would be to use alternate keys. As Fan indicated, Web API OData provides an implementation of alternate keys that will allow you to request Employees by name or number with a more straightforward syntax:
GET /Employees(123)
GET /Employees(Name='Fred')
You will need to add the following code to your OData configuration.
using Microsoft.OData.Edm;
using Microsoft.OData.Edm.Library;
// config is an instance of HttpConfiguration
config.EnableAlternateKeys(true);
// builder is an instance of ODataConventionModelBuilder
var edmModel = builder.GetEdmModel() as EdmModel;
var employeeType = edmModel.FindDeclaredType(typeof(Employee).FullName) as IEdmEntityType;
var employeeNameProp = employeeType.FindProperty("Name");
edmModel.AddAlternateKeyAnnotation(employeeType, new Dictionary<string, IEdmProperty> { { "Name", employeeNameProp } });
Make sure you add the alternate key annotations before you pass the model to config.MapODataServiceRoute.
In your controller, add a method to retrieve Employees by name (very similar to the GetByKey method in your question).
[HttpGet]
[ODataRoute("Employees(Name={name})")]
public IHttpActionResult GetEmployeeByName([FromODataUri] string name)
{
var result = EmployeesHolder.Employees.FirstOrDefault(e => e.Name == name);
if (result == null)
{
return this.NotFound();
}
return this.Ok(result);
}
Can this document about function support for OData/Webapi help you? http://odata.github.io/WebApi/#04-06-function-parameter-support there is some problem in your second approach.
you are call with Employees/GetByKeyFromConfig(Key={key}) so you should declare the function like:
builder.EntityType<Employee>().Collection.Function("GetByKeyFromConfig")
you should call with namespace, like Employees/yournamespace.GetByKeyFromConfig
The first scenario can you use this feature? http://odata.github.io/WebApi/#04-17-Alternate-Key
Hope this can help,Thanks.
Update:
Function declaration is right now, the error message shown because your ODataRoute is wrong, remove your ODataRoute, that function can be called as Employees/Default.GetByKey(1), WebAPI/OData can route to this function by default.
Or add the namespace to ODataRoute, it's Default by default, change builder.Namespace is not right, you have to change the function's namespace:
var func = employeeType.Collection.Function("GetByKey").Returns<Employee>().Parameter<int>("Key");
func.Namespace = "NamespaceX";
Then ODataRoute like [EnableQuery, HttpGet, ODataRoute("Employees/NamespaceX.GetByKey(Key={Key})")] should work.
There are two issues with the code you paste,
1. You try to bind a function to Employees collection, the model build is incorrect.
It should be something like
var employeeType = builder.EntityType();
employeeType .Collection.Function("GetByKeyFromConfig").Returns().Parameter("Key");
You can refer to examples in link https://github.com/OData/ODataSamples/tree/master/WebApi/v4/ODataFunctionSample for different ways to bind functions.
In the ODataRoute, we either need to specify the function with namespace or enable unqualified function call via this config in register method "config.EnableUnqualifiedNameCall(unqualifiedNameCall: true);".
Refer to link http://odata.github.io/WebApi/#03-03-attrribute-routing and search unqualified.
Let me know if this does not resolve your issue.
Related
I'd like to reject an OData request if it has $top option value exceeding a global limit. ODataQueryOptions is created manually and I don't know how to correctly trigger its validation.
Assemblies affected
<PackageReference Include="Microsoft.AspNetCore.OData" Version="7.0.0" />
Reproduce steps
I do global setup like this:
public static void UseODataRoutes(this IRouteBuilder routeBuilder,
IEdmModel edmModel)
{
routeBuilder.MapODataServiceRoute(RouteName,
RoutePrefix,
containerBuilder => containerBuilder.AddService(ServiceLifetime.Singleton, provider => edmModel));
routeBuilder.Count().Filter().OrderBy().Select().MaxTop(1);
routeBuilder.EnableDependencyInjection();
}
And manually create an ODataQueryOptions<TEntity> instance:
public ODataQueryFactory(IHttpContextAccessor httpAccessor, IPerRouteContainer container)
{
_request = httpAccessor.HttpContext.Request;
_odataServiceProvider = container.GetODataRootContainer(ODataExtentions.RouteName);
}
public ODataQueryOptions<TEntity> CreateQueryOptions<TEntity>()
where TEntity : class
{
var model = _odataServiceProvider.GetService<IEdmModel>();
var path = _request.ODataFeature().Path;
var context = new ODataQueryContext(model, typeof(TEntity), path);
var queryOptions = new ODataQueryOptions<TEntity>(context, _request);
return queryOptions;
}
The problem is that the global MaxTop is ignored leading to successful GET /foo?$top=10 request even if MaxTop(1) has been called.
However, if I just add:
queryOptions.Validate(new ODataValidationSettings()
{
MaxTop = 1
});
To my factory method, then the request with $top=10 produce a perfectly looking exception leading to 400 response. That's my goal.
How to automatically trigger this validation or automatically create an ODataValidationSettings instance using all the global settings previously passed to IRouteBuilder?
P.S. I'd like to avoid manual ODataValidationSettings creation and use the standard Odata API instead.
To validate the query using only global options, pass an empty ODataValidationSettings parameter:
queryOptions.Validate(new ODataValidationSettings());
More info on GitHub (I directly asked OData developers).
P.S. I still have no idea how to set a default global limit to prevent selecting all the entities from the db if $top hasn't been specified. The default validation doesn't cover this case.
I'm using Microsoft.AspNet.OData v6.0.0 and expect that setting the MaxTop value to 10 will enable the $top query option.
However, requesting the URL http://localhost:23344/odata/v4/Resources?$top=10 still gives me the error:
{"error":{"code":"","message":"The query specified in the URI is not valid. The limit of '0' for Top query has been exceeded. The value from the incoming request is '10'.","innererror":{"message":"The limit of '0' for Top query has been exceeded. The value from the incoming request is '10'.","type":"Microsoft.OData.ODataException","stacktrace":" at System.Web.OData.Query.Validators.TopQueryValidator.Validate(TopQueryOption topQueryOption, ODataValidationSettings validationSettings)\r\n at System.Web.OData.Query.TopQueryOption.Validate(ODataValidationSettings validationSettings)\r\n at System.Web.OData.Query.Validators.ODataQueryValidator.Validate(ODataQueryOptions options, ODataValidationSettings validationSettings)\r\n at System.Web.OData.Query.ODataQueryOptions.Validate(ODataValidationSettings validationSettings)\r\n at System.Web.OData.EnableQueryAttribute.ValidateQuery(HttpRequestMessage request, ODataQueryOptions queryOptions)\r\n at System.Web.OData.EnableQueryAttribute.ExecuteQuery(Object response, HttpRequestMessage request, HttpActionDescriptor actionDescriptor, ODataQueryContext queryContext)\r\n at System.Web.OData.EnableQueryAttribute.OnActionExecuted(HttpActionExecutedContext actionExecutedContext)"}}}
As though the top query still has a limit of 0.
Controller
public class ResourcesController : ODataController
{
private IResourceService resourceService;
public ResourcesController(IResourceService resourceService)
{
this.resourceService = resourceService;
}
[EnableQuery(MaxTop=10)]
public IQueryable<Resource> Get()
{
return resourceService.GetResources().AsQueryable();
}
[EnableQuery]
public SingleResult<Resource> Get([FromODataUri] int key)
{
var result = resourceService.GetResources().Where(r => r.Id == key).AsQueryable();
return SingleResult.Create(result);
}
}
WebApiConfig.cs
public static void Register(HttpConfiguration config)
{
ODataConventionModelBuilder builder = new ODataConventionModelBuilder
{
Namespace = "MyNamespace",
ContainerName = "DefaultContainer"
};
builder.EntitySet<Resource>("Resources");
builder.EntityType<Resource>().Select().Count().Expand().OrderBy();
config.MapODataServiceRoute(
routeName: "ODataRoute",
routePrefix: "odata/v4",
model: builder.GetEdmModel());
}
What I've found
Github issue describing possible bug with behaviour of MaxTop
What does work
Every other query option I've enabled, including $skip.
What I've Tried
As in this question Setting config.Select().Expand().Filter().OrderBy().MaxTop(null).Count(); in the WebApiConfig.cs before config.mapODataServiceRoute(.... Didn't work.
Adding [Page(MaxTop = 100)] to my Resource model as in the same question. Didn't work.
Setting [Page] attribute on model. From WebApi OData documentation "if you set the Page Attribute, by default it will enable the $top with no-limit maximum value". Didn't work.
Setting [EnableQuery(PageSize=10)] attribute on controller. From WebApi OData documentation "if you set the Page Attribute, by default it will enable the $top with no-limit maximum value". Enabled paging but Didn't work.
The error says the limit was 0 for top in every case
I had the same issue and discovered that if you specify any rules in builder like you did.
builder.EntitySet<Resource>("Resources");
builder.EntityType<Resource>().Select().Count().Expand().OrderBy();
Value set by attribute will be overriden and set to 0.
If you remove those entries and put global configuration like
config.Select().Expand().Filter().OrderBy().MaxTop(600).Count();
It will work.
Also you can define MaxTop using fluent interface on builder like this.
builder.EntityType<T>().Page(100, 100);
And it'll work even if you define other rules in builder for entity type.
Summarizing
It's probably caused that new configuration is created when you define some config in fluent builder interface and you cannot use attributes (EnableQueryAttribute on controller and Page on model).
It's probably a bug because they still recommend attribute approach. I will report it as issue on OData repository.
I had the same error. In my case I tried to hide a specific field from an entity with the Select(..) extension on EntityType<..>().
builder
.EntityType<User>()
.Select(System.Web.OData.Query.SelectExpandType.Disabled, "Password");
This happens inside my GetModel()-method. So I just changed the order in which I set the global settings:
config.Count().Filter().OrderBy().Expand().Select().MaxTop(100);
IEdmModel model;
model = GetModel();
This is for the People coming with the same issue for .NetCore 6.0 . These filters can be added in the Program.cs with the AddController (controller extension) as shown below.
builder.Services.AddControllers(options =>
{
options.Filters.Add(new EnableQueryAttribute()
{
AllowedQueryOptions = AllowedQueryOptions.All,
AllowedOrderByProperties = null,
AllowedLogicalOperators = AllowedLogicalOperators.All,
});
}).AddOData(o => o.Select().Filter().OrderBy().SetMaxTop(100));
Required namespaces are
using Microsoft.AspNetCore.OData;
using Microsoft.AspNetCore.OData.Query;
So, I tried following the instructions here to implement an action on my Job entity, but for the life of me, I can't get OData to recognize it.
The Action is pretty simple. Just a toggle for a boolean:
[HttpPost]
public IHttpActionResult Pause([FromODataUri]int key)
{
if (!ModelState.IsValid)
{
return BadRequest();
}
Job job = _context.Job.Find(key);
if (job == null)
{
return NotFound();
}
job.IsPaused = !job.IsPaused;
_context.SaveChanges();
return Ok(acquisition.IsPaused);
}
And it is defined in my WebApiConfig as:
var jobEntity = builder.EntityType<Job>();
var pause = jobEntity.Action("Pause");
pause.Returns<bool>();
So, I should be able to POST to //url/odata/Job(key)/Pause to call it. Unfortunately, it doesn't recognize the Pause action, listing it with a response of entityset/key/unresolved action. If I try to use the ODataRoute attribute on it:
[ODataRoute("Job({key})/Pause")]
It chokes on the Pause, giving me a compile error of "The path template 'Job({key})/Pause' on the action Pause in controller 'Job' is not a valid OData path template. Found an unresolved path segment 'Pause' in the OData path template."
Now, if I make it an unbound action:
var pause = builder.Action("Pause");
pause = Parameter<int>("key");
pause.Returns<bool>();
and
[HttpPost]
[ODataRoute("Pause")]
public IHttpActionResult Pause(ODataActionParameters parameters)
{
if (!ModelState.IsValid)
{
return BadRequest();
}
var key = Convert.ToInt32(parameters["key"]);
Job job = _context.Job.Find(key);
if (job == null)
{
return NotFound();
}
job.IsPaused = !job.IsPaused;
_context.SaveChanges();
return Ok(acquisition.IsPaused);
}
It works just fine. So, why can't I bind this to the Job entity? Just as a note I did try adding ODataActionParameters, even tho I don't have any parameters to see if that changed anything. It didn't.
FYI the bound to entityset examples, from that, you can see that you need a namespace, you should request like: odata/Job(key)/Default.Pause, bound action doesn't need ODataRoute.
I'm also struggling with it...
I tried to apply the Fan Ouyang's suggestion without success...
http://odata.github.io/WebApi/#04-07-action-parameter-support
OData route config:
EntityTypeConfiguration<tblMeetings> MeetingsType = builder.EntitySet<tblMeetings>("Meetings").EntityType;
MeetingsType.HasKey(p => p.Meeting_ID);
var MeetingsFunctionBadges = MeetingsType.Collection.Function("Badges");
MeetingsFunctionBadges.Parameter<int>("key");
MeetingsFunctionBadges.Returns<List<tblBadges>>();
The meeting's controller:
[HttpGet]
[ODataRoute("Meetings({key})/Badges")]
public IHttpActionResult GetBadges(ODataActionParameters parameters)
{
return Ok();
}
An then the error...
An exception of type 'System.InvalidOperationException' occurred in
System.Web.OData.dll but was not handled in user code
Additional information: The path template 'Meetings({key})/Badges' on
the action 'GetBadges' in controller 'Meetings_OData' is not a valid
OData path template. Found an unresolved path segment 'Badges' in the
OData path template 'Meetings({key})/Badges'.
I have a method in the controller ApplicationsController, in which I need to get the base URL for an action method:
public ActionResult MyAction(string id)
{
var url = Url.Action("MyAction", "Applications");
...
}
The problem is that this includes the string id from the current route data, when I need the URL without (the URL is used to fetch content from a CMS on a URL-based lookup).
I have tried passing null and new { } as the routeValues parameter to no avail.
The matching route is as follows (above all other routes):
routes.MapLowercaseRoute(
name: "Applications",
url: "applications/{action}/{id}",
defaults: new { controller = "Applications",
action = "Index", id = UrlParameter.Optional });
I've seen a couple of other questions touch on this but none of them seem to have a viable solution. At present, I am resorting to hardcoding the path in the controller; however, I'd like to be able to abstract this into an action filter, so I need to be able to generate the URL.
Is there a clean/conventional way to prevent this behaviour?
Ok, I see the problem. It's something called "Segment variable reuse". When generating the routes for outbound URLs, and trying to find values for each of the segment variables in a route’s URL pattern, the routing system will look at the values from the current request. This is a behavior that confuses many programmers and can lead to a lengthy debugging session. The routing system is keen to make a match against a route, to the extent that it will reuse segment variable values from the incoming URL. So I think you have to override the value like Julien suggested :
var url = Url.Action("MyAction", "Applications", new { id = "" })
Ended up getting around this with a different approach. The only way I could come up with to prevent arbitrarily-named route values from being inserted into the generated URL was to temporarily remove them from RouteData when calling Url.Action. I've written a couple of extension methods to facilitate this:
public static string NonContextualAction(this UrlHelper helper, string action)
{
return helper.NonContextualAction(action,
helper.RequestContext.RouteData.Values["controller"].ToString());
}
public static string NonContextualAction(this UrlHelper helper, string action,
string controller)
{
var routeValues = helper.RequestContext.RouteData.Values;
var routeValueKeys = routeValues.Keys.Where(o => o != "controller"
&& o != "action").ToList();
// Temporarily remove routevalues
var oldRouteValues = new Dictionary<string, object>();
foreach (var key in routeValueKeys)
{
oldRouteValues[key] = routeValues[key];
routeValues.Remove(key);
}
// Generate URL
string url = helper.Action(routeValues["Action"].ToString(),
routeValues["Controller"].ToString());
// Reinsert routevalues
foreach (var kvp in oldRouteValues)
{
routeValues.Add(kvp.Key, kvp.Value);
}
return url;
}
This allows me to do this in an action filter where I won't necessarily know what the parameter names for the action are (and therefore can't just pass an anonymous object as in the other answers).
Still very much interested to know if someone has a more elegant solution, however.
Use a null or empty value for id to prevent Url.Action from using the current one:
var url = Url.Action("MyAction", "Applications", new { id = "" })
I was not entirely comfortable with the altering, transient or otherwise, of the RouterData in #AntP's otherwise fine solution. Since my code for creating the links was already centralized, I borrowed #Tomasz Jaskuλa and #AntP to augment the ExpandoObject, I was already using.
IDictionary<string,object> p = new ExpandoObject();
// Add the values I want in the route
foreach (var (key, value) in linkAttribute.ParamMap)
{
var v = GetPropertyValue(origin, value);
p.Add(key, v);
}
// Ideas borrowed from https://stackoverflow.com/questions/20349681/urlhelper-action-includes-undesired-additional-parameters
// Null out values that I don't want, but are already in the RouteData
foreach (var key in _urlHelper.ActionContext.RouteData.Values.Keys)
{
if (p.ContainsKey(key))
continue;
p.Add(key, null);
}
var href = _urlHelper.Action("Get", linkAttribute.HRefControllerName, p);
I have a MVC4 WebApi project with routing that is working correctly with an optional "id" parameter in the route:
routes.Add(new ApiRouteInfo
{
Name = this.AreaName.ToLower() + "_readingsplans",
RouteTemplate = baseUrl + "/plans/readingalerts/{id}",
Defaults = new
{
area = this.AreaName.ToLower(),
controller = "ReadingAlerts",
id = RouteParameter.Optional
}
});
When making an actual request the routing works to hit either the GetAll or Get method in the controller methods:
public HttpResponseMessage GetAll(BaseQueryFilter filter)
public HttpResponseMessage Get(int id)
But in the unit test, the RouteTester object always hits the Get method, not the GetAll.
Works:
Assert.AreEqual(ReflectionHelper.GetMethodName((ReadingAlertsController p) => p.Get(It.IsAny<int>())), routeTester.GetActionName());
Fails:
Assert.AreEqual(ReflectionHelper.GetMethodName((ReadingAlertsController p) => p.GetAll(null)), routeTester.GetActionName());
I've tried passing in an actual filter object instead of null but that doesn't change the outcome at all.
I know I can fix it by creating two different routes, but I'm a bit reluctant since the current routing does work for everything except the unit test.
Any suggestions?
Did you look at this? It explains a lot about unit testing web api and it may be useful to you.
I found a stackoverflow thread which describes how to test out the route. I am using something similar that I found on the net, but I am willing to try it.
Here is another article with a similar implementation. This is what I am using and having a similar issue with.
--Updated--
I believe I found the fix for the issue. Using the article mentioned above, I replaced the 'GetActionDescriptor()' function with the following:
private HttpActionDescriptor GetActionDescriptor()
{
if (controllerContext.ControllerDescriptor == null)
GetControllerType();
var actionSelector = new ApiControllerActionSelector();
var results = actionSelector.GetActionMapping(controllerContext.ControllerDescriptor);
try
{
return actionSelector.SelectAction(controllerContext);
}
catch
{
var subActions = results[request.RequestUri.Segments.Last()];
var action = subActions.FirstOrDefault(a => a.SupportedHttpMethods.First(m => m.Method == request.Method.Method) != null);
return action;
}
}