MapMvcAttributeRoutes: This method cannot be called during the application's pre-start initialization phase - asp.net-mvc

I have a very simple test in a test project in a solution using ASP MVC V5 and attribute routing. Attribute routing and the MapMvcAttributeRoutes method are part of ASP MVC 5.
[Test]
public void HasRoutesInTable()
{
var routes = new RouteCollection();
routes.MapMvcAttributeRoutes();
Assert.That(routes.Count, Is.GreaterThan(0));
}
This results in:
System.InvalidOperationException :
This method cannot be called during the applications pre-start initialization phase.
Most of the answers to this error message involve configuring membership providers in the web.config file. This project has neither membership providers or a web.config file so the error seems be be occurring for some other reason. How do I move the code out of this "pre-start" state so that the tests can run?
The equivalent code for attributes on ApiController works fine after HttpConfiguration.EnsureInitialized() is called.

I recently upgraded my project to ASP.NET MVC 5 and experienced the exact same issue. When using dotPeek to investigate it, I discovered that there is an internal MapMvcAttributeRoutes extension method that has a IEnumerable<Type> as a parameter which expects a list of controller types. I created a new extension method that uses reflection and allows me to test my attribute-based routes:
public static class RouteCollectionExtensions
{
public static void MapMvcAttributeRoutesForTesting(this RouteCollection routes)
{
var controllers = (from t in typeof(HomeController).Assembly.GetExportedTypes()
where
t != null &&
t.IsPublic &&
t.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase) &&
!t.IsAbstract &&
typeof(IController).IsAssignableFrom(t)
select t).ToList();
var mapMvcAttributeRoutesMethod = typeof(RouteCollectionAttributeRoutingExtensions)
.GetMethod(
"MapMvcAttributeRoutes",
BindingFlags.NonPublic | BindingFlags.Static,
null,
new Type[] { typeof(RouteCollection), typeof(IEnumerable<Type>) },
null);
mapMvcAttributeRoutesMethod.Invoke(null, new object[] { routes, controllers });
}
}
And here is how I use it:
public class HomeControllerRouteTests
{
[Fact]
public void RequestTo_Root_ShouldMapTo_HomeIndex()
{
// Arrange
var routes = new RouteCollection();
// Act - registers traditional routes and the new attribute-defined routes
RouteConfig.RegisterRoutes(routes);
routes.MapMvcAttributeRoutesForTesting();
// Assert - uses MvcRouteTester to test specific routes
routes.ShouldMap("~/").To<HomeController>(x => x.Index());
}
}
One problem now is that inside RouteConfig.RegisterRoutes(route) I cannot call routes.MapMvcAttributeRoutes() so I moved that call to my Global.asax file instead.
Another concern is that this solution is potentially fragile since the above method in RouteCollectionAttributeRoutingExtensions is internal and could be removed at any time. A proactive approach would be to check to see if the mapMvcAttributeRoutesMethod variable is null and provide an appropriate error/exceptionmessage if it is.
NOTE: This only works with ASP.NET MVC 5.0. There were significant changes to attribute routing in ASP.NET MVC 5.1 and the mapMvcAttributeRoutesMethod method was moved to an internal class.

In ASP.NET MVC 5.1 this functionality was moved into its own class called AttributeRoutingMapper.
(This is why one shouldn't rely on code hacking around in internal classes)
But this is the workaround for 5.1 (and up?):
public static void MapMvcAttributeRoutes(this RouteCollection routeCollection, Assembly controllerAssembly)
{
var controllerTypes = (from type in controllerAssembly.GetExportedTypes()
where
type != null && type.IsPublic
&& type.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase)
&& !type.IsAbstract && typeof(IController).IsAssignableFrom(type)
select type).ToList();
var attributeRoutingAssembly = typeof(RouteCollectionAttributeRoutingExtensions).Assembly;
var attributeRoutingMapperType =
attributeRoutingAssembly.GetType("System.Web.Mvc.Routing.AttributeRoutingMapper");
var mapAttributeRoutesMethod = attributeRoutingMapperType.GetMethod(
"MapAttributeRoutes",
BindingFlags.Public | BindingFlags.Static,
null,
new[] { typeof(RouteCollection), typeof(IEnumerable<Type>) },
null);
mapAttributeRoutesMethod.Invoke(null, new object[] { routeCollection, controllerTypes });
}

