I'd like to have controllers with default Route URLs and HTTP method generated by custom code and not require any HttpPost|Get|etc
attributes (route actions will default to Post
). I can do this using Microsoft.AspNetCore.Mvc.ApplicationModels.IActionModelConvention
.
However I cannot then use [ApiController]
on the controller since there is no explicit attribute routing - I get the error:
Controller Action> does not have an attribute route. Action methods on controllers annotated with ApiControllerAttribute must be attribute routed
Is there a way to dynamically add default routes and http methods to controller actions AND use the [ApiController]
attribute?
Related questions
Conventional Routing in ASP.NET Core API - this is close - but I want to have all the niceties/defaults of [ApiController]
- i.e. not having to set [FromBody]
for params, etc
Core requirement
I'd like to create an OpenAPI backend and for use on an JS SPA client code gen on the front end. Since the API is only consumed by the SPA, and the client API will be auto code gen - I don't really care about hardcoded routes, or even using "correct" HTTP methods (everything could just be a POST). So I'd like to get rid of all that boilerplate in my controllers.
Current thoughts/ideas
The I*Convention
s aren't applied until after Microsoft.AspNetCore.Mvc.ApplicationModels.ApiBehaviorApplicationModelProvider.EnsureActionIsAttributeRouted
validation is executed. So even though I apply a AttributeRouteModel
in the convention, it isn't included in the validation:
actionSelector.AttributeRouteModel = new AttributeRouteModel
{
Template = $"foo/bar/{Guid.NewGuid():N}",
};
My next idea is to implement some sort of Microsoft.AspNetCore.Mvc.ApplicationModels.IApplicationModelProvider
(which is how the [ApiController]
seems to be validated - see: Microsoft.AspNetCore.Mvc.ApplicationModels.ApiBehaviorApplicationModelProvider
) - and try to make sure it's registered and executed before the ApiBehaviorApplicationModelProvider
?
If that doesn't work then it seems the option would be to get even deeper into something like Microsoft.AspNetCore.Mvc.Abstractions.IActionDescriptorProvider
- but that seems pretty scary.
Cross Posted: https://github.com/dotnet/aspnetcore/discussions/52526
I ended up using the IApplicationModelProvider
strategy. I just had to make sure to set the Order
property to run before the ApiBehaviorApplicationModelProvider
- so I ended up with:
/// <summary>
/// [ApiController] attribute model provider uses <c>-1000 + 100</c> -
/// this needs to be applied first so that we get attribute routing.
/// <see cref="AuthorizationApplicationModelProvider"/> uses +10 - so we're trying
/// to split the difference here.
/// </summary>
public int Order => -1000 + 50;
And register it as a service in the app startup:
...
builder.Services.AddTransient<IApplicationModelProvider, MyFancyApplicationModelProvider>();
...
And, if anyone is interested, here is how I am adding Routes and HttpMethods to the Actions (it seems to work...):
public void OnProvidersExecuting(ApplicationModelProviderContext context)
{
var actionSelectors = context.Result.Controllers
.SelectMany(
c => c.Actions.SelectMany(
a => a.Selectors.Select(
selector => (
controller: c,
action: a, selector))));
foreach (var actionSelector in actionSelectors)
{
var selectorModel = actionSelector.selector;
var actionDebugName = $"{actionSelector.controller.ControllerName}:{actionSelector.action.ActionName}";
if (selectorModel.AttributeRouteModel == null)
{
var endpointUrl = // ... magic here to create the URL
_logger.LogDebug(
"Endpoint has no attribute route - adding default ({Action}): {Url}",
actionDebugName,
endpointUrl);
selectorModel.AttributeRouteModel = new AttributeRouteModel
{
Template = endpointUrl,
};
}
else
{
_logger.LogInformation(
"{Endpoint}: Endpoint has existing attribute route - will not add a default",
actionDebugName);
}
/*
* Add a default HTTP Method to the action (POST) if not specified
*/
var httpMethodConstraint =
(HttpMethodActionConstraint?)selectorModel.ActionConstraints.FirstOrDefault(
c => c is HttpMethodActionConstraint);
/*
* Debug out the HTTP Methods
*/
var httpMethods = httpMethodConstraint == null
? "NONE"
: string.Join(",", httpMethodConstraint.HttpMethods);
_logger.LogDebug("Existing HTTP Methods: {HttpMethods}", httpMethods);
if (httpMethodConstraint == null)
{
_logger.LogDebug("{Endpoint}: Adding default http method", actionDebugName);
selectorModel.ActionConstraints.Add(
new HttpMethodActionConstraint(new[] { DefaultHttpMethod }));
}
else
{
_logger.LogInformation("{Endpoint}: already has http action", actionDebugName);
}
}
}