Can I store ASP.NET MVC routes in web.config? - asp.net-mvc

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

Related

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

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.

Can MVC Views be transformed for multiple deployements like web.config files can?

In one of my MVC projects, I have a special configuration setup for a test deployment site. Doing this, I was able to add a config tranformation to override various settings in my web.config file. For example, I have the following files:
web.config
web.release.config
web.debug.config
web.testsite.config
When I deploy to my test site, it now overwrites some settings specified in my web.testsite.config
Is it possible to get the same behavior on some of my views? For example, could I have a Index.testsite.cshtml? I could toggle behavior on and off with flags from the configuration, however it seems like a cleaner approach would be to allow for additional transformations/replacement views based on configuration.
This is actually easy to do.
*global.asax - Inside Application_Start()*
var displayModes = DisplayModeProvider.Instance.Modes;
displayModes.Insert(0, new DefaultDisplayMode("TestSite")
{
ContextCondition = (context => IsTestSite())
});
Definition of IsTestSite()
public bool IsTestSite()
{
bool isTestSite;
return bool.TryParse(ConfigurationManager.AppSettings["isTestSite"], out isTestSite);
}
That's it, now your app will use Intex.TestSite.cshtml if present otherwise it will serve Index.cshtml. The same holds true for any other view name as well, just stick TestSite before the extension.
Add to your Base/Controller:
protected override void OnResultExecuting(ResultExecutingContext filterContext)
{
var viewResult = filterContext.Result as ViewResult;
if (viewResult != null)
{
string env = ... // determine your environment somehow
var razorEngine = viewResult.ViewEngineCollection.OfType<RazorViewEngine>().Single();
var viewName = !String.IsNullOrEmpty(viewResult.ViewName) ? viewResult.ViewName : filterContext.RouteData.Values["action"].ToString();
var razorView = razorEngine.FindView(filterContext.Controller.ControllerContext, viewName, viewResult.MasterName, false).View as RazorView;
var currentPath = razorView.ViewPath;
var newPath = currentPath.Replace(".cshtml", env + ".cshtml");
if (razorEngine.FileExists(filterContext.Controller.ControllerContext, newPath))
viewResult.View = new RazorView(filterContext.Controller.ControllerContext, newPath, razorView.LayoutPath, razorView.RunViewStartPages, razorView.ViewStartFileExtensions);
}
base.OnResultExecuting(filterContext);
}
Also, if you're using MVC 4 (hence WebPages 2.0), you can use DisplayModeProvider to achieve this easily.
In your Global.asax:
protected void Application_Start()
{
DisplayModeProvider.Instance.Modes.Add(new DefaultDisplayMode("debug")
{
ContextCondition = (context => context.IsDebuggingEnabled)
});
DisplayModeProvider.Instance.Modes.Add(new DefaultDisplayMode("test")
{
ContextCondition = (context => context.Request.IsLocal)
});
}
You might be able to implement a custom action filter that checks your configuration setting and serves up the correct view based on its value.

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;
}
}

Best practice for ASP.NET MVC resource files

What are the best usage of the following resource files.
Properties → Resources (Phil used this resource for localization in DataAnnotation)
App_GlobalResources folder
App_LocalResources folder
I also would like to know what is the difference between (1) and (2) in asp.net mvc application.
You should avoid App_GlobalResources and App_LocalResources.
Like Craig mentioned, there are problems with App_GlobalResources/App_LocalResources because you can't access them outside of the ASP.NET runtime. A good example of how this would be problematic is when you're unit testing your app.
K. Scott Allen blogged about this a while ago. He does a good job of explaining the problem with App_GlobalResources in ASP.NET MVC here.
If you go with the recommended solution (1) (i.e. as in K. Scott Allen's blog):
For those of you trying to use explicit localization expressions (aka declarative resource binding expressions), e.g. <%$ Resources, MyResource:SomeString %>
public class AppResourceProvider : IResourceProvider
{
private readonly string _ResourceClassName;
ResourceManager _ResourceManager = null;
public AppResourceProvider(string className)
{
_ResourceClassName = className;
}
public object GetObject(string resourceKey, System.Globalization.CultureInfo culture)
{
EnsureResourceManager();
if (culture == null)
{
culture = CultureInfo.CurrentUICulture;
}
return _ResourceManager.GetObject(resourceKey, culture);
}
public System.Resources.IResourceReader ResourceReader
{
get
{
// Not needed for global resources
throw new NotSupportedException();
}
}
private void EnsureResourceManager()
{
var assembly = typeof(Resources.ResourceInAppToGetAssembly).Assembly;
String resourceFullName = String.Format("{0}.Resources.{1}", assembly.GetName().Name, _ResourceClassName);
_ResourceManager = new global::System.Resources.ResourceManager(resourceFullName, assembly);
_ResourceManager.IgnoreCase = true;
}
}
public class AppResourceProviderFactory : ResourceProviderFactory
{
// Thank you, .NET, for providing no way to override global resource providing w/o also overriding local resource providing
private static Type ResXProviderType = typeof(ResourceProviderFactory).Assembly.GetType("System.Web.Compilation.ResXResourceProviderFactory");
ResourceProviderFactory _DefaultFactory;
public AppResourceProviderFactory()
{
_DefaultFactory = (ResourceProviderFactory)Activator.CreateInstance(ResXProviderType);
}
public override IResourceProvider CreateGlobalResourceProvider(string classKey)
{
return new AppResourceProvider(classKey);
}
public override IResourceProvider CreateLocalResourceProvider(string virtualPath)
{
return _DefaultFactory.CreateLocalResourceProvider(virtualPath);
}
}
Then, add this to your web.config:
<globalization requestEncoding="utf-8" responseEncoding="utf-8" fileEncoding="utf-8" culture="en-US" uiCulture="en"
resourceProviderFactoryType="Vendalism.ResourceProvider.AppResourceProviderFactory" />
Properties → Resources can be seen outside of your views and strong types are generated when you compile your application.
App_* is compiled by ASP.NET, when your views are compiled. They're only available in the view. See this page for global vs. local.

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