Well, it's really ugly and I'm not sure if it'll be worth the test complexity, but here's how you can do it without modifying your RouteConfig.Register code:
[TestClass]
public class MyTestClass
{
[TestMethod]
public void MyTestMethod()
{
// Move all files needed for this test into a subdirectory named bin.
Directory.CreateDirectory("bin");
foreach (var file in Directory.EnumerateFiles("."))
{
File.Copy(file, "bin\\" + file, overwrite: true);
}
// Create a new ASP.NET host for this directory (with all the binaries under the bin subdirectory); get a Remoting proxy to that app domain.
RouteProxy proxy = (RouteProxy)ApplicationHost.CreateApplicationHost(typeof(RouteProxy), "/", Environment.CurrentDirectory);
// Call into the other app domain to run route registration and get back the route count.
int count = proxy.RegisterRoutesAndGetCount();
Assert.IsTrue(count > 0);
}
private class RouteProxy : MarshalByRefObject
{
public int RegisterRoutesAndGetCount()
{
RouteCollection routes = new RouteCollection();
RouteConfig.RegisterRoutes(routes); // or just call routes.MapMvcAttributeRoutes() if that's what you want, though I'm not sure why you'd re-test the framework code.
return routes.Count;
}
}
}
Mapping attribute routes needs to find all the controllers you're using to get their attributes, which requires accessing the build manager, which only apparently works in app domains created for ASP.NET.

What are you testing here? Looks like you are testing a 3rd party extension method. You shouldn't be using your unit tests to test 3rd party code.

Related

How to dynamically add a controller in a ASP.NET Core 6 MVC application

