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.
ApiController
.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).
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
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