Can MVC route resolution mechanism be re-configured? - asp.net-mvc

I have implemented custom VirtualPathProvider to serve customizable Views from a DB and when i put a breakpoint on the FileExists method I noticed that the framework does ton of unnecessary (for my project) requests. For example when I make a request for non-existing action (e.g. http://localhost/Example/Action) the framework looks for:
"~/Example/Action/5"
"~/Example/Action/5.cshtml"
"~/Example/Action/5.vbhtml"
"~/Example/Action.cshtml"
"~/Example/Action.vbhtml"
"~/Example.cshtml"
"~/Example.vbhtml"
"~/Example/Action/5/default.cshtml"
"~/Example/Action/5/default.vbhtml"
"~/Example/Action/5/index.cshtml"
"~/Example/Action/5/index.vbhtml"
"~/favicon.ico"
"~/favicon.ico.cshtml"
"~/favicon.ico.vbhtml"
"~/favicon.ico/default.cshtml"
"~/favicon.ico/default.vbhtml"
"~/favicon.ico/index.cshtml"
"~/favicon.ico/index.vbhtml"
When I make a request that matches an added route (e.g http://localhost/Test) the framework looks for:
"~/Test"
"~/Test.cshtml"
"~/Test.vbhtml"
"~/Test/default.cshtml"
"~/Test/default.vbhtml"
"~/Test/index.cshtml"
"~/Test/index.vbhtml"
before even initialising the controller. After the controller is initialised the framework looks for the view as defined in the custom RazorViewEngine that I have implemented.
This is my ViewEngine
AreaViewLocationFormats = new string[] { };
AreaMasterLocationFormats = new string[] { };
AreaPartialViewLocationFormats = new string[] { };
MasterLocationFormats = new string[] { };
ViewLocationFormats = new string[] {
"~/Views/Dynamic/{1}/{0}.cshtml",
"~/Views/Dynamic/Shared/{0}.cshtml",
"~/Views/{1}/{0}.cshtml",
"~/Views/Shared/{0}.cshtml"
};
PartialViewLocationFormats = new string[] {
"~/Views/Dynamic/{1}/Partial/{0}.cshtml",
"~/Views/Dynamic/Shared/Partial/{0}.cshtml",
"~/Views/{1}/Partial/{0}.cshtml",
"~/Views/Shared/Partial/{0}.cshtml"
};
FileExtensions = new string[] { "cshtml" };
So the question is can these default routes be removed and how?

Could they be related to the RouteCollection.RouteExistingFiles property? I doesn't make sense for it to check for lots of files rather than just one that matches, but it might be worth turning off to see if it makes any difference.

Related

MvcSiteMapProvider + Autofac + ISiteMapNodeVisibilityProvider from another assembly

I'm having the toughest time figuring out how to register a custom ISiteMapNodeVisibilityProvider (SiteMapNodeVisibilityProviderBase) using Autofac in MvcSiteMapProvider.
Everything was working fine up until the point that I moved the visibility provider to another assembly. Now, no matter what I try, I always get
The visibility provider instance named 'MyWasWorkingVisibilityProvider, MyNewAssembly' was not found. Check your DI configuration to ensure a visibility provider instance with this name exists and is configured correctly.
According to the MvcSiteMapProvider documentation and code, it appears I need to somehow into the SiteMapNodeVisibilityProviderStrategy... and I think I've done that below... But I'm no Autofac ninja.
In MvcSiteMapProviderModule.cs, I added the new assembly everywhere I could think...
string[] includeAssembliesForScan = new string[] { "MyOldAssembly", "MyNewAssembly" };
var allAssemblies = new Assembly[] { currentAssembly, siteMapProviderAssembly, typeof(MyWasWorkingVisibilityProvider).Assembly };
builder.RegisterType<SiteMapNodeVisibilityProviderStrategy>()
.As<ISiteMapNodeVisibilityProviderStrategy>()
.WithParameters(new List<Parameter> { new NamedParameter("defaultProviderName", string.Empty), new NamedParameter("siteMapNodeVisibilityProviders", new [] { new MyWasWorkingVisibilityProvider() }) });
builder.RegisterType<MyWasWorkingVisibilityProvider>()
.As<ISiteMapNodeVisibilityProvider>();
But it still doesn't work.
For what it's worth, the visibility provider for any specific menu item is configured in the database, and the entire menu structure is loaded with a dynamic node provider that is also now in the same assembly as where I've moved the visibility providers. The dynamic node provider is obviously working because it's getting all the way to the point where it's trying to load visibility providers.
I thought https://github.com/maartenba/MvcSiteMapProvider/issues/237 looked helpful, I couldn't get the visibility provider-specific code to compile..
Another example that didn't have any effect: MVC Site Map Provider - SiteMapPath Performance Very Slow?
So I'm stuck now. I'm not a wizard with Autofac OR MvcSiteMap provider, but, like I said, everything was working fine until I moved the visibility provider to another assembly.
Thanks very much for your time and attention! I'm frustrated at this point.
Just a hunch, but I suspect that you didn't update all of the visibilityProvider="MyNamespace.MyVisibilityProvider, MyAssembly" references in your configuration to the new assembly. The SiteMapNodeVisibilityProviderBase uses the .NET full type name to locate the correct type, including the assembly name.
// From MvcSiteMapProvider.SiteMapNodeVisibilityProviderBase
public virtual bool AppliesTo(string providerName)
{
if (string.IsNullOrEmpty(providerName))
return false;
return this.GetType().Equals(Type.GetType(providerName, false));
}
As for the DI registration, provided you left the first call to CommonConventions.RegisterAllImplementationsOfInterface() in place, you had it right with this line:
var allAssemblies = new Assembly[] { currentAssembly, siteMapProviderAssembly, typeof(MyWasWorkingVisibilityProvider).Assembly };
So the code should look something like this:
var allAssemblies = new Assembly[] { currentAssembly, siteMapProviderAssembly, typeof(MyWasWorkingVisibilityProvider).Assembly };
var excludeTypes = new Type[] {
// Use this array to add types you wish to explicitly exclude from convention-based
// auto-registration. By default all types that either match I[TypeName] = [TypeName] or
// I[TypeName] = [TypeName]Adapter will be automatically wired up as long as they don't
// have the [ExcludeFromAutoRegistrationAttribute].
//
// If you want to override a type that follows the convention, you should add the name
// of either the implementation name or the interface that it inherits to this list and
// add your manual registration code below. This will prevent duplicate registrations
// of the types from occurring.
// Example:
// typeof(SiteMap),
// typeof(SiteMapNodeVisibilityProviderStrategy)
};
var multipleImplementationTypes = new Type[] {
typeof(ISiteMapNodeUrlResolver),
typeof(ISiteMapNodeVisibilityProvider),
typeof(IDynamicNodeProvider)
};
// Matching type name (I[TypeName] = [TypeName]) or matching type name + suffix Adapter (I[TypeName] = [TypeName]Adapter)
// and not decorated with the [ExcludeFromAutoRegistrationAttribute].
CommonConventions.RegisterDefaultConventions(
(interfaceType, implementationType) => builder.RegisterType(implementationType).As(interfaceType).SingleInstance(),
new Assembly[] { siteMapProviderAssembly },
allAssemblies,
excludeTypes,
string.Empty);
// Multiple implementations of strategy based extension points (and not decorated with [ExcludeFromAutoRegistrationAttribute]).
CommonConventions.RegisterAllImplementationsOfInterface(
(interfaceType, implementationType) => builder.RegisterType(implementationType).As(interfaceType).SingleInstance(),
multipleImplementationTypes,
allAssemblies,
excludeTypes,
string.Empty);
// Registration of internal controllers
CommonConventions.RegisterAllImplementationsOfInterface(
(interfaceType, implementationType) => builder.RegisterType(implementationType).As(interfaceType).AsSelf().InstancePerDependency(),
new Type[] { typeof(IController) },
new Assembly[] { siteMapProviderAssembly },
new Type[0],
string.Empty);
So, in short your DI configuration is right, but your node configuration of the VisibilityProvider attribute/property is not.
NOTE: The below line is only for scanning for the [MvcSiteMapNode] attribute on controllers that may not be in the same project as MvcSiteMapProvider, and has nothing to do with the setup of visibility providers.
string[] includeAssembliesForScan = new string[] { "MyOldAssembly", "MyNewAssembly" };

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.

How test that ASP.NET MVC route redirects to other site?

Due to a prinitng error in some promotional material I have a site that is receiving a lot of requests which should be for one site arriving at another.
i.e.
The valid sites are http://site1.com/abc & http://site2.com/def but people are being told to go to http://site1.com/def.
I have control over site1 but not site2.
site1 contains logic for checking that the first part of the route is valid in an actionfilter, like this:
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
base.OnActionExecuting(filterContext);
if ((!filterContext.ActionParameters.ContainsKey("id"))
|| (!manager.WhiteLabelExists(filterContext.ActionParameters["id"].ToString())))
{
if (filterContext.ActionParameters["id"].ToString().ToLowerInvariant().Equals("def"))
{
filterContext.HttpContext.Response.Redirect("http://site2.com/def", true);
}
filterContext.Result = new ViewResult { ViewName = "NoWhiteLabel" };
filterContext.HttpContext.Response.Clear();
}
}
I'm not sure how to test the redirection to the other site though.
I already have tests for redirecting to "NoWhiteLabel" using the MvcContrib Test Helpers, but these aren't able to handle (as far as I can see) this situation.
How do I test the redirection to antoher site?
I would recommend you using RedirectResult instead of calling Response.Redirect:
if (youWantToRedirect)
{
filterContext.Result = new RedirectResult("http://site2.com/def")
}
else
{
filterContext.Result = new ViewResult { ViewName = "NoWhiteLabel" };
}
Now if you know how to test ViewResult with MVCContrib TestHelper you will be able to test the RedirectResult the same way. The tricky part is mocking the manager to force it to satisfy the if condition.
UPDATE:
Here's how a sample test might look like:
// arrange
var mock = new MockRepository();
var controller = mock.StrictMock<Controller>();
new TestControllerBuilder().InitializeController(controller);
var sut = new MyFilter();
var aec = new ActionExecutingContext(
controller.ControllerContext,
mock.StrictMock<ActionDescriptor>(),
new Dictionary<string, object>());
// act
sut.OnActionExecuting(aec);
// assert
aec.Result.ShouldBe<RedirectResult>("");
var result = (RedirectResult)aec.Result;
result.Url.ShouldEqual("http://site2.com/def", "");
Update (By Matt Lacey)
Here's how I actually got this working:
// arrange
var mock = new MockRepository();
// Note that in the next line I create an actual instance of my real controller - couldn't get a mock to work correctly
var controller = new HomeController(new Stubs.BlankContextInfoProvider(), new Stubs.BlankWhiteLabelManager());
new TestControllerBuilder().InitializeController(controller);
var sut = new UseBrandedViewModelAttribute(new Stubs.BlankWhiteLabelManager());
var aec = new ActionExecutingContext(
controller.ControllerContext,
mock.StrictMock<ActionDescriptor>(),
// being sure to specify the necessary action parameters
new Dictionary<string, object> { { "id", "def" } });
// act
sut.OnActionExecuting(aec);
// assert
aec.Result.ShouldBe<RedirectResult>("");
var result = (RedirectResult)aec.Result;
result.Url.ShouldEqual("http://site2.com/def", "");

MasterLocationFormats in WebFormViewEngine not used?

I tried to make the ViewEngine use an additional path using:
base.MasterLocationFormats = new string[] {
"~/Views/AddedMaster.Master"
};
in the constructor of the ViewEngine. It works well for aspx and ascx(PartialViewLocationFormats, ViewLocationFormats).
I still have to supply the MasterPage in web.config or in the page declaration. But if I do, then this declaration is used, not the one in the ViewEngine.
If I use am empty MasterLocationFormats, no error is thrown. Is this not implemeted in RC1?
EDIT:
using:
return View("Index", "AddedMaster");
instead of
return View("Index");
in the Controller worked.
Your example isn't really complete, but I am going to guess that your block of code exists at the class level and not inside of a constructor method. The problem with that is that the base class (WebFormViewEngine) initializes the "location format" properties in a constructor, hence overriding your declaration;
public CustomViewEngine()
{
MasterLocationFormats = new string[] {
"~/Views/AddedMaster.Master"
};
}
If you want the hard-coded master to only kick in as a sort of last effort default, you can do something like this:
public CustomViewEngine()
{
MasterLocationFormats = new List<string>(MasterLocationFormats) {
"~/Views/AddedMaster.Master"
}.ToArray();
}

Change lookup rule for views

I have an application that gets rolled out in multiple countries. There will be a setting in the web.config file, that defines the country.
The country will not be in the URL.
Some of the the views change depending on the country.
My first attempt is to use a folder inside the views folder that contains views, if they differ from the default view:
Default
/questions/ask.aspx
Spain
/questions/ESP/ask.aspx
If there is no view in the country-folder the default view is used. Is there a way to extend the ViewEngine to lookup views in the country folder first?
EDIT:
This is a poc only. To see a full implementation have a look at
http://pietschsoft.com/?tag=/mvc
private static string[] LocalViewFormats =
new string[] {
"~/Views/{1}/ESP/{0}.aspx",
"~/Views/{1}/{0}.aspx",
"~/Views/{1}/{0}.ascx",
"~/Views/Shared/{0}.aspx",
"~/Views/Shared/{0}.ascx"
};
public LocalizationWebFormViewEngine()
{
ViewLocationFormats = LocalViewFormats;
}
public class MyViewEngine : WebFormViewEngine
{
private static string[] LocalViewFormats = new[] { "~/Views/ESP/{0}.aspx",
"~/Views/ESP/{0}.ascx" };
public MyViewEngine()
{
ViewLocationFormats = LocalViewFormats.Union(ViewLocationFormats).ToArray();
}
}
Obviously, you don't want to hardcode the location, but this should give you the general idea.

Resources