I need to dynamically creates controllers in a ASP.NET Core 6 MVC application.
I found some way to somewhat achieve this but not quite.
I'm able to dynamically add my controller but somehow it reflects only on the second request.
So here is what I do: first I initialize my console app as follows:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Mvc.Infrastructure;
namespace DynamicControllerServer
{
internal class Program
{
static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
ApplicationPartManager partManager = builder.Services.AddMvc().PartManager;
// Store thePartManager in my Middleware to be able to add controlelr after initialization is done
MyMiddleware._partManager = partManager;
// Register controller change event
builder.Services.AddSingleton<IActionDescriptorChangeProvider>(MyActionDescriptorChangeProvider.Instance);
builder.Services.AddSingleton(MyActionDescriptorChangeProvider.Instance);
var app = builder.Build();
app.UseAuthorization();
app.MapControllers();
// Add Middleware which is responsible to cactn the request and dynamically add the missing controller
app.UseMiddleware<MyMiddleware>();
app.RunAsync();
Console.WriteLine("Server has been started successfully ...");
Console.ReadLine();
}
}
}
Then my middleware looks like this: it basically detects that there is the "dynamic" keyword in the url. If so, it will load the assembly containing the DynamicController:
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using System;
using System.Reflection;
namespace DynamicControllerServer
{
public class MyMiddleware
{
public RequestDelegate _next { get; }
private string dllName = "DynamicController1.dll";
static public ApplicationPartManager _partManager = null;
public MyMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext httpContext)
{
if (httpContext.Request.Path.HasValue)
{
var queryParams = httpContext.Request.Path.Value;
if(httpContext.Request.Path.Value.Contains("api/dynamic"))
{
// Dynamically load assembly
Assembly assembly = assembly = Assembly.LoadFrom(#"C:\Temp\" + dllName);
// Add controller to the application
AssemblyPart _part = new AssemblyPart(assembly);
_partManager.ApplicationParts.Add(_part);
// Notify change
MyActionDescriptorChangeProvider.Instance.HasChanged = true;
MyActionDescriptorChangeProvider.Instance.TokenSource.Cancel();
}
}
await _next(httpContext); // calling next middleware
}
}
}
The ActionDescriptorChange provider looks like this:
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.Primitives;
namespace DynamicControllerServer
{
public class MyActionDescriptorChangeProvider : IActionDescriptorChangeProvider
{
public static MyActionDescriptorChangeProvider Instance { get; } = new MyActionDescriptorChangeProvider();
public CancellationTokenSource TokenSource { get; private set; }
public bool HasChanged { get; set; }
public IChangeToken GetChangeToken()
{
TokenSource = new CancellationTokenSource();
return new CancellationChangeToken(TokenSource.Token);
}
}
}
Dynamic controller is in separate dll and is very simple:
using Microsoft.AspNetCore.Mvc;
namespace DotNotSelfHostedOwin
{
[Route("api/[controller]")]
[ApiController]
public class DynamicController : ControllerBase
{
public string[] Get()
{
return new string[] { "dynamic1", "dynamic1", DateTime.Now.ToString() };
}
}
}
Here are the packages used in that project:
<PackageReference Include="Microsoft.AspNetCore" Version="2.2.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
This works "almost" fine ... when first request is made to:
https://localhost:5001/api/dynamic
then it goes in the middleware and load the assembly, but returns a 404 error.
Then second request will actually work as expected:
Second request returns the expected result:
I must doing it wrong and probably my middleware is executed too late in the flow to reflect the dynamic controller right away.
Question is: what should be the proper way to achieve this?
Second question I have is say now the external dll holding our dynamic controller is updated.
How can I reload that controller to get the new definition?
Any help would be appreciated
Thanks in advance
Nick
Here is the answer to my own question in case it can help somebody out there.
It seems building and loading the controller from the middleware will always end up with failure on the first call.
This makes sense since we are already in the http pipeline.
I end up doing same thing from outside the middleware.
Basically my application detect a change in the controller assembly, unload the original assembly and load the new one.
You cannot use the Default context since it will not allow reloading different dll for same assembly:
var assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(assemblyPath); // Produce an exception on updates
To be able to reload new dll for same assembly, I’m loading each controller in its own assembly context. To do that you need to create your own class deriving from AssemblyLoadContext and managing assembly load:
public class MyOwnContext: AssemblyLoadContext
{
// You can find lots of example in the net
}
When you want to unload the assembly, you just unload the context:
MyOwnContextObj.Unload();
Now to add or remove the controller on the fly, you need to keep reference of the PartManager and the ApplicationPart.
To add controller
ApplicationPart part = new AssemblyPart(assembly);
_PartManager.ApplicationParts.Add(part);
To remove:
_PartManager.ApplicationParts.Remove(part);
On course once done, still use following piece of code to acknowledge the change:
MyActionDescriptorChangeProvider.Instance.HasChanged = true;
MyActionDescriptorChangeProvider.Instance.TokenSource.Cancel();
That allow updating controller on the fly with no interruption of service.
Hope this helps people out there.
I have done a similar solution (used for managing a web app plugins) with some differences that may help you:
List all the external assemblies in a config file or appsettings.json so all the dll names and/or addresses are known at startup
Instead of registering controllers when they are called, register them at program.cs/start up :
//Foreah dllName from settings file
var assembly = Assembly.LoadFrom(#"Base address" + dllNameLoadedFromSettings);
var part = new AssemblyPart(assembly);
services.AddControllersWithViews()
.ConfigureApplicationPartManager(apm => apm.ApplicationParts.Add(part));
// Any other configuration based on the usage you want
Second: I usually keep plugin dlls in the bin folder so when using IIS as soon as a dll file in bin is changed the upper-level app is automatically reset. So your second question would be solved too.

Difference between RouteCollection.Ignore and RouteCollection.IgnoreRoute?

What's the difference between RouteCollection.Ignore(url, constraints) and RouteCollection.IgnoreRoute(url, constraints)?
Background
New MVC projects include this IgnoreRoute call in Global.asax RegisterRoutes method to skip routing for requests to .axd locations that are handled elsewhere in the ASP.NET system.
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
I wanted to add an additional ignored route to a project and I started to type out the new line. After routes.I, Intellisense pops up with .Ignore and .IgnoreRoute, both sounding about the same.
According to the MSDN docs, you can see that one is an instance method of the System.Web.Routing.RouteCollection class and the other is an extension method on that class from System.Web.Mvc.RouteCollectionExtensions.
RouteCollection.Ignore: "Defines a URL pattern that should not be checked for matches against routes if a request URL meets the specified constraints" (MSDN docs).
RouteCollection.IgnoreRoute: "Ignores the specified URL route for the given list of the available routes and a list of constraints" (MSDN docs).
Both take a route URL pattern and a set of constraints restricting the application of the route on that URL pattern.
Between the source for System.Web.Mvc.RouteCollectionExtensions on CodePlex and running a little ILSpy on my local GAC for System.Web.Routing.RouteCollection, it doesn't appear there is a difference, though they seem to have completely independent code to do the same thing.
RouteCollection.IgnoreRoute (via CodePlex source)
public static void IgnoreRoute(this RouteCollection routes, string url, object constraints) {
if (routes == null) {
throw new ArgumentNullException("routes");
}
if (url == null) {
throw new ArgumentNullException("url");
}
IgnoreRouteInternal route = new IgnoreRouteInternal(url) {
Constraints = new RouteValueDictionary(constraints)
};
routes.Add(route);
}
RouteCollection.Ignore (via ILSpy decompile)
public void Ignore(string url, object constraints) {
if (url == null) {
throw new ArgumentNullException("url");
}
RouteCollection.IgnoreRouteInternal item = new RouteCollection.IgnoreRouteInternal(url) {
Constraints = new RouteValueDictionary(constraints)
};
base.Add(item);
}
Differences
The only real difference is the obvious difference in location, one being an instance method in the RouteCollection class itself and one being an extensions method on that class. After you factor in the code differences that come from instance vs. extension execution (like the vital null check on the extended instance), they appear identical.
At their core, they both use the exact same StopRoutingHandler class. Both have their own versions of a sealed IgnoreRouteInternal class, but those versions are identical in code.
private sealed class IgnoreRouteInternal : Route {
public IgnoreRouteInternal(string url)
: base(url, new StopRoutingHandler()) {
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary routeValues) {
return null;
}
}

Plugin controllers, StructureMap and ASP.NET MVC

I'm using ASP.NET MVC (1.0) and StructureMap (2.5.3), I'm doing a plugin feature where dll's with controller are to be picked up in a folder. I register the controllers with SM (I am able to pick it up afterwards, so I know it's in there)
foreach (string file in path)
{
var assy = System.Reflection.Assembly.LoadFile(file);
Scan(x =>{
x.Assembly(assy);
x.AddAllTypesOf<IController>();
});
}
My problem is with the GetControllerInstance method of my override of DefaultControllerFactory. Everytime I send in enything else than a valid controller (valid in the sense that it is a part of the web project) I get the input Type parameter as null.
I've tried setting up specific routes for it.
I've done a test with Castle.Windsor and there it is not a problem.
Can anyone point me in the right direction? I'd appreciate it.
[Edit]
Here is the code:
-> Controller factory for Windsor
public WindsorControllerFactory()
{
container = new WindsorContainer(new XmlInterpreter(
new ConfigResource("castle")));
// Register all the controller types as transient
// This is for the regular controllers
var controllerTypes =
from t in
Assembly.GetExecutingAssembly().GetTypes()
where typeof(IController).IsAssignableFrom(t)
select t;
foreach (Type t in controllerTypes)
{
container.AddComponentLifeStyle(t.FullName, t,
LifestyleType.Transient);
}
/* Now the plugin controllers */
foreach (string file in Plugins() )
{
var assy = System.Reflection.Assembly.LoadFile(file);
var pluginContr =
from t in assy.GetTypes()
where typeof(IController).IsAssignableFrom(t)
select t;
foreach (Type t in pluginContr)
{
AddToPlugins(t);
/* This is the only thing I do, with regards to Windsor,
for the plugin Controllers */
container.AddComponentLifeStyle(t.FullName, t,
LifestyleType.Transient);
}
}
}
-> StructureMap; adding the controllers:
public class PluginRegistry : Registry
{
public PluginRegistry()
{
foreach (string file in Plugins() ) // Plugins return string[] of assemblies in the plugin folder
{
var assy = System.Reflection.Assembly.LoadFile(file);
Scan(x =>
{
x.Assembly(assy);
//x.AddAllTypesOf<IController>().
// NameBy(type => type.Name.Replace("Controller", ""));
x.AddAllTypesOf<IController>();
});
}
}
}
-> Controller factory for SM version
Not really doing much, as I'm registering the controllers with SM in the earlier step
public SMControllerFactory()
: base()
{
foreach (string file in Plugins() )
{
var assy = System.Reflection.Assembly.LoadFile(file);
var pluginContr =
from t in assy.GetTypes()
where typeof(IController).IsAssignableFrom(t)
select t;
foreach (Type t in pluginContr)
{
AddPlugin();
}
}
}
Can you post your controller factory?
I don't understand why Castle would work since I would think you would also get null passed in for the Type param of GetControllerInstance regardless of the DI framework you use inside that method. MVC is in charge of matching up the string name of the controller in the URL with a real type (unless you overrode those methods too). So I'm guessing it isn't the DI framework, but that MVC can't find your controller classes for some reason.

