Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Modular ASP.NET Web API: how to add/remove route at run time to a Web API

I am trying to design a modular Web API application (It is not an MVC app!) in which a user in admin role can add or remove modules without restarting the ASP.NET application.

  • Module: each module is an assembly (.dll file) that contains at least one class which is derived from ApiController.
  • Routing is based on Attribute Routing in ASP.NET Web API 2
  • Production of modules (assemblies) is not in the scope of this question.
  • Modules (assembly files) are copied to / deleted from `~/plugins/ folder in the root of the project. This process is not in the scope of this question either.
  • The main ASP.NET Web API project has basically only one controller to manage (add/remove) modules. The other controllers will be added as modules.

So the only controller in the main Web API project is:

[RoutePrefix("api/modules")]
public class ModulesController : ApiController
{
    private ModuleService _moduleService = new ModuleService();

    // GET: api/Modules
    [Route]
    public IEnumerable<string> Get()
    {
        return _moduleService.Get().Select(a => a.FullName);
    }

    // POST: api/Modules/{moduleName}
    [Route("{id}")]
    public void Post(string id)
    {
        Assembly _assembly;
        var result = _moduleService.TryLoad(id, out _assembly);

        if(!result) throw new Exception("problem loading " + id);

        // Refresh routs or add the new rout
        Configuration.Routes.Clear();
        Configuration.MapHttpAttributeRoutes();
        // ^ it does not work :(
    }

    // DELETE: api/Modules/{moduleName}
    [Route("{id}")]
    public void Delete(string id)
    {
        _moduleService.Remove(id);
    }
}

ModuleService.TryLoad() simply finds and loads the assembly to the application domain by using AppDomain.CurrentDomain.Load(). This part is working well.

Configuration.MapHttpAttributeRoutes() doesn't raise any error, but it breaks the whole routing system. After that line, any routing attempt causes this error:

The object has not yet been initialized. Ensure that HttpConfiguration.EnsureInitialized() is called in the application's startup code after all other initialization code.

I added HttpConfiguration.EnsureInitialized() to the code, but it didn't resolve the problem (same error).

Questions

  1. Does this design make any sense? Will it work?
  2. How can I add a new route to the route collection or refresh the route collection completely?
like image 203
Tohid Avatar asked Aug 22 '16 21:08

Tohid


1 Answers

I solved it.

Firstly, and thanks to @Aleksey L., a little change to the ModuleController (adding Configuration.Initializer(Configuration)):

[RoutePrefix("api/modules")]
public class ModulesController : ApiController
{
    private ModuleService _moduleService = new ModuleService();

    // Other codes

    public void Post(string id)
    {
        _moduleService.Load(id);

        Configuration.Routes.Clear();
        Configuration.MapHttpAttributeRoutes();
        Configuration.Initializer(Configuration);
    }

    // Other codes
}

Then we should extend DefaultHttpControllerSelector:

public class ModularHttpControllerSelector : DefaultHttpControllerSelector
{
    private readonly HttpConfiguration _configuration;

    public ModularHttpControllerSelector(HttpConfiguration configuration)
        : base(configuration)
    {
        _configuration = configuration;
    }

    public override IDictionary<string, HttpControllerDescriptor> GetControllerMapping()
    {
        var result = base.GetControllerMapping();
        AddPluginsControllerMapping(ref result);
        return result;
    }

    private void AddPluginsControllerMapping(ref IDictionary<string, HttpControllerDescriptor> controllerMappings)
    {
        var custom_settings = _getControllerMapping();

        foreach (var item in custom_settings)
        {
            if (controllerMappings.ContainsKey(item.Key))
                controllerMappings[item.Key] = item.Value;
            else
                controllerMappings.Add(item.Key, item.Value);
        }
    }

    private ConcurrentDictionary<string, HttpControllerDescriptor> _getControllerMapping()
    {
        var result = new ConcurrentDictionary<string, HttpControllerDescriptor>(StringComparer.OrdinalIgnoreCase);
        var duplicateControllers = new HashSet<string>();
        Dictionary<string, ILookup<string, Type>> controllerTypeGroups = GetControllerTypeGroups();

        foreach (KeyValuePair<string, ILookup<string, Type>> controllerTypeGroup in controllerTypeGroups)
        {
            string controllerName = controllerTypeGroup.Key;

            foreach (IGrouping<string, Type> controllerTypesGroupedByNs in controllerTypeGroup.Value)
            {
                foreach (Type controllerType in controllerTypesGroupedByNs)
                {
                    if (result.Keys.Contains(controllerName))
                    {
                        duplicateControllers.Add(controllerName);
                        break;
                    }
                    else
                    {
                        result.TryAdd(controllerName, new HttpControllerDescriptor(_configuration, controllerName, controllerType));
                    }
                }
            }
        }

        foreach (string duplicateController in duplicateControllers)
        {
            HttpControllerDescriptor descriptor;
            result.TryRemove(duplicateController, out descriptor);
        }

        return result;
    }

    private Dictionary<string, ILookup<string, Type>> GetControllerTypeGroups()
    {
        IAssembliesResolver assembliesResolver = new DefaultAssembliesResolver(); //was: _configuration.Services.GetAssembliesResolver();
        IHttpControllerTypeResolver controllersResolver = new DefaultHttpControllerTypeResolver(); //was: _configuration.Services.GetHttpControllerTypeResolver();

        ICollection<Type> controllerTypes = controllersResolver.GetControllerTypes(assembliesResolver);
        var groupedByName = controllerTypes.GroupBy(
            t => t.Name.Substring(0, t.Name.Length - DefaultHttpControllerSelector.ControllerSuffix.Length),
            StringComparer.OrdinalIgnoreCase);

        return groupedByName.ToDictionary(
            g => g.Key,
            g => g.ToLookup(t => t.Namespace ?? String.Empty, StringComparer.OrdinalIgnoreCase),
            StringComparer.OrdinalIgnoreCase);
    }
}

And of course we have to replace the default HttpControllerSelector with our HttpControllerSelector, in the App_start\WebApiConfig.cs:

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        GlobalConfiguration.Configuration.Services.Replace(
            typeof(System.Web.Http.Dispatcher.IHttpControllerSelector),
            new ModularHttpControllerSelector(config));

        config.MapHttpAttributeRoutes();
    }
}

If anybody is interested in how I implemented the ModuleService, I can upload the code to GitHub.

Here is the whole source code in the GitHub: https://github.com/tohidazizi/modular-web-api-poc

like image 139
Tohid Avatar answered Nov 15 '22 08:11

Tohid