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;
}
}
Related
I wanted to make my WebAPI application change the used SessionStateBehavior based on action attributes like that:
[HttpPost]
[Route("api/test")]
[SetSessionStateBehavior(SessionStateBehavior.Required)] // <--- This modifies the behavior
public async Task<int> Test(){}
It seems, however, that the only place I can change the session behavior is inside my HttpApplication's Application_PostAuthorizeRequest (or in similar places, early in the request lifetime), otherwise I get this error:
'HttpContext.SetSessionStateBehavior' can only be invoked before 'HttpApplication.AcquireRequestState' event is raised.
So, at that point no controller or action resolution is done, so I don't know what action will be called in order to check its attributes.
So, I am thinking of resolving the action manually.
I started with these lines of code to resolve the controller first:
var httpCtx = HttpContext.Current;
var ctrlSel = GlobalConfiguration.Configuration.DependencyResolver.GetService(typeof(IHttpControllerSelector)) as IHttpControllerSelector;
var actionSel = GlobalConfiguration.Configuration.DependencyResolver.GetService(typeof(IHttpActionSelector)) as IHttpActionSelector;
HttpControllerDescriptor controllerDescriptor = ctrlSel.SelectController(httpCtx.Request);
But in the last line I can't get the proper HttpRequestMessage from the request.
Any idea ho how get that?
This is not inside a controller, so I don't have it ready there.
Or, is there a better way to do this?
I am trying to see the disassembled code of the framework to copy portions of it, but I am quite lost at this point...
UPDATE:
This is the closest I got to resolving the action manually, but it doesn't work:
I have registered those two services:
container.RegisterType<IHttpControllerSelector, DefaultHttpControllerSelector>();
container.RegisterType<IHttpActionSelector, ApiControllerActionSelector>();
...and try to get the required session behavior like that:
private SessionStateBehavior GetDesiredSessionBehavior(HttpContext httpCtx)
{
var config = GlobalConfiguration.Configuration;
var diResolver = config.Services;
var ctrlSel = diResolver.GetService(typeof(IHttpControllerSelector)) as IHttpControllerSelector;
var actionSel = diResolver.GetService(typeof(IHttpActionSelector)) as IHttpActionSelector;
if (ctrlSel is null || actionSel is null)
{
return DefaultSessionBehavior;
}
var method = new HttpMethod(httpCtx.Request.HttpMethod);
var requestMsg = new HttpRequestMessage(method, httpCtx.Request.Url);
requestMsg.Properties.Add(HttpPropertyKeys.RequestContextKey, httpCtx.Request.RequestContext);
requestMsg.Properties.Add(HttpPropertyKeys.HttpConfigurationKey, config);
httpCtx.Request.Headers.Cast<string>().ForEach(x => requestMsg.Headers.Add(x, httpCtx.Request.Headers[x]));
var httpRouteData = httpCtx.Request.RequestContext.RouteData;
var routeData = config.Routes.GetRouteData(requestMsg);
requestMsg.Properties.Add(HttpPropertyKeys.HttpRouteDataKey, routeData);
requestMsg.SetRequestContext(new HttpRequestContext(){RouteData = routeData });
requestMsg.SetConfiguration(config);
var route = config.Routes["DefaultApi"];
requestMsg.SetRouteData(routeData ?? route.GetRouteData(config.VirtualPathRoot, requestMsg));
var routeHandler = httpRouteData.RouteHandler ?? new WebApiConfig.SessionStateRouteHandler();
var httpHandler = routeHandler.GetHttpHandler(httpCtx.Request.RequestContext);
if (httpHandler is IHttpAsyncHandler httpAsyncHandler)
{
httpAsyncHandler.BeginProcessRequest(httpCtx, ar => httpAsyncHandler.EndProcessRequest(ar), null);
}
else
{
httpHandler.ProcessRequest(httpCtx);
}
var values = requestMsg.GetRouteData().Values; // Hm this is empty and makes the next call fail...
HttpControllerDescriptor controllerDescriptor = ctrlSel.SelectController(requestMsg);
IHttpController controller = controllerDescriptor?.CreateController(requestMsg);
if (controller == null)
{
return DefaultSessionBehavior;
}
var ctrlContext = CreateControllerContext(requestMsg, controllerDescriptor, controller);
var actionCtx = actionSel.SelectAction(ctrlContext);
var attr = actionCtx.GetCustomAttributes<ActionSessionStateAttribute>().FirstOrDefault();
return attr?.Behavior ?? DefaultSessionBehavior;
}
I have an alternative hack to make it work (send header values from the client to modify the session behavior), but it would be nice if the version above worked.
UPDATE:
Eventually, I went with setting the session behavior based on a client header value and validating the validity of sending that header based on the action attributes later-on in the request lifetime. If someone can solve the action resolution code I was fighting with above, feel free to post the answer here.
I don't know if this is going to be helpful for you, but I was just following a Pluralsight course (https://app.pluralsight.com/player?course=implementing-restful-aspdotnet-web-api) and in the Versioning chapter the author shows how to implement a controller selector where he does have access to the request.
The controller selector looks like:
public class CountingKsControllerSelector : DefaultHttpControllerSelector
{
private HttpConfiguration _config;
public CountingKsControllerSelector(HttpConfiguration config)
: base(config)
{
_config = config;
}
public override HttpControllerDescriptor SelectController(HttpRequestMessage request)
{
var controllers = GetControllerMapping();
var routeData = request.GetRouteData();
var controllerName = (string)routeData.Values["controller"];
HttpControllerDescriptor descriptor;
if (controllers.TryGetValue(controllerName, out descriptor))
{
[...]
return descriptor;
}
return null;
}
}
And it's registered in WebApiConfig with:
config.Services.Replace(typeof(IHttpControllerSelector),
new CountingKsControllerSelector(config));
I'm creating a .NET Core 2.1 web api and am experiencing issues with the UrlHelper. I use the Link method to determine links from one resource to another. Most of the time it is working just fine, but sometimes the resulting string lacks the base url.
E.g.:
Request 1: http://localhost:1234/api/books?pageNumber=1&pageSize=10 OK
Request 2: http:///api/books?pageNumber=1&pageSize=10 not OK
And it goes back and forth between the correct and the wrong result, sometimes all of them are wrong, other times all of them are OK.
ConfigureServices:
services.AddSingleton<IActionContextAccessor, ActionContextAccessor>();
services.AddScoped<IUrlHelper, UrlHelper>(factory =>
{
var actionContext = factory.GetService<IActionContextAccessor>().ActionContext;
return new UrlHelper(actionContext);
});
Example method for creating the links (pagination):
public string CreateResourceUri(
ResourceUriType type,
string name,
IUrlHelper urlHelper,
object additionalValues = null)
{
dynamic values = CreateValues().Merge(additionalValues);
switch (type)
{
case ResourceUriType.PreviousPage:
values.pageNumber = values.pageNumber - 1;
return urlHelper.Link(
name,
values);
case ResourceUriType.NextPage:
values.pageNumber = values.pageNumber + 1;
return urlHelper.Link(
name,
values);
default:
return urlHelper.Link(
name,
values);
}
}
Any ideas on what's going wrong here would be greatly appreciated.
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.
In an existing C# Web project here at my Job I've added a Web API part.
In four of my own classes that I use for the Web API I need to access some of the existing Controller-classes. Right now I just create a new Instance of them and everything works as intented: ProductController controller = new ProductController();
Still, creating a new ProductController while one should already exist obviously isn't a good practice. I know the Controllers are created in the Config-file in the Routes.MapHttpRoute, since it's using the C# Web MVC method. Below I've copied that piece of code:
config.Routes.MapHttpRoute(
name: "Default",
routeTemplate: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
namespaces: new[] { "MyProject.Controllers" }
);
route.DataTokens["UseNamespaceFallback"] = false;
I've tried to access these Controllers in my one of my API-classes like so:
private void getControllerInstance()
{
var url = "~/Products";
// Original path is stored and will be rewritten in the end
var httpContext = new HttpContextWrapper(HttpContext.Current);
string originalPath = httpContext.Request.Path;
try
{
// Fake a request to the supplied URL into the routing system
httpContext.RewritePath(url);
RouteData urlRouteData = RouteTable.Routes.GetRouteData(httpContext);
// If the route data was not found (e.g url leads to another site) then authorization is denied.
// If you want to have a navigation to a different site, don't use AuthorizationMenu
if (urlRouteData != null)
{
string controllerName = urlRouteData.Values["controller"].ToString();
// Get an instance of the controller that would handle this route
var requestContext = new RequestContext(httpContext, urlRouteData);
var controllerFactory = ControllerBuilder.Current.GetControllerFactory();
// TODO: Fix error (The controller for path '/Products' was not found or does not implement IController.) on this line:
var controllerbase = (ControllerBase)controllerFactory.CreateController(requestContext, controllerName);
controller = (ProductController)controllerbase;
}
}
finally
{
// Reset our request path.
httpContext.RewritePath(originalPath);
}
}
As you might have noticed by the TODO-comment, at the line var controllerbase = (ControllerBase)controllerFactory.CreateController(requestContext, controllerName);, I get the following error:
HttpException was unhandler by user code: The controller for path '/Products' was not found or does not implement IController.
Does anyone know how to fix this error? Has this got something to do with one of the following two lines of the code in the Config-file?
namespaces: new[] { "MyProject.Controllers" }
route.DataTokens["UseNamespaceFallback"] = false;
Or did I do something else wrong?
A tip to everyone: Don't continue programming when you are very, very tired.. Anyway, everything was correct except for a small flaw:
My API Controller is called ProductsController and my normal (default) controller is called ProductController. In the method above I use:
var url = "~/Products";
To access the ProductController..
So, after removing the "s" (and for good measure make everything lower case) I have the following instead:
var url = "~/product";
And now it works..
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);