Can I store ASP.NET MVC routes in web.config?

I'm looking for a method of storing routing information in my web.config file in addition to the Global.asax class. The routes stored in the configuration file would need to take higher precedence than those added programmatically.
I've done my searching, but the closest I can come up with is the RouteBuilder on Codeplex (http://www.codeplex.com/RouteBuilder), but this doesn't work with the RTM version of MVC. Does a solution out there exist compatible with the final 1.0?
I can't guarantee the following code will work, but it builds :) Change the Init method in RouteBuilder.cs to the following:
public void Init(HttpApplication application)
{
// Grab the Routes from Web.config
RouteConfiguration routeConfig =
(RouteConfiguration)System.Configuration.ConfigurationManager.GetSection("RouteTable");
// Add each Route to RouteTable
foreach (RouteElement routeElement in routeConfig.Routes)
{
RouteValueDictionary defaults = new RouteValueDictionary();
string[] defaultsArray = routeElement.Defaults.Trim().Split(',');
if (defaultsArray.Length > 0)
{
foreach (string defaultEntry in defaultsArray)
{
string[] defaultsEntryArray = defaultEntry.Trim().Split('=');
if ((defaultsEntryArray.Length % 2) != 0)
{
throw new ArgumentException("RouteBuilder: All Keys in Defaults must have values!");
}
else
{
defaults.Add(defaultsEntryArray[0], defaultsEntryArray[1]);
}
}
}
else
{
throw new ArgumentException("RouteBuilder: Defaults value is empty or malformed.");
}
Route currentRoute = new Route(routeElement.Url, defaults, new MvcRouteHandler());
RouteTable.Routes.Add(currentRoute);
}
}
Also, feel free to delete the DefaultsType class. It was needed because the defaults system was much more complicated back in CTP than in RTM.
Edit: Oh and add using System.Web.Routing; to the top and make sure you add System.Web.Mvc and System.Web.Routing as references.
look at this:
http://weblogs.asp.net/fredriknormen/archive/2008/03/11/asp-net-mvc-framework-2-define-routes-in-web-config.aspx

