I notice that there are a bunch of similar questions out there about this topic.
I'm getting this error when calling any of the methods below.
Microsoft.AspNetCore.Routing.Matching.AmbiguousMatchException: The request matched multiple endpoints.
I can't however sort out what is best practice in resolving the issue.
So far I haven't set up any specific routing middleware.
// api/menus/{menuId}/menuitems
[HttpGet("{menuId}/menuitems")]
public IActionResult GetAllMenuItemsByMenuId(int menuId)
{
....
}
// api/menus/{menuId}/menuitems?userId={userId}
[HttpGet("{menuId}/menuitems")]
public IActionResult GetMenuItemsByMenuAndUser(int menuId, int userId)
{
...
}
What you're trying to do is impossible because the actions are dynamically activated. The request data (such as a query string) cannot be bound until the framework knows the action signature. It can't know the action signature until it follows the route. Therefore, you can't make routing dependent on things the framework doesn't even know yet.
Long and short, you need to differentiate the routes in some way: either some other static path or making the userId a route param. However, you don't actually need separate actions here. All action params are optional by default. Therefore, you can just have:
[HttpGet("{menuId}/menuitems")]
public IActionResult GetMenuItemsByMenu(int menuId, int userId)
And then you can branch on whether userId == 0 (the default). That should be fine here, because there will never be a user with an id of 0, but you may also consider making the param nullable and then branching on userId.HasValue instead, which is a bit more explicit.
You can also continue to keep the logic separate, if you prefer, by utilizing private methods. For example:
[HttpGet("{menuId}/menuitems")]
public IActionResult GetMenuItems(int menuId, int userId) =>
userId == 0 ? GetMenuItemsByMenuId(menuId) : GetMenuItemsByUserId(menuId, userId);
private IActionResult GetMenuItemsByMenuId(int menuId)
{
...
}
private IActionResult GetMenuItemsByUserId(int menuId, int userId)
{
...
}
Action routes need to be unique to avoid route conflicts.
If willing to change the URL consider including the userId in the route
// api/menus/{menuId}/menuitems
[HttpGet("{menuId:int}/menuitems")]
public IActionResult GetAllMenuItemsByMenuId(int menuId)
//....
}
// api/menus/{menuId}/menuitems/{userId}
[HttpGet("{menuId:int}/menuitems/{userId:int}")]
public IActionResult GetMenuItemsByMenuAndUser(int menuId, int userId) {
//...
}
##Reference Routing to controller actions in ASP.NET Core
##Reference Routing in ASP.NET Core
You have the same route in your HttpGet attribute
Change to something like this :
// api/menus/{menuId}/menuitems
[HttpGet("{menuId}/getAllMenusItems")]
public IActionResult GetAllMenuItemsByMenuId(int menuId)
{
....
}
// api/menus/{menuId}/menuitems?userId={userId}
[HttpGet("{menuId}/getMenuItemsFiltered")]
public IActionResult GetMenuItemsByMenuAndUser(int menuId, int userId)
{
...
}
This is another solution that you can use for this kind of scenario:
Solution 1 and more complex, using IActionConstrain, and ModelBinders(this gives you the flexibility to bind your input to a specific DTO):
The problem you have is that your controller has the same routing for 2 different methods receiving different parameters.
Let me illustrate it with a similar example, you can have the 2 methods like this:
Get(string entityName, long id)
Get(string entityname, string timestamp)
So far this is valid, at least C# is not giving you an error because it is an overload of parameters. But with the controller, you have a problem, when aspnet receives the extra parameter it doesn't know where to redirect your request.
You can change the routing which is one solution.
Normally I prefer to keep the same names and wrap the parameters on a DtoClass, IntDto and StringDto for example
public class IntDto
{
public int i { get; set; }
}
public class StringDto
{
public string i { get; set; }
}
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
[HttpGet]
public IActionResult Get(IntDto a)
{
return new JsonResult(a);
}
[HttpGet]
public IActionResult Get(StringDto i)
{
return new JsonResult(i);
}
}
but still, you have the error. In order to bind your input to the specific type on your methods, I create a ModelBinder, for this scenario, it is below(see that I am trying to parse the parameter from the query string but I am using a discriminator header which is used normally for content negotiation between the client and the server(Content negotiation):
public class MyModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
throw new ArgumentNullException(nameof(bindingContext));
dynamic model = null;
string contentType = bindingContext.HttpContext.Request.Headers.FirstOrDefault(x => x.Key == HeaderNames.Accept).Value;
var val = bindingContext.HttpContext.Request.QueryString.Value.Trim('?').Split('=')[1];
if (contentType == "application/myContentType.json")
{
model = new StringDto{i = val};
}
else model = new IntDto{ i = int.Parse(val)};
bindingContext.Result = ModelBindingResult.Success(model);
return Task.CompletedTask;
}
}
Then you need to create a ModelBinderProvider (see that if I am receiving trying to bind one of these types, then I use MyModelBinder)
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context.Metadata.ModelType == typeof(IntDto) || context.Metadata.ModelType == typeof(StringDto))
return new MyModelBinder();
return null;
}
and register it into the container
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers(options =>
{
options.ModelBinderProviders.Insert(0, new MyModelBinderProvider());
});
}
So far you didn't resolve the issue you have but we are close. In order to hit the controller actions now, you need to pass a header type on the request: application/json or application/myContentType.json. But in order to support conditional logic to determine whether or not an associated action method is valid or not to be selected for a given request, you can create your own ActionConstraint. Basically the idea here is to decorate your ActionMethod with this attribute to restrict the user to hit that action if he doesn't pass the correct media type. See below the code and how to use it
[AttributeUsage(AttributeTargets.All, Inherited = true, AllowMultiple = true)]
public class RequestHeaderMatchesMediaTypeAttribute : Attribute, IActionConstraint
{
private readonly string[] _mediaTypes;
private readonly string _requestHeaderToMatch;
public RequestHeaderMatchesMediaTypeAttribute(string requestHeaderToMatch,
string[] mediaTypes)
{
_requestHeaderToMatch = requestHeaderToMatch;
_mediaTypes = mediaTypes;
}
public RequestHeaderMatchesMediaTypeAttribute(string requestHeaderToMatch,
string[] mediaTypes, int order)
{
_requestHeaderToMatch = requestHeaderToMatch;
_mediaTypes = mediaTypes;
Order = order;
}
public int Order { get; set; }
public bool Accept(ActionConstraintContext context)
{
var requestHeaders = context.RouteContext.HttpContext.Request.Headers;
if (!requestHeaders.ContainsKey(_requestHeaderToMatch))
{
return false;
}
// if one of the media types matches, return true
foreach (var mediaType in _mediaTypes)
{
var mediaTypeMatches = string.Equals(requestHeaders[_requestHeaderToMatch].ToString(),
mediaType, StringComparison.OrdinalIgnoreCase);
if (mediaTypeMatches)
{
return true;
}
}
return false;
}
}
Here is your final change:
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
[HttpGet]
[RequestHeaderMatchesMediaTypeAttribute("Accept", new[] { "application/json" })]
public IActionResult Get(IntDto a)
{
return new JsonResult(a);
}
[RequestHeaderMatchesMediaTypeAttribute("Accept", new[] { "application/myContentType.json" })]
[HttpGet]
public IActionResult Get(StringDto i)
{
return new JsonResult(i);
}
}
Now the error is gone if you run your app. But how you pass the parameters?:
This one is going to hit this method:
public IActionResult Get(StringDto i)
{
return new JsonResult(i);
}
And this one the other one:
public IActionResult Get(IntDto a)
{
return new JsonResult(a);
}
Solution 2: Routes constrains
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
[HttpGet("{i:int}")]
public IActionResult Get(int i)
{
return new JsonResult(i);
}
[HttpGet("{i}")]
public IActionResult Get(string i)
{
return new JsonResult(i);
}
}
This is a kind of test because I am using the default routing:
https://localhost:44374/weatherforecast/"test" should go to the one that receives the string parameter
https://localhost:44374/weatherforecast/1 should go to the one that receives an int parameter
In my case [HttpPost("[action]")] was written twice.
I got this error, and just needed to restart the service to get it working again. Probably because I was modifying the code, and it re-registered the same controller method somehow.
You can have a dispatcher endpoint that will get the calls from both endpoints and will call the right based on parameters.
(It will works fine if their are in same controller).
Example:
// api/menus/{menuId}/menuitems
[HttpGet("{menuId}/menuitems")]
public IActionResult GetAllMenuItemsByMenuId(int menuId, int? userId)
{
if(userId.HasValue)
return GetMenuItemsByMenuAndUser(menuId, userId)
.... original logic
}
public IActionResult GetMenuItemsByMenuAndUser(int menuId, int userId)
{
...
}
Related
I notice that there are a bunch of similar questions out there about this topic.
I'm getting this error when calling any of the methods below.
Microsoft.AspNetCore.Routing.Matching.AmbiguousMatchException: The request matched multiple endpoints.
I can't however sort out what is best practice in resolving the issue.
So far I haven't set up any specific routing middleware.
// api/menus/{menuId}/menuitems
[HttpGet("{menuId}/menuitems")]
public IActionResult GetAllMenuItemsByMenuId(int menuId)
{
....
}
// api/menus/{menuId}/menuitems?userId={userId}
[HttpGet("{menuId}/menuitems")]
public IActionResult GetMenuItemsByMenuAndUser(int menuId, int userId)
{
...
}
What you're trying to do is impossible because the actions are dynamically activated. The request data (such as a query string) cannot be bound until the framework knows the action signature. It can't know the action signature until it follows the route. Therefore, you can't make routing dependent on things the framework doesn't even know yet.
Long and short, you need to differentiate the routes in some way: either some other static path or making the userId a route param. However, you don't actually need separate actions here. All action params are optional by default. Therefore, you can just have:
[HttpGet("{menuId}/menuitems")]
public IActionResult GetMenuItemsByMenu(int menuId, int userId)
And then you can branch on whether userId == 0 (the default). That should be fine here, because there will never be a user with an id of 0, but you may also consider making the param nullable and then branching on userId.HasValue instead, which is a bit more explicit.
You can also continue to keep the logic separate, if you prefer, by utilizing private methods. For example:
[HttpGet("{menuId}/menuitems")]
public IActionResult GetMenuItems(int menuId, int userId) =>
userId == 0 ? GetMenuItemsByMenuId(menuId) : GetMenuItemsByUserId(menuId, userId);
private IActionResult GetMenuItemsByMenuId(int menuId)
{
...
}
private IActionResult GetMenuItemsByUserId(int menuId, int userId)
{
...
}
Action routes need to be unique to avoid route conflicts.
If willing to change the URL consider including the userId in the route
// api/menus/{menuId}/menuitems
[HttpGet("{menuId:int}/menuitems")]
public IActionResult GetAllMenuItemsByMenuId(int menuId)
//....
}
// api/menus/{menuId}/menuitems/{userId}
[HttpGet("{menuId:int}/menuitems/{userId:int}")]
public IActionResult GetMenuItemsByMenuAndUser(int menuId, int userId) {
//...
}
##Reference Routing to controller actions in ASP.NET Core
##Reference Routing in ASP.NET Core
You have the same route in your HttpGet attribute
Change to something like this :
// api/menus/{menuId}/menuitems
[HttpGet("{menuId}/getAllMenusItems")]
public IActionResult GetAllMenuItemsByMenuId(int menuId)
{
....
}
// api/menus/{menuId}/menuitems?userId={userId}
[HttpGet("{menuId}/getMenuItemsFiltered")]
public IActionResult GetMenuItemsByMenuAndUser(int menuId, int userId)
{
...
}
This is another solution that you can use for this kind of scenario:
Solution 1 and more complex, using IActionConstrain, and ModelBinders(this gives you the flexibility to bind your input to a specific DTO):
The problem you have is that your controller has the same routing for 2 different methods receiving different parameters.
Let me illustrate it with a similar example, you can have the 2 methods like this:
Get(string entityName, long id)
Get(string entityname, string timestamp)
So far this is valid, at least C# is not giving you an error because it is an overload of parameters. But with the controller, you have a problem, when aspnet receives the extra parameter it doesn't know where to redirect your request.
You can change the routing which is one solution.
Normally I prefer to keep the same names and wrap the parameters on a DtoClass, IntDto and StringDto for example
public class IntDto
{
public int i { get; set; }
}
public class StringDto
{
public string i { get; set; }
}
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
[HttpGet]
public IActionResult Get(IntDto a)
{
return new JsonResult(a);
}
[HttpGet]
public IActionResult Get(StringDto i)
{
return new JsonResult(i);
}
}
but still, you have the error. In order to bind your input to the specific type on your methods, I create a ModelBinder, for this scenario, it is below(see that I am trying to parse the parameter from the query string but I am using a discriminator header which is used normally for content negotiation between the client and the server(Content negotiation):
public class MyModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
throw new ArgumentNullException(nameof(bindingContext));
dynamic model = null;
string contentType = bindingContext.HttpContext.Request.Headers.FirstOrDefault(x => x.Key == HeaderNames.Accept).Value;
var val = bindingContext.HttpContext.Request.QueryString.Value.Trim('?').Split('=')[1];
if (contentType == "application/myContentType.json")
{
model = new StringDto{i = val};
}
else model = new IntDto{ i = int.Parse(val)};
bindingContext.Result = ModelBindingResult.Success(model);
return Task.CompletedTask;
}
}
Then you need to create a ModelBinderProvider (see that if I am receiving trying to bind one of these types, then I use MyModelBinder)
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context.Metadata.ModelType == typeof(IntDto) || context.Metadata.ModelType == typeof(StringDto))
return new MyModelBinder();
return null;
}
and register it into the container
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers(options =>
{
options.ModelBinderProviders.Insert(0, new MyModelBinderProvider());
});
}
So far you didn't resolve the issue you have but we are close. In order to hit the controller actions now, you need to pass a header type on the request: application/json or application/myContentType.json. But in order to support conditional logic to determine whether or not an associated action method is valid or not to be selected for a given request, you can create your own ActionConstraint. Basically the idea here is to decorate your ActionMethod with this attribute to restrict the user to hit that action if he doesn't pass the correct media type. See below the code and how to use it
[AttributeUsage(AttributeTargets.All, Inherited = true, AllowMultiple = true)]
public class RequestHeaderMatchesMediaTypeAttribute : Attribute, IActionConstraint
{
private readonly string[] _mediaTypes;
private readonly string _requestHeaderToMatch;
public RequestHeaderMatchesMediaTypeAttribute(string requestHeaderToMatch,
string[] mediaTypes)
{
_requestHeaderToMatch = requestHeaderToMatch;
_mediaTypes = mediaTypes;
}
public RequestHeaderMatchesMediaTypeAttribute(string requestHeaderToMatch,
string[] mediaTypes, int order)
{
_requestHeaderToMatch = requestHeaderToMatch;
_mediaTypes = mediaTypes;
Order = order;
}
public int Order { get; set; }
public bool Accept(ActionConstraintContext context)
{
var requestHeaders = context.RouteContext.HttpContext.Request.Headers;
if (!requestHeaders.ContainsKey(_requestHeaderToMatch))
{
return false;
}
// if one of the media types matches, return true
foreach (var mediaType in _mediaTypes)
{
var mediaTypeMatches = string.Equals(requestHeaders[_requestHeaderToMatch].ToString(),
mediaType, StringComparison.OrdinalIgnoreCase);
if (mediaTypeMatches)
{
return true;
}
}
return false;
}
}
Here is your final change:
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
[HttpGet]
[RequestHeaderMatchesMediaTypeAttribute("Accept", new[] { "application/json" })]
public IActionResult Get(IntDto a)
{
return new JsonResult(a);
}
[RequestHeaderMatchesMediaTypeAttribute("Accept", new[] { "application/myContentType.json" })]
[HttpGet]
public IActionResult Get(StringDto i)
{
return new JsonResult(i);
}
}
Now the error is gone if you run your app. But how you pass the parameters?:
This one is going to hit this method:
public IActionResult Get(StringDto i)
{
return new JsonResult(i);
}
And this one the other one:
public IActionResult Get(IntDto a)
{
return new JsonResult(a);
}
Solution 2: Routes constrains
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
[HttpGet("{i:int}")]
public IActionResult Get(int i)
{
return new JsonResult(i);
}
[HttpGet("{i}")]
public IActionResult Get(string i)
{
return new JsonResult(i);
}
}
This is a kind of test because I am using the default routing:
https://localhost:44374/weatherforecast/"test" should go to the one that receives the string parameter
https://localhost:44374/weatherforecast/1 should go to the one that receives an int parameter
In my case [HttpPost("[action]")] was written twice.
I got this error, and just needed to restart the service to get it working again. Probably because I was modifying the code, and it re-registered the same controller method somehow.
You can have a dispatcher endpoint that will get the calls from both endpoints and will call the right based on parameters.
(It will works fine if their are in same controller).
Example:
// api/menus/{menuId}/menuitems
[HttpGet("{menuId}/menuitems")]
public IActionResult GetAllMenuItemsByMenuId(int menuId, int? userId)
{
if(userId.HasValue)
return GetMenuItemsByMenuAndUser(menuId, userId)
.... original logic
}
public IActionResult GetMenuItemsByMenuAndUser(int menuId, int userId)
{
...
}
I have a controller getting AmbiguousActionException. I have 3 methods I want to perform
1. Get a member based on their Id
2. Find all members with certain eye color
3. Get all members
The first works fine but 2 and 3 get AmbiguousActionException I assume because they both only use HTTPGet and nothing else. Is there a way I can differentiate between a default HTTPGet with a query parameter and HTTPGet. I'm using a query parameter as the eyecolor filter criteria, not a key for the member.
Here is my code:
namespace MyController.Controllers
{
[Route("members")]
public class MemberController: Controller
{
[HttpGet("{memberId}")]
public IActionResult GetMemberFor(string memberId)
{
return Ok(DoSomeMethodWithId(memberId));
}
[HttpGet(Name = "FindMembersWith")]
public IActionResult FindMembersFor([FromQuery(Name = "eyeColor")] string eyeColor)
{
return Ok(DoSomeOtherMethodWithFilter(eyeColor);
}
[HttpGet(Name = "GetAllMembers")]
public IActionResult GetAllMembers()
{
return Ok(DoMethodToRetrieveAllMembers);
}
}
}
urls:
www.mysite.com/members/{id} -- get single member by id
www.mysite.com/members -- get all members
www.mysite.com/members?eyeColor=blue --get all members with blue eyes
Is there something I can add to make the 2nd and 3rd routes work?
You can add custom routes:
[Route("FindMembersWith")]
[HttpGet]
public IActionResult FindMembersFor([FromQuery(Name = "eyeColor")] string eyeColor)
{
return Ok(DoSomeOtherMethodWithFilter(eyeColor);
}
[Route("GetAllMembers")]
[HttpGet]
public IActionResult GetAllMembers()
{
return Ok(DoMethodToRetrieveAllMembers);
}
In asp.net core I would like to set up my API controller to do the following:
by default return View(model);
/api/id.json to return model; as json
/api/id.xml to return model; as xml
The second two can be achieved by using the [FormatFilter] see here
[FormatFilter]
public class ProductsController
{
[Route("[controller]/[action]/{id}.{format?}")]
public Product GetById(int id)
However this requires the method to return an object and not a View(object). Is there anyway to cleanly support also returning Views?
You cannot do both in the same action. However, you can factor out the common functionality into a private method and then implement two actions with minimal code duplication:
[Route("[controller]")]
[FormatFilter]
public class ProductsController : Controller
{
private Product GetByIdCore(int id)
{
// common code here, return product
}
[HttpGet("[action]/{id}")]
[ActionName("GetById")]
public IActionResult GetByIdView(int id) => View(GetByIdCore(id));
[HttpGet("[action]/{id}.{format}")]
public Product GetById(int id) => GetByIdCore(id);
}
It's necessary to use different action names here, because the method signatures cannot differ merely on return type. However, the [ActionName] attribute can be used as above to make them appear to have the same name for the purposes of URL generation and such.
You can actually achieve this just using the one action. Here's an example of how I got it to work:
[FormatFilter]
public class ProductsController : Controller
{
[Route("[controller]/[action]/{id}.{format?}")]
public IActionResult GetById(int id, string format)
{
var yourModel = ...;
if (string.IsNullOrWhiteSpace(format))
return View(yourModel);
return Ok(yourModel);
}
By using IActionResult as the return type, you can return either a ViewResult or an OkObjectResult. You can get access to the format value by taking it as a parameter in your action, check if it's empty and then react accordingly.
I also added Controller as the base class in order to access the convenience methods for creating the relevant results (View(...) and Ok(...)).
If you're going to be using this pattern a lot, to keep your controllers as clean as possible, you could create a base class that exposed a "FormatOrView" method:
[FormatFilter]
public abstract class FormatController : Controller
{
protected ActionResult FormatOrView(object model)
{
var filter = HttpContext.RequestServices.GetRequiredService<FormatFilter>();
if (filter.GetFormat(ControllerContext) == null)
{
return View(model);
}
else
{
return new ObjectResult(model);
}
}
}
And then your controller can inherit from this and use the FormatOrView method
public class ProductsController : FormatController
{
[Route("[controller]/[action]/{id}.{format?}")]
public ActionResult GetById(int id)
{
var product = new { Id = id };
return FormatOrView(product);
}
}
Edit to list final accepted answer by GreyCloud: Here is a generic slightly simplified method you can put into a controller (or make an extension method or put into an abstract base class as above). Note the ?. in case the service is not defined for some reason.
private ActionResult<T> FormatOrView<T>(T model) {
return HttpContext.RequestServices.GetRequiredService<FormatFilter>()?.GetFormat(ControllerContext) == null
? View(model)
: new ActionResult<T>(model);
}
The FormatFilter is part of the content negotiation of your app, in AspNetCore, you have the control to handle your input or output formatters also on the ConfigureServices where you have more control, even you can add more media types there
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(options =>
{
options .OutputFormatters.Add(new XmlDataContractSerializerOutputFormatter());
options .InputFormatters.Add(new XmlDataContractSerializerInputFormatter(options ));
//more output formatters
var jsonOutputFormatter = options.OutputFormatters.OfType<JsonOutputFormatter>().FirstOrDefault();
if (jsonOutputFormatter != null)
{
jsonOutputFormatter.SupportedMediaTypes.Add("application/vnd.myvendormediatype");
}
}
}
But going back on the content negotiation in your controllers you can keep just one. The only thing is that you need to know the mediaType to return your View or your json content. Only be sure to pass an accept header with the content type you want. With the content type you are defining for an api or for an mvc application which is the content/format the client should expect
[HttpGet("[action]/{id}")]
public IActionResult public Product GetById(int id, [FromHeader(Name = "Accept")] string mediaType)
{
if (mediaType == "application/vnd.myvendormediatype")
{
var data = GetYourData(...)
return Json(data);
}
else return View("YourDefaultView");
}
In my webApi I have all methods located in one controller with routes attached to every method like this:
[HttpPost]
[Route("api/Users/Notary/Seal/Generate")]
public IHttpActionResult GenerateNotarySeal([FromBody]GenerateSealContract SealInfo)
{
return Ok(new RestResponse<byte[]>(buffer));
}
When I test my webApi in swagger it does a preflight OPTIONS request to make sure "Access-Control-Allow-Headers" is set right. To send a response to this, I do something like:
[HttpOptions]
[Route("api/Users/Notary/Seal/Generate")]
public IHttpActionResult OptionsHandler([FromBody]GenerateSealContract SealInfo)
{
///.... I set my custom headers in ActionFilterAttribute
return Ok();
}
But I do not want to duplicate this code for every method. Is there any way to do a wildcard for just the OPTIONS requests? Something like:
[HttpOptions]
[Route("api/*")]
public IHttpActionResult OptionsHandler()
{
///.... I set my custom headers in ActionFilterAttribute
return Ok();
}
First I think you should check the regular attribute routing constrains found here
Secondly what you want to do can be done but you have to implement your own Custom Route Constraint but it probably won't be a wildcard (*) symbol.
From Microsoft docs links above:
You can create custom route constraints by implementing the IHttpRouteConstraint interface. For example, the following constraint restricts a parameter to a non-zero integer value.
public class NonZeroConstraint : IHttpRouteConstraint
{
public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName,
IDictionary<string, object> values, HttpRouteDirection routeDirection)
{
object value;
if (values.TryGetValue(parameterName, out value) && value != null)
{
long longValue;
if (value is long)
{
longValue = (long)value;
return longValue != 0;
}
string valueString = Convert.ToString(value, CultureInfo.InvariantCulture);
if (Int64.TryParse(valueString, NumberStyles.Integer,
CultureInfo.InvariantCulture, out longValue))
{
return longValue != 0;
}
}
return false;
}
}
The following code shows how to register the constraint:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
var constraintResolver = new DefaultInlineConstraintResolver();
constraintResolver.ConstraintMap.Add("nonzero", typeof(NonZeroConstraint));
config.MapHttpAttributeRoutes(constraintResolver);
}
}
Now you can apply the constraint in your routes:
[Route("{id:nonzero}")]
public HttpResponseMessage GetNonZero(int id) { ... }
You can also implement a custom ActionFilter where you have access to the HttpRequst including the request headers
public class CustomActionFilter: ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
// Before Execution
}
public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
{
//On Execution
}
}
Then you can apply the CustomActionFilter
Controller Level
Action Level
Global Level: By registering it globally in web api config
I am trying to get Web API versioning working with an inherited class. I am working with two very simple variations of the stock Values controller.
[ApiVersion("1.0")]
[RoutePrefix("api/v{version:apiVersion}/Values")]
[ControllerName("Values")]
public class ValuesController : ApiController
{
// GET api/values
[Route("")]
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
// GET api/values/5
[Route("{id:int}")]
public virtual string Get(int id)
{
return "value from 1";
}
}
[ApiVersion("2.0")]
[RoutePrefix("api/v{version:apiVersion}/Values")]
[ControllerName("Values")]
public class Values2Controller : ValuesController
{
//Want to use the method in the base class
//public IEnumerable<string> Get()
//{
// return new string[] { "value2-1", "value2-2" };
// }
[Route("{id:int}")]
// GET api/values/5
public new string Get(int id)
{
return "value from 2";
}
}
My start up configuration is also pretty straightforward.
public static void Register(HttpConfiguration config)
{
var constraintResolver = new DefaultInlineConstraintResolver()
{
ConstraintMap = {["apiVersion"] = typeof(ApiVersionRouteConstraint)}
};
config.MapHttpAttributeRoutes(constraintResolver);
config.AddApiVersioning(o => { o.AssumeDefaultVersionWhenUnspecified = true; });
}
The non-overridden routes work exactly as I would expect
http://localhost:32623/api/v1.0/Values/12 -> "value from 1"
http://localhost:32623/api/v2.0/Values/12 -> "value from 2"
Calling v1 of the default Get Route
http://localhost:32623/api/v1.0/Values -> Value1, Value2
However trying the same route on the child controller results in an error.
http://localhost:32623/api/v2.0/Values
<Message>
The HTTP resource that matches the request URI 'http://localhost:32623/api/v2.0/Values' does not support the API version '2.0'.
</Message>
<InnerError>
<Message>
No route providing a controller name with API version '2.0' was found to match request URI 'http://localhost:32623/api/v2.0/Values'.
</Message>
</InnerError>
The error message suggests that the overridden member expects a "1.0" route and I am able to work around this with a method like this in the child class.
[Route("")]
public override IEnumerable<string> Get()
{
return base.Get();
}
But this seems less than ideal across a larger application. Is there a way to make this work the way I would like, without these "empty" overrides?
What you need to do is overwrite the DefaultDirectRoutePrivider to allow route inheritance:
public class WebApiCustomDirectRouteProvider : DefaultDirectRouteProvider {
protected override IReadOnlyList<IDirectRouteFactory>
GetActionRouteFactories(HttpActionDescriptor actionDescriptor) {
// inherit route attributes decorated on base class controller's actions
return actionDescriptor.GetCustomAttributes<IDirectRouteFactory>(inherit: true);
}
}
And with that done, you will then need to configure it in your web api configuration along with the custom route constraint
public static void Register(HttpConfiguration config) {
var constraintResolver = new DefaultInlineConstraintResolver() {
ConstraintMap = {["apiVersion"] = typeof(ApiVersionRouteConstraint)}
};
var directRouteProvider = new WebApiCustomDirectRouteProvider();
// Attribute routing. (with inheritance)
config.MapHttpAttributeRoutes(constraintResolver, directRouteProvider);
config.AddApiVersioning(_ => { _.AssumeDefaultVersionWhenUnspecified = true; });
}
So now the inheriting value controller will now have the base routes available in derived controllers
For demonstrative purposes
[ApiVersion("1.0")]
[RoutePrefix("api/v{version:apiVersion}/Values")]
[ControllerName("Values")]
public class ValuesController : ApiController {
[HttpGet]
[Route("")] // GET api/v1.0/values
public virtual IHttpActionResult Get() {
return Ok(new string[] { "value1", "value2" });
}
[HttpGet]
[Route("{id:int}")] // GET api/v1.0/values/5
public virtual IHttpActionResult Get(int id) {
return Ok("value from 1");
}
}
[ApiVersion("2.0")]
[RoutePrefix("api/v{version:apiVersion}/Values")]
[ControllerName("Values")]
public class Values2Controller : ValuesController {
//Will have inherited GET "api/v2.0/Values" route
// GET api/v2.0/values/5 (Route also inherited from base controller)
public override IHttpActionResult Get(int id) {
return Ok("value from 2");
}
}
You will notice that the route in the child is not used on the overridden action as it too will also be inherited from the base controller.