Search code examples
restasp.net-coreasp.net-mvc-routingasp.net-core-webapi

Attribute routing with default root parameter in WebAPI


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.


Solution

  • 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());
    });
    

    Demo

    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:

    1. The HTTP Request GET https://localhost:5001/cust/123/values will match the Get(int customerId) method.
    2. The HTTP Request POST https://localhost:5001/cust/123/values/opq will match the Post(int customerId) method
    3. Because we intentionally put a leading slash within /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].