Asp.Net MVC - Best approach for "dynamic" routing

I am trying to come up with an approach to create "dynamic" routing. What I mean, exactly, is that I want to be able to assign the controller and action of a route for each hit rather than having it mapped directly.
For example, a route may look like this "path/{object}" and when that path is hit, a lookup is performed providing the appropriate controller / action to call.
I've tried discovering the mechanisms for creating a custom route handler, but the documentation / discoverability is a bit shady at the moment (I know, its beta - I wouldn't expect any more). Although, I'm not sure if thats even the best approach and perhaps a controller factory or even a default controller/action that performs all of the mappings may be the best route (no pun intended) to go.
Any advice would be appreciated.
You can always use a catch all syntax ( I have no idea if the name is proper).
Route:
routeTable.MapRoute(
"Path",
"{*path}",
new { controller = "Pages", action = "Path" });
Controller action is defined as:
public ActionResult Path(string path)
In the action for controller you will have a path, so just have to spilt it and analyse.
To call another controller you can use a RedirectToAction ( I think this is more proper way). With redirection you can set up a permanent redirectionfor it.
Or use a something like that:
internal class MVCTransferResult : RedirectResult
{
public MVCTransferResult(string url) : base(url)
{
}
public MVCTransferResult(object routeValues)
: base(GetRouteURL(routeValues))
{
}
private static string GetRouteURL(object routeValues)
{
UrlHelper url = new UrlHelper(
new RequestContext(
new HttpContextWrapper(HttpContext.Current),
new RouteData()),
RouteTable.Routes);
return url.RouteUrl(routeValues);
}
public override void ExecuteResult(ControllerContext context)
{
var httpContext = HttpContext.Current;
// ASP.NET MVC 3.0
if (context.Controller.TempData != null &&
context.Controller.TempData.Count() > 0)
{
throw new ApplicationException(
"TempData won't work with Server.TransferRequest!");
}
// change to false to pass query string parameters
// if you have already processed them
httpContext.Server.TransferRequest(Url, true);
// ASP.NET MVC 2.0
//httpContext.RewritePath(Url, false);
//IHttpHandler httpHandler = new MvcHttpHandler();
//httpHandler.ProcessRequest(HttpContext.Current);
}
}
However this method require to run on IIS or a IIS Expres Casinni is not supporting a Server.Transfer method

Resources