I'd like to have a catch all route that matches a general route prefix ("api/myaccount/1") execute if there are no more specific routes defined on other controllers (i.e "api/myaccount/1/feature") however I get the following exception when I do this:
Multiple controller types were found that match the URL. This can
happen if attribute routes on multiple controllers match the requested
URL.
As mentioned here:
Multiple controller types were found that match the URL. This can happen if attribute routes on multiple controllers match the requested URL it seems this may not be possible.
Wanting a default route to execute when no better one is found sounds pretty common so what am I missing? Do I need to hook lower in the pipeline or something...
FYI: I have the catch all working fine ("api/myaccount/1/{*uri}") it's just the being able to override it that's the problem.
Turns out this is pretty easy, I just needed to create a custom Controller Selector and override the GetControllerName function. That particular override is required because the method you would expect to override:
HttpControllerDescriptor SelectController(HttpRequestMessage request)
does not just return the descriptor (or null if It can't find a match) as you may expect. The method actually handles the request for you and returns a 404 :/ However once you're aware of that it is trivial to work around and I was able to get the behavior I wanted using the code below:
using System.Web.Http;
using System.Web.Http.Dispatcher;
public class CustomControllerSelector : DefaultHttpControllerSelector
{
public override string GetControllerName(HttpRequestMessage request)
{
var name = base.GetControllerName(request);
if(string.IsNullOrEmpty(name))
{
return "MyFeature"; //important not to include "Controller" suffix
}
return name;
}
}
And add it to your configuration:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
...
config.Services.Replace(typeof(IHttpControllerSelector),
new CustomControllerSelector(config));
...
}
}
Related
I use OpenAPI (Swagger) in a .NET Core project and when using multiple methods that have similar get requests, I encounter "Microsoft.AspNetCore.Routing.Matching.AmbiguousMatchException: The request matched multiple endpoints." error during runtime. I look at several pages on the web and SO and tried to apply the workarounds like The request matched multiple endpoints but why?, but it does not fix the problem. Here are the API methods and route definitions that I use.
[Route("get", Name="get")]
public IEnumerable<DemoDto> Get()
{
//
}
[Route("get/{id}", Name="getById")]
public DemoDto GetById(int id)
{
//
}
[Route("get/{query}", Name="getWithPagination")]
public IEnumerable<DemoDto> GetWithPagination(DemoQuery query)
{
//
}
I use Name property in order to fix the problem but not solved. Any idea to make changes on the routes to differentiate Get() and GetWithPagination()?
You have two endpoints with equals routes:
get/{id} and get/{query}.
If you write in browser line: get/123, the system can't understand what route to use, because they have the same pattern.
You need to distinguish them and I suggest you use restful style for routes, like:
item/{id},
items?{your query}
[Route("get/{query}", Name="getWithPagination")]
This doesn't make sense. DemoQuery is an object, it can't be represented by a single part of a url. You can tell the ModelBinder to build your object from multiple query parameters, though.
The routing engine is getting this route confused with the [Route("get/{id}", Name="getById")] route. They both appear to match get/blah.
In addition to fixing your DemoQuery route, try adding a route constraint on the id route -
[Route("get/{id:int}", Name="getById")]
to better help the engine.
To get DemoQuery to work, assume it looks something like:
public class DemoQuery
{
public string Name { get; set; }
public int Value { get; set; }
}
Then change your action to
[Route("getPaged/{query}", Name="getWithPagination")]
public IEnumerable<DemoDto> GetWithPagination([FromQuery] DemoQuery query)
and call then endpoint like /getPaged?name=test&value=123. And the ModelBinder should build your object for you.
ASP.NET Web API 2 supports a new type of routing. Offical Doc
Route constraints let you restrict your parameters type and matched with these types (int, string, even date etc). The general syntax is "{parameter:constraint}"
[Route("users/{id:int}")]
public User GetUserById(int id) { ... }
[Route("users/{name}")]
public User GetUserByName(string name) { ... }
I tested at API;
//match : api/users/1
[HttpGet("{id:int}")]
public IActionResult GetUserById(int id){ ... }
//match : api/users/gokhan
[HttpGet("{name}")]
public IActionResult GetUserByName(string name){ ... }
I'm transitioning out of using WCF Data Services (since they're apparently dead), and attempting to build OData feeds using the newer Microsoft ASP.NET WebApi 2.1. I'm looking for a way to rename my feeds to be different than the class name.
I built out all my controllers, and now I'm trying to rename them just slightly to preserve the idea of set vs. single entities. (For example, the feed should be named WorkCategories, but the class name should be WorkCategory). Is this possible? I want to do something like this:
public static void Register(HttpConfiguration config)
{
builder.EntitySet<EmailSequenceItem>("EmailSequenceItems");
builder.EntitySet<EmailSequence>("EmailSequences");
builder.EntitySet<WorkCategory>("WorkCategories");
...
config.Routes.MapODataRoute("odata", "odata", builder.GetEdmModel());
config.MapHttpAttributeRoutes();
}
My controller looks like this (built from templates):
public class WorkCategoryController: ODataController
{
private dcMaintContext db = new dcMaintContext();
// GET odata/WorkCategory
[Queryable]
public IQueryable<WorkCategory> GetWorkCategory()
{
return db.WorkCategories;
}
...
}
But what I get when I rename any of the feeds is a 404 when navigating to http://localhost/odata/WorkCategories:
HTTP/1.1 404 Not Found
Message: No HTTP resource was found that matches the request URI 'http://localhost/odata/WorkCategories'."
MessageDetail: No type was found that matches the controller named 'WorkCategories'
The name of the controller should be same with the EntitySet name by default, which is WorkCategories.
So other controller name except for WorkCategoriesController won't work unless you create your own IODataRoutingConvention.
For the method name, webapi has its default routing rule.
For get entityset, GetWorkCategories() and Get() will work.
For get entity, GetWorkCategory(int key) and Get(int key) will work.
If you want to customize the method name, you can use AttributeRouting in webapi 2.2.
http://blogs.msdn.com/b/webdev/archive/2014/03/13/getting-started-with-asp-net-web-api-2-2-for-odata-v4-0.aspx
[ODataRoute("WorkCategories/WhateverName")]
public IQueryable WhateverName() {...}
Aha! I found this just after I posted it. I just need to rename my controller class to WorkCategoriesController, and the 2 Queryable methods to GetWorkCategories instead of WorkCategory
public class WorkCategoriesController : ODataController
{
private dcMaintContext db = new dcMaintContext();
// GET odata/WorkCategory
[Queryable]
public IQueryable<WorkCategory> GetWorkCategories()
{
return db.WorkCategories;
}
// GET odata/WorkCategory(5)
[Queryable]
public SingleResult<WorkCategory> GetWorkCategories([FromODataUri] int key)
{
return SingleResult.Create(db.WorkCategories.Where(workcategory => workcategory.ID == key));
}
...
}
I have seen how to do this globally in numerous posts and have it working in my code. The problem is that it's firing on EVERY call which isn't what I want, I only want it to fire on the calls to the methods where I have decorated the method with the attribute:
public class MyController : ApiController
{
[MyAttribute]
public void MethodA()
{
// Do Work - should have called the attribute filter
}
public void MethodB()
{
// Do Work - should NOT have called the attribute filter
}
}
This seems really basic to me and that I'm missing something but the only way I can get the attribute to fire at all is by registering it in global.asax using GlobalConfiguration.Configuration.Filters.Add(new MyAttribute()); which causes it to fire on requests to both MethodA and MethodB. Is there any way to register the attribute and only fire on the methods where it is tagged? I have tried using AttributeUsage to no avail.
EDIT Added code for attribute per comment although had to remove the inner workings. It's firing on all requests...
[AttributeUsage(AttributeTargets.Method)]
public class MyAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
// Do work
}
}
Edit 11/25
In addition to the information below where I accepted the answer I would like to add that a previous developer had removed the default ActionDescriptorFilterProvider with the following code that needed to be commented out in order for the default behavior of the custom action filters to take effect:
var providers = GlobalConfiguration.Configuration.Services.GetFilterProviders();
var defaultprovider = providers.First(i => i is ActionDescriptorFilterProvider);
// This line was causing the problem.
GlobalConfiguration.Configuration.Services.Remove(typeof(System.Web.Http.Filters.IFilterProvider), defaultprovider);
see HttpConfiguration.Filters Property - it clearly says
Gets the list of filters that apply to all requests served using this
HttpConfiguration instance.
but you need ActionFilter - which is, by definition,
Action filters contain logic that is executed before and after a
controller action executes. You can use an action filter, for
instance, to modify the view data that a controller action returns.
so basically what you need to do is to remove
GlobalConfiguration.Configuration.Filters.Add(new MyAttribute());
line from your Global.asax.cs file.
Using filter on the Action or Controller level you need to register it in the same ConfigureServices method but as a service:
services.AddScoped<ActionFilterExample>();
services.AddScoped<ControllerFilterExample>();
Finally, to use a filter registered on the Action or Controller level, you need to place it on top of the Controller or Action as a ServiceType:
namespace AspNetCore.Controllers
{
[ServiceFilter(typeof(ControllerFilterExample))]
[Route("api/[controller]")]
[ApiController]
public class TestController : ControllerBase
{
[HttpGet]
[ServiceFilter(typeof(ActionFilterExample))]
public IEnumerable<string> Get()
{
return new string[] { "example", "data" };
}
}
}
I have two API controllers located in two different folders which has GetEvents() action:
V1
EventsController
V2
EventsController
I'd like to be able to access the controllers using:
api/v1/events and api/v2/events
my route template looks like this
api/v{version}/{controller}
private static string GetRequestVersion(HttpRequestMessage request)
{
// how to find the {version} value from url?
}
I've written a code using REGEX which works fine:
private static string ExtractVersionFromUrl(HttpRequestMessage request)
{
Match match = Regex.Match(request.RequestUri.PathAndQuery, #"/v(?<version>\d+)/", RegexOptions.IgnoreCase);
return match.Success ? match.Groups["version"].Value : null;
}
What's the standard approach to retreive such data from uri?
Try,
Request.GetRouteData().Values["version"];
GetRouteData is an extension method and is available in the namespace System.Net.Http.
I'm hazarding a guess regarding the return value. Another way to do this is simply define it as a parameter in your method in the controller like so:
public IEnumerable<Event> GetEvents(string version, HttpRequestMessage request) {
...
}
I just figured this out myself. Web API is just beautiful!
I wonder if there's any way to map a URL to ASMX service much like it is done with pages (using routes.MapPageRoute() method).
When I tried to do it simply by pointing the MapPageRoute to my service I get the error
Type 'MvcApplication1.Services.EchoService' does not inherit from 'System.Web.UI.Page'.
Matthias.
I stumbled upon this question trying to find the answer myself, and since I did figure out a way to do it, I figured I'd answer it.
The reason I needed this is because I'm converting an old ASP.NET website to ASP.NET MVC, and for compatibility purposes I need a web service available at a specific URL. However, the path of that URL is now handled by a Controller in the new site, so I cannot have a physical directory with the same name (since that will prevent the controller from being invoked for other URLs with that path other than the web service).
The PageRouteHandler, which is used by RouteCollection.MapPageRoute, indeed requires that the handler for the target path derives from System.Web.Page, which isn't the case for web services. So instead, it is necessary to create a custom page handler:
using System;
using System.Web;
using System.Web.Routing;
using System.Web.Services.Protocols;
public class ServiceRouteHandler : IRouteHandler
{
private readonly string _virtualPath;
private readonly WebServiceHandlerFactory _handlerFactory = new WebServiceHandlerFactory();
public ServiceRouteHandler(string virtualPath)
{
if( virtualPath == null )
throw new ArgumentNullException("virtualPath");
if( !virtualPath.StartsWith("~/") )
throw new ArgumentException("Virtual path must start with ~/", "virtualPath");
_virtualPath = virtualPath;
}
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
// Note: can't pass requestContext.HttpContext as the first parameter because that's
// type HttpContextBase, while GetHandler wants HttpContext.
return _handlerFactory.GetHandler(HttpContext.Current, requestContext.HttpContext.Request.HttpMethod, _virtualPath, requestContext.HttpContext.Server.MapPath(_virtualPath));
}
}
This route handler will create an appropriate handler for the web service based on the request and mapped virtual path of the service.
You can add a route for a web service now as follows:
routes.Add("RouteName", new Route("path/to/your/service", new RouteValueDictionary() { { "controller", null }, { "action", null } }, new ServiceRouteHandler("~/actualservice.asmx")));
Note: you must specify the controller and action values in the route value dictionary (even though they're set to null), otherwise the Html.ActionLink helper will always use this route for every single link (unless a match was found in the list before this route). Since you probably want to add this route before the default MVC route, it's important that it doesn't get matched that way.
Of course, you can create your own extension method to alleviate this task:
public static Route MapServiceRoute(this RouteCollection routes, string routeName, string url, string virtualPath)
{
if( routes == null )
throw new ArgumentNullException("routes");
Route route = new Route(url, new RouteValueDictionary() { { "controller", null }, { "action", null } }, new ServiceRouteHandler(virtualPath));
routes.Add(routeName, route);
return route;
}
After which you can simply do:
routes.MapServiceRoute("RouteName", "path/to/your/service", "~/actualservice.asmx");
I hope this helps someone, despite the age of this question. :)
Now that we waited two years with an anwer, how about using Web API instead? :)
EDIT: Kidding aside if that doesn't work for you and you still need an answer, leave a comment and I will see if I can't come up with a better one.
I attempted the original post's solution (also posted here), but I encountered a serious problem. I couldn't target the web method within the web service. When attempting to do so I got an exception stating the file didn't exist.
If you truly want to map an MVC route to a .ASMX web service the solution is explained here.
I believe that solution to be a hack by abusing the built-in types, because it uses reflection to bypass the intentional restrictive access members on the built-in .NET types.
Here is the method I'm taking which I believe to be much more straightforward.
First off, you should design your web services in the .ASMX file so that all the web service does is act as a published interface. At that point we don't need to target the .ASMX web service's methods directly. The important code has been made re-useable in core classes that are agnostic to the application's entry-point. We need this anyway so we can run automated tests!
Replace the MVC's web service method with a new route that has a custom route handler and http handler.
Old Route:
routes.MapRoute(
"Lead",
"lead/{action}.mvc",
new { controller = "Lead" });
New Route:
var dict = new RouteValueDictionary
{
{ "controller", null },
{ "action", null }
};
var handler = new LeadRouteHandler();
var route = new Route("lead/MVC_General.mvc", dict, handler);
routes.Add("Lead", route);
Note that the new route has the action hard-coded "MVC_General". This is because I wish to improve upon the giant controller class and create a handler for each action that way I can have small class with a single responsibility for each web method.
Implement the route's handlers.
IRouteHandler:
public class LeadRouteHandler : IRouteHandler
{
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
return new LeadHttpHandler();
}
}
IHttpHandler:
public class LeadHttpHandler : IHttpHandler
{
public bool IsReusable
{
get { return false; }
}
public void ProcessRequest(HttpContext context)
{
// Just enough code to preserve the route's json interface for tests
var typedResult = new PsaLeadSubmissionResult();
typedResult.Registered = false;
typedResult.Message = new List<string>
{
"Not Implemented"
};
var jsonResult = JsonConvert.SerializeObject(typedResult);
context.Response.Write(jsonResult);
}
}
From within IHttpHandler's ProcessRequest method we gain full control over that route's action. With a well designed web service architecture all we need to do is call the class's that support the .ASMX web method you are trying to map the route to.
The result is a very clean Global.asax file. We could have done all of this without the URL routing just by manually inspecting the URL, but that's too important of a file to bloat.