模块化 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 应用程序的情况下添加或删除模块。
- 模块:每个模块都是一个程序集(.dll 文件),其中至少包含一个 class 派生自
ApiController
.
- 路由基于Attribute Routing in ASP.NET Web API 2
- 模块(组件)的生产不在本题范围内。
- 模块(程序集文件)被复制到项目根目录中的 `~/plugins/ 文件夹中/从中删除。这个过程也不在本题范围内
- 主要的 ASP.NET Web API 项目基本上只有一个控制器来管理 (add/remove) 模块。其他控制器将作为模块添加。
所以主 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()
,但没有解决问题(同样的错误)。
问题
- 这样的设计有意义吗?会成功吗?
- 如何向路由集合添加新路由或完全刷新路由集合?
我解决了
首先,感谢 @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
我正在尝试设计一个模块化的 Web API 应用程序(它不是 MVC 应用程序!),其中管理员角色的用户可以在不重新启动 ASP.NET 应用程序的情况下添加或删除模块。
- 模块:每个模块都是一个程序集(.dll 文件),其中至少包含一个 class 派生自
ApiController
. - 路由基于Attribute Routing in ASP.NET Web API 2
- 模块(组件)的生产不在本题范围内。
- 模块(程序集文件)被复制到项目根目录中的 `~/plugins/ 文件夹中/从中删除。这个过程也不在本题范围内
- 主要的 ASP.NET Web API 项目基本上只有一个控制器来管理 (add/remove) 模块。其他控制器将作为模块添加。
所以主 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()
,但没有解决问题(同样的错误)。
问题
- 这样的设计有意义吗?会成功吗?
- 如何向路由集合添加新路由或完全刷新路由集合?
我解决了
首先,感谢 @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