Dynamic route prefix for controllers in separate library

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.

  • It is based on one I wrote for adding culture prefixes, but the idea is very similar to the article you linked.
  • Basically it will either update any existing 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/");