I'm developing an MVC API in a separate class library. The API methods use attribute routing. The API will be used by other MVC applications (not built by me).
The main MVC application will reference my library assembly and call AddMvc()
/ UseMvc()
in it's own startup class. It will be able to set the root API url's for my API library dynamically (from configuration or options setup delegate), so that it can make sure there are no conflicts with it's own routes, which can use either attribute routing or centralized routing.
So let's say my API library has a product/{id}
route. The main application should be able to choose any route prefix, like api/product/{id}
or some/other/prefix/product/{id}
.
At startup, MVC will discover all controllers/routes in all referenced assemblies, and it will also discover and register my API library routes, but only on the hardcoded product/{id}
route without any prefix.
I've been trying to get MVC to register the routes with a prefix, but so far no success. The main application will call custom AddMyApi()
/ UseMyApi()
config methods, so I can do configuration / setup for my library. Some of the things I tried:
Mapping
app.Map("/custom-prefix", api =>
{
api.UseMvc();
});
This will result in duplicate routes for both custom-prefix/product/{id}
and product/{id}
.
Route Convention
Based on http://www.strathweb.com/2016/06/global-route-prefix-with-asp-net-core-mvc-revisited/
services.AddMvc(options =>
{
options.Conventions.Insert(0, new RouteConvention(new RouteAttribute("custom-prefix")));
});
It looks like this will not work because the options will be overwritten by the main application's call to AddMvc()
, or the other way around, depending which gets called first.
Custom route attribute
A custom route attribute based on IRouteTemplateProvider
on the Controller classes will not work because I need the prefix injected from an options class, and attributes do not support constructor injection.
Postpone discovery of routes
Based on http://www.strathweb.com/2015/04/asp-net-mvc-6-discovers-controllers/
I've added [NonController]
to the library controllers to prevent them being discovered at the main application's startup. However I've not been able to add them later, and also I suppose I will run into the same problem of the main application overwriting the MVC options again.
Areas
I can't use areas, because the main application may decide to run the API from the root (without prefix).
So I'm stuck as to how to solve this problem. Any help is appreciated.
I believe a convention is the right approach here and the bit you are missing is just providing the proper extension method for your library to be registered within MVC.
Start by creating a convention that will add a prefix to all controllers that pass a certain selector.
AttributeRouteModel
or add a new one if none is found.This would be an example of such a convention:
public class ApiPrefixConvention: IApplicationModelConvention
{
private readonly string prefix;
private readonly Func<ControllerModel, bool> controllerSelector;
private readonly AttributeRouteModel onlyPrefixRoute;
private readonly AttributeRouteModel fullRoute;
public ApiPrefixConvention(string prefix, Func<ControllerModel, bool> controllerSelector)
{
this.prefix = prefix;
this.controllerSelector = controllerSelector;
// Prepare AttributeRouteModel local instances, ready to be added to the controllers
// This one is meant to be combined with existing route attributes
onlyPrefixRoute = new AttributeRouteModel(new RouteAttribute(prefix));
// This one is meant to be added as the route for api controllers that do not specify any route attribute
fullRoute = new AttributeRouteModel(
new RouteAttribute("api/[controller]"));
}
public void Apply(ApplicationModel application)
{
// Loop through any controller matching our selector
foreach (var controller in application.Controllers.Where(controllerSelector))
{
// Either update existing route attributes or add a new one
if (controller.Selectors.Any(x => x.AttributeRouteModel != null))
{
AddPrefixesToExistingRoutes(controller);
}
else
{
AddNewRoute(controller);
}
}
}
private void AddPrefixesToExistingRoutes(ControllerModel controller)
{
foreach (var selectorModel in controller.Selectors.Where(x => x.AttributeRouteModel != null).ToList())
{
// Merge existing route models with the api prefix
var originalAttributeRoute = selectorModel.AttributeRouteModel;
selectorModel.AttributeRouteModel =
AttributeRouteModel.CombineAttributeRouteModel(onlyPrefixRoute, originalAttributeRoute);
}
}
private void AddNewRoute(ControllerModel controller)
{
// The controller has no route attributes, lets add a default api convention
var defaultSelector = controller.Selectors.First(s => s.AttributeRouteModel == null);
defaultSelector.AttributeRouteModel = fullRoute;
}
}
Now, if this was all part of an app you are writing instead of a library, you would just register it as:
services.AddMvc(opts =>
{
var prefixConvention = new ApiPrefixConvention("api/", (c) => c.ControllerType.Namespace == "WebApplication2.Controllers.Api");
opts.Conventions.Insert(0, prefixConvention);
});
However since you are providing a library, what you want is to provide an extension method like AddMyLibrary("some/prefix")
that will take care of adding this convention and any other setup like registering required services.
So you can write an extension method for IMvcBuilder
and update the MvcOptions
inside that method. The nice thing is that since is an extension of IMvcBuilder
, it will always be called after the default AddMvc()
:
public static IMvcBuilder AddMyLibrary(this IMvcBuilder builder, string prefix = "api/")
{
// instantiate the convention with the right selector for your library.
// Check for namespace, marker attribute, name pattern, whatever your prefer
var prefixConvention = new ApiPrefixConvention(prefix, (c) => c.ControllerType.Namespace == "WebApplication2.Controllers.Api");
// Insert the convention within the MVC options
builder.Services.Configure<MvcOptions>(opts => opts.Conventions.Insert(0, prefixConvention));
// perform any extra setup required by your library, like registering services
// return builder so it can be chained
return builder;
}
Then you would ask users of your library to include it within their app as in:
services.AddMvc().AddMyLibrary("my/api/prefix/");
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With