模块化 ASP.NET Web API:如何 add/remove 在 运行 时间路由到 Web API

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

我正在尝试设计一个模块化的 Web API 应用程序(它不是 MVC 应用程序!),其中管理员角色的用户可以在不重新启动 ASP.NET 应用程序的情况下添加或删除模块。

所以主 Web API 项目中唯一的控制器是:

[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() 只是使用 AppDomain.CurrentDomain.Load() 查找程序集并将其加载到应用程序域。这部分效果很好。

Configuration.MapHttpAttributeRoutes() 不会引发任何错误,但会破坏整个路由系统。在该行之后,任何路由尝试都会导致此错误:

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

我在代码中添加了HttpConfiguration.EnsureInitialized(),但没有解决问题(同样的错误)。

问题

  1. 这样的设计有意义吗?会成功吗?
  2. 如何向路由集合添加新路由或完全刷新路由集合?

我解决了

首先,感谢 @Aleksey L.,对 ModuleController 做了一点改动(添加 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
}

那么我们应该扩展 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);
    }
}

当然我们必须用我们的 HttpControllerSelector 替换默认的 HttpControllerSelector,在 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();
    }
}

如果有人对我如何实现 ModuleService 感兴趣,我可以将代码上传到 GitHub。

这是 GitHub 中的完整源代码:https://github.com/tohidazizi/modular-web-api-poc