I have an API where all methods need a fixed parameter {customer} :
/cust/{customerId}/purchases
/cust/{customerId}/invoices
/cust/{customerId}/whatever*
How can I map all controllers to receive this parameter by default in a reusable way like:
endpoints.MapControllerRoute(name: "Default", pattern: "/cust/{customerId:int}/{controller}*"
I am using .net core 3.0 with the new .useEndpoints method on Startup.
You could create an implementation of ControllerModelConvention
to custom the attribute route behavior. For more details, see official docs.
For example, suppose you want to combine an attribute route convention (like /cust/{customerId:int}/[controller]
) with the existing attribute globally, simply create a Convention as below:
public class FixedCustomIdControllerConvention : IControllerModelConvention
{
public void Apply(ControllerModel controller)
{
var customerRouteModel= new AttributeRouteModel(){
Template="/cust/{customerId:int}",
};
var isApiController= controller.ControllerType.CustomAttributes.Select(c => c.AttributeType)
.Any(a => a == typeof(ApiControllerAttribute));
foreach (var selector in controller.Selectors)
{
if(!isApiController)
{
var oldAttributeRouteModel=selector.AttributeRouteModel;
var newAttributeRouteModel= oldAttributeRouteModel;
if(oldAttributeRouteModel != null){
newAttributeRouteModel= AttributeRouteModel.CombineAttributeRouteModel(customerRouteModel, oldAttributeRouteModel);
}
selector.AttributeRouteModel=newAttributeRouteModel;
} else{
// ApiController won't honor the by-convention route
// so I just replace the template
var oldTemplate = selector.AttributeRouteModel.Template;
if(! oldTemplate.StartsWith("/") ){
selector.AttributeRouteModel.Template= customerRouteModel.Template + "/" + oldTemplate;
}
}
}
}
}
And then register it in Startup:
services.AddControllersWithViews(opts =>{
opts.Conventions.Add(new FixedCustomIdControllerConvention());
});
Suppose we have a ValuesController
:
[Route("[controller]")]
public class ValuesController : Controller
{
[HttpGet]
public IActionResult Get(int customerId)
{
return Json(new {customerId});
}
[HttpPost("opq")]
public IActionResult Post(int customerId)
{
return Json(new {customerId});
}
[HttpPost("/rst")]
public IActionResult PostRst(int customerId)
{
return Json(new {customerId});
}
}
After registering the above FixedCustomIdControllerConvention
, the routing behavior is:
GET https://localhost:5001/cust/123/values
will match the Get(int customerId)
method.POST https://localhost:5001/cust/123/values/opq
will match the Post(int customerId)
method/rst
, the global convention is passed by. As a result, the POST https://localhost:5001/rst
will match the PostRst(int customerId)
method( with customId=0)In case you're using a controller annotated with [ApiController]
:
[ApiController]
[Route("[controller]")]
public class ApiValuesController : ControllerBase
{
[HttpGet]
public IActionResult Get([FromRoute]int customerId)
{
return new JsonResult(new {customerId});
}
[HttpPost("opq")]
public IActionResult Post([FromRoute]int customerId)
{
return new JsonResult(new {customerId});
}
[HttpPost("/apirst")]
public IActionResult PostRst([FromRoute]int customerId)
{
return new JsonResult(new {customerId});
}
}
You probably need decorate the parameter from routes with [FromRoute]
.