I was wondering if anybody can help me with bundling and minifying using the new optimization namespace shipped with MVC 4.
I have a Multitenant-application in which I want to decide which js files should be loaded based on settings per user. One approach would be to create all bundles upfront and change the virtual path of resolvebundleurl based on the setting of the user, but that feels not really the right way.
Also I have dynamic css in a cshtml view based on user-settings, which I would like to have minified in runtime.
Any suggestions? I also see a lot of reactions in other questions to check out Requestreduce, but they are all from the same user.
What would be the best approach to handle both situations?
Thanks in advance!
One approach you can take is building the bundle dynamically when the application starts. So if your scripts are located in ~/scripts you can do:
Bundle bundle = new Bundle("~/scripts/js", new JsMinify());
if (includeJquery == true) {
bundle.IncludeDirectory("~/scripts", "jquery-*");
bundle.IncludeDirectory("~/scripts", "jquery-ui*");
}
if (includeAwesomenes == true) {
bundle.IncludeDirectory("~/scripts", "awesomeness.js");
}
BundleTable.Bundles.Add(bundle);
Then your markup can look like this
#Scripts.Render("~/Scripts/Libs/js")
Note: I'm using the latest nuget package for system.web.optimization (now Microsoft.AspNet.Web.Optimization) located here. Scott Hanselman has a good post about it.
i wrote a helper function to dynamic minify my css & js
public static IHtmlString RenderStyles(this HtmlHelper helper, params string[] additionalPaths)
{
var page = helper.ViewDataContainer as WebPageExecutingBase;
if (page != null && page.VirtualPath.StartsWith("~/"))
{
var virtualPath = "~/bundles" + page.VirtualPath.Substring(1);
if (BundleTable.Bundles.GetBundleFor(virtualPath) == null)
{
var defaultPath = page.VirtualPath + ".css";
BundleTable.Bundles.Add(new StyleBundle(virtualPath).Include(defaultPath).Include(additionalPaths));
}
return MvcHtmlString.Create(#"<link href=""" + HttpUtility.HtmlAttributeEncode(BundleTable.Bundles.ResolveBundleUrl(virtualPath)) + #""" rel=""stylesheet""/>");
}
return MvcHtmlString.Empty;
}
public static IHtmlString RenderScripts(this HtmlHelper helper, params string[] additionalPaths)
{
var page = helper.ViewDataContainer as WebPageExecutingBase;
if (page != null && page.VirtualPath.StartsWith("~/"))
{
var virtualPath = "~/bundles" + page.VirtualPath.Substring(1);
if (BundleTable.Bundles.GetBundleFor(virtualPath) == null)
{
var defaultPath = page.VirtualPath + ".js";
BundleTable.Bundles.Add(new ScriptBundle(virtualPath).Include(defaultPath).Include(additionalPaths));
}
return MvcHtmlString.Create(#"<script src=""" + HttpUtility.HtmlAttributeEncode(BundleTable.Bundles.ResolveBundleUrl(virtualPath)) + #"""></script>");
}
return MvcHtmlString.Empty;
}
usage
~/views/Home/Test1.cshtml
~/Views/Home/Test1.cshtml.css
~/Views/Home/Test1.cshtml.js
in Test1.cshtml
#model object
#{
// init
}#{
}#section MainContent {
{<div>#{
if ("work" != "fun")
{
{<hr/>}
}
}</div>}
}#{
}#section Scripts {#{
{#Html.RenderScripts()}
}#{
}#section Styles {#{
{#Html.RenderStyles()}
}}
but ofcoz, i put most of my sripts,styles in ~/Scripts/.js, ~/Content/.css
and register them in Appp_Start
We considered supporting dynamic bundles early on, but the fundamental issue with that approach is multi server scenarios (i.e. cloud) won't work. If all bundles are not defined in advance, any bundle requests that get sent to a different server than the one that served the page request will get 404 response(as the bundle definition would only exist on server that handled the page request). As a result, I would suggest creating all bundles up front, that's the mainline scenario. Dynamic configuration of bundles might work as well, but that is not a fully supported scenario.
Update: Not sure if it matters but I am using MVC 5.2.3 and Visual Studio 2015, question is a little old.
However I made dynamic bundling that works in _viewStart.cshtml. What I did was I made a helper class that stores bundles in a dictionary of bundles. Then at app start I pull them from the dictionary and register them. And I made a static boolen "bundlesInitialzed" so that the bundles only add to the dictionary once.
Example Helper:
public static class KBApplicationCore: .....
{
private static Dictionary<string, Bundle> _bundleDictionary = new Dictionary<string, Bundle>();
public static bool BundlesFinalized { get { return _BundlesFinalized; } }
/// <summary>
/// Add a bundle to the bundle dictionary
/// </summary>
/// <param name="bundle"></param>
/// <returns></returns>
public static bool RegisterBundle(Bundle bundle)
{
if (bundle == null)
throw new ArgumentNullException("bundle");
if (_BundlesFinalized)
throw new InvalidOperationException("The bundles have been finalized and frozen, you can only finalize the bundles once as an app pool recycle is needed to change the bundles afterwards!");
if (_bundleDictionary.ContainsKey(bundle.Path))
return false;
_bundleDictionary.Add(bundle.Path, bundle);
return true;
}
/// <summary>
/// Finalize the bundles, which commits them to the BundleTable.Bundles collection, respects the web.config's debug setting for optimizations
/// </summary>
public static void FinalizeBundles()
{
FinalizeBundles(null);
}
/// <summary>
/// Finalize the bundles, which commits them to the BundleTable.Bundles collection
/// </summary>
/// <param name="forceMinimize">Null = Respect web.config debug setting, True force minification regardless of web.config, False force no minification regardless of web.config</param>
public static void FinalizeBundles(bool? forceMinimize)
{
var bundles = BundleTable.Bundles;
foreach (var bundle in _bundleDictionary.Values)
{
bundles.Add(bundle);
}
if (forceMinimize != null)
BundleTable.EnableOptimizations = forceMinimize.Value;
_BundlesFinalized = true;
}
}
Example _ViewStart.cshtml
#{
var bundles = BundleTable.Bundles;
var baseUrl = string.Concat("~/App_Plugins/", KBApplicationCore.PackageManifest.FolderName, "/");
//Maybe there is a better way to do this, the goal is to make the bundle configurable without having to recompile the code
if (!KBApplicationCore.BundlesFinalized)
{
//Note, you need to reset the application pool in order for any changes here to be reloaded as the BundlesFinalized property is a static field that will only reset to false when the app restarts.
Bundle mainScripts = new ScriptBundle("~/bundles/scripts/main.js");
mainScripts.Include(new string[] {
baseUrl + "Assets/lib/jquery/jquery.js",
baseUrl + "Assets/lib/jquery/plugins/jqcloud/jqcloud.js",
baseUrl + "Assets/lib/bootstrap/js/bootstrap.js",
baseUrl + "Assets/lib/bootstrap/plugins/treeview/bootstrap-treeview.js",
baseUrl + "Assets/lib/angular/angular.js",
baseUrl + "Assets/lib/ckEditor/ckEditor.js"
});
KBApplicationCore.RegisterBundle(mainScripts);
Bundle appScripts = new ScriptBundle("~/bundles/scripts/app.js");
appScripts.Include(new string[] {
baseUrl + "Assets/app/app.js",
baseUrl + "Assets/app/services/*.js",
baseUrl + "Assets/app/directives/*.js",
baseUrl + "Assets/app/controllers/*.js"
});
KBApplicationCore.RegisterBundle(appScripts);
Bundle mainStyles = new StyleBundle("~/bundles/styles/main.css");
mainStyles.Include(new string[] {
baseUrl + "Assets/lib/bootstrap/build/less/bootstrap.less",
baseUrl + "Assets/lib/bootstrap/plugins/treeview/bootstrap-treeview.css",
baseUrl + "Assets/lib/ckeditor/contents.css",
baseUrl + "Assets/lib/font-awesome/less/font-awesome.less",
baseUrl + "Assets/styles/tlckb.less"
});
mainStyles.Transforms.Add(new BundleTransformer.Core.Transformers.CssTransformer());
mainStyles.Transforms.Add(new CssMinify());
mainStyles.Orderer = new BundleTransformer.Core.Orderers.NullOrderer();
KBApplicationCore.RegisterBundle(mainStyles);
KBApplicationCore.FinalizeBundles(true); //true = Force Optimizations, false = Force non Optmizations, null = respect web.config which is the same as calling the parameterless constructor.
}
}
Note: This should be updated to use thread locking to prevent 2 requests entering the bundle code before the first one exits.
The way this works is the view start runs on the first request to the site after an app pool reset. It calls the RegisterBundle on the helper and passes the ScriptBundle or StyleBundle to the dictionary in the order RegisterBundles is called.
When FinalizeBundles is called you can specify True which will force optimizations regardless of web.config debug setting, or leave it null or use the constructor without that parameter to have it respect the web.config setting. Passing false will force it to use no optimization even if debug is true. FinalizeBundles Registers the bundles in the bundles table and set's _BundlesFinalized to true.
Once finalized, an attempt to call RegisterBundle again will throw an exception, it's frozen at that point.
This setup allows you to add new bundles to view start and reset the app pool to get them to take effect. The original goal I had writing this was because I am making something others will use so I wanted them to be able to completely change the front end UI without having to rebuild the source to change the bundles.
Related
We are transitioning from Xamarin.Forms to .Net MAUI but our project uses Prism.Unity.Forms. We have a lot of code that basically uses the IContainer.Resolve() passing in a collection of ParameterOverrides with some primitives but some are interfaces/objects. The T we are resolving is usually a registered View which may or may not be the correct way of doing this but it's what I'm working with and we are doing it in backend code (sometimes a service). What is the correct way of doing this Unity thing in DryIoC? Note these parameters are being set at runtime and may only be part of the parameters a constructor takes in (some may be from already registered dependencies).
Example of the scenario:
//Called from service into custom resolver method
var parameterOverrides = new[]
{
new ParameterOverride("productID", 8675309),
new ParameterOverride("objectWithData", IObjectWithData)
};
//Custom resolver method example
var resolverOverrides = new List<ResolverOverride>();
foreach(var parameterOverride in parameterOverrides)
{
resolverOverrides.Add(parameterOverride);
}
return _container.Resolve<T>(resolverOverrides.ToArray());
You've found out why you don't use the container outside of the resolution root. I recommend not trying to replicate this error with another container but rather fixing it - use handcoded factories:
internal class SomeFactory : IProductViewFactory
{
public SomeFactory( IService dependency )
{
_dependency = dependency ?? throw new ArgumentNullException( nameof(dependency) );
}
#region IProductViewFactory
public IProductView Create( int productID, IObjectWithData objectWithData ) => new SomeProduct( productID, objectWithData, _dependency );
#endregion
#region private
private readonly IService _dependency;
#endregion
}
See this, too:
For dependencies that are independent of the instance you're creating, inject them into the factory and store them until needed.
For dependencies that are independent of the context of creation but need to be recreated for each created instance, inject factories into the factory and store them.
For dependencies that are dependent on the context of creation, pass them into the Create method of the factory.
Also, be aware of potential subtle differences in container behaviours: Unity's ResolverOverride works for the whole call to resolve, i.e. they override parameters of dependencies, too, whatever happens to match by name. This could very well be handled very differently by DryIOC.
First, I would agree with the #haukinger answer to rethink how do you pass the runtime information into the services. The most transparent and simple way in my opinion is by passing it via parameters into the consuming methods.
Second, here is a complete example in DryIoc to solve it head-on + the live code to play with.
using System;
using DryIoc;
public class Program
{
record ParameterOverride(string Name, object Value);
record Product(int productID);
public static void Main()
{
// get container somehow,
// if you don't have an access to it directly then you may resolve it from your service provider
IContainer c = new Container();
c.Register<Product>();
var parameterOverrides = new[]
{
new ParameterOverride("productID", 8675309),
new ParameterOverride("objectWithData", "blah"),
};
var parameterRules = Parameters.Of;
foreach (var po in parameterOverrides)
{
parameterRules = parameterRules.Details((_, x) => x.Name.Equals(po.Name) ? ServiceDetails.Of(po.Value) : null);
}
c = c.With(rules => rules.With(parameters: parameterRules));
var s = c.Resolve<Product>();
Console.WriteLine(s.productID);
}
}
I have many AOP libraries that use Castle DynamicProxy with Autofac DI container for logging, auditing, transaction control, etc.
I wonder if there is a way to declare interceptors using the default .NET Core DI container. It will be good to have this flexibility since many .NET Core projects don't use Autofac.
Yes, you can use DynamicProxy using Core DI. I've written up a blog post explaining it at http://codethug.com/2021/03/17/Caching-with-Attributes-in-DotNet-Core5/, but here is the code for it:
Create an attribute
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class CacheAttribute : Attribute
{
public int Seconds { get; set; } = 30;
}
Create an interceptor (requires Castle.Core nuget package)
public class CacheInterceptor : IInterceptor
{
private IMemoryCache _memoryCache;
public CacheInterceptor(IMemoryCache memoryCache)
{
_memoryCache = memoryCache;
}
// Create a cache key using the name of the method and the values
// of its arguments so that if the same method is called with the
// same arguments in the future, we can find out if the results
// are cached or not
private static string GenerateCacheKey(string name,
object[] arguments)
{
if (arguments == null || arguments.Length == 0)
return name;
return name + "--" +
string.Join("--", arguments.Select(a =>
a == null ? "**NULL**" : a.ToString()).ToArray());
}
public void Intercept(IInvocation invocation)
{
var cacheAttribute = invocation.MethodInvocationTarget
.GetCustomAttributes(typeof(CacheAttribute), false)
.FirstOrDefault() as CacheAttribute;
// If the cache attribute is added ot this method, we
// need to intercept this call
if (cacheAttribute != null)
{
var cacheKey = GenerateCacheKey(invocation.Method.Name,
invocation.Arguments);
if (_memoryCache.TryGetValue(cacheKey, out object value))
{
// The results were already in the cache so return
// them from the cache instead of calling the
// underlying method
invocation.ReturnValue = value;
}
else
{
// Get the result the hard way by calling
// the underlying method
invocation.Proceed();
// Save the result in the cache
var options = new MemoryCacheEntryOptions
{
AbsoluteExpirationRelativeToNow =
new System.TimeSpan(hours: 0, minutes: 0,
seconds: cacheAttribute.Seconds)
};
_memoryCache.Set(cacheKey, invocation.ReturnValue,
options);
}
}
else
{
// We don't need to cache the results,
// nothing to see here
invocation.Proceed();
}
}
}
Add an extension method to help register classes in DI:
public static void AddProxiedScoped<TInterface, TImplementation>
(this IServiceCollection services)
where TInterface : class
where TImplementation : class, TInterface
{
// This registers the underlying class
services.AddScoped<TImplementation>();
services.AddScoped(typeof(TInterface), serviceProvider =>
{
// Get an instance of the Castle Proxy Generator
var proxyGenerator = serviceProvider
.GetRequiredService<ProxyGenerator>();
// Have DI build out an instance of the class that has methods
// you want to cache (this is a normal instance of that class
// without caching added)
var actual = serviceProvider
.GetRequiredService<TImplementation>();
// Find all of the interceptors that have been registered,
// including our caching interceptor. (you might later add a
// logging interceptor, etc.)
var interceptors = serviceProvider
.GetServices<IInterceptor>().ToArray();
// Have Castle Proxy build out a proxy object that implements
// your interface, but adds a caching layer on top of the
// actual implementation of the class. This proxy object is
// what will then get injected into the class that has a
// dependency on TInterface
return proxyGenerator.CreateInterfaceProxyWithTarget(
typeof(TInterface), actual, interceptors);
});
}
Add these lines to ConfigureServices in Startup.cs
// Setup Interception
services.AddSingleton(new ProxyGenerator());
services.AddScoped<IInterceptor, CacheInterceptor>(
After that, if you want to use the cache interceptor, you need to do two things:
First, add the attribute to your method
[Cache(Seconds = 30)]
public async Task<IEnumerable<Person>> GetPeopleByLastName(string lastName)
{
return SomeLongRunningProcess(lastName);
}
Second, register the class in DI using the Proxy/Interception:
services.AddProxiedScoped<IPersonRepository, PersonRepository>();
Instead of the normal way without the Proxy/Interception:
services.AddScoped<IPersonRepository, PersonRepository>();
The base .NET Core container does not have any extra features like interceptors. The whole reason the DI container in .NET Core can be swapped out for something like Autofac is so you can move to a different container once you outgrow the default one.
I am creating a new project in Visual Studio using the SPA template. My goal is to have an Angular/SPA application that will "host"/contain some legacy applications that will eventually be modernized/migrated. So, I have an iframe on a page in my SPA app, and when a menu item is clicked, I want to load one of the legacy ASP.NET apps in that iframe (it has to be in an iframe, as the legacy site used them, and its architecture relies on them).
I am having trouble getting the routing right. The SPA template defines a DefaultRoute class like this (I changed RouteExistingFiles to true):
public class DefaultRoute : Route
{
public DefaultRoute()
: base("{*path}", new DefaultRouteHandler()) {
this.RouteExistingFiles = true;
}
}
and I have edited the RouteConfig.cs file to ignore "aspx" page requests, like this:
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes) {
routes.Ignore("*.aspx");
routes.Add("Default", new DefaultRoute());
}
}
The default route handler that is defined, looks like this:
public class DefaultRouteHandler : IRouteHandler
{
public IHttpHandler GetHttpHandler(RequestContext requestContext) {
// Use cases:
// ~/ -> ~/views/index.cshtml
// ~/about -> ~/views/about.cshtml or ~/views/about/index.cshtml
// ~/views/about -> ~/views/about.cshtml
// ~/xxx -> ~/views/404.cshtml
var filePath = requestContext.HttpContext.Request.AppRelativeCurrentExecutionFilePath;
if (filePath == "~/") {
filePath = "~/views/index.cshtml";
}
else {
if (!filePath.StartsWith("~/views/", StringComparison.OrdinalIgnoreCase)) {
filePath = filePath.Insert(2, "views/");
}
if (!filePath.EndsWith(".cshtml", StringComparison.OrdinalIgnoreCase)) {
filePath = filePath += ".cshtml";
}
}
var handler = WebPageHttpHandler.CreateFromVirtualPath(filePath); // returns NULL if .cshtml file wasn't found
if (handler == null) {
requestContext.RouteData.DataTokens.Add("templateUrl", "/views/404");
handler = WebPageHttpHandler.CreateFromVirtualPath("~/views/404.cshtml");
}
else {
requestContext.RouteData.DataTokens.Add("templateUrl", filePath.Substring(1, filePath.Length - 8));
}
return handler;
}
}
The directory structure is like this:
MySPA
SPA <- contains the SPA application (.net 4.5)
Legacy <- contains the legacy applications (.net 3.0)
In IIS, I have the legacy folder set as a virtual directory (subdirectory) within the SPA application.
How do I set up routing so that, when a menu item is clicked, and the request is sent (containing a url that has query string information) for an .aspx page, the request can be routed to the legacy application?
I have solved this issue. The issue was caused by several problems. But, the problem that most pertains to my information above was this...I needed to find the correct way to ignore the requests for the legacy aspx pages from within the routing code of the new site. So, in the RouteConfig.cs file, I placed this ignore statement as the first line of the RegisterRoute function:
routes.Ignore("{*allaspx}", new { allaspx = #".*\.aspx(/.*)?"});
That corrected the main issue, there were other minor issues that confused the situation but, I have identified what they are and I am working on those. So, ignoring the legacy urls in the routing functionality was the correct solution.
I want to write a unit test that verifies my route registration and ControllerFactory so that given a specific URL, a specific controller will be created. Something like this:
Assert.UrlMapsToController("~/Home/Index",typeof(HomeController));
I've modified code taken from the book "Pro ASP.NET MVC 3 Framework", and it seems it would be perfect except that the ControllerFactory.CreateController() call throws an InvalidOperationException and says This method cannot be called during the application's pre-start initialization stage.
So then I downloaded the MVC source code and debugged into it, looking for the source of the problem. It originates from the ControllerFactory looking for all referenced assemblies - so that it can locate potential controllers. Somewhere in the CreateController call-stack, the specific trouble-maker call is this:
internal sealed class BuildManagerWrapper : IBuildManager {
//...
ICollection IBuildManager.GetReferencedAssemblies() {
// This bails with InvalidOperationException with the message
// "This method cannot be called during the application's pre-start
// initialization stage."
return BuildManager.GetReferencedAssemblies();
}
//...
}
I found a SO commentary on this. I still wonder if there is something that can be manually initialized to make the above code happy. Anyone?
But in the absence of that...I can't help notice that the invocation comes from an implementation of IBuildManager. I explored the possibility of injecting my own IBuildManager, but I ran into the following problems:
IBuildManager is marked internal, so I need some other authorized derivation from it. It turns out that the assembly System.Web.Mvc.Test has a class called MockBuildManager, designed for test scenarios, which is perfect!!! This leads to the second problem.
The MVC distributable, near as I can tell, does not come with the System.Web.Mvc.Test assembly (DOH!).
Even if the MVC distributable did come with the System.Web.Mvc.Test assembly, having an instance of MockBuildManager is only half the solution. It is also necessary to feed that instance into the DefaultControllerFactory. Unfortunately the property setter to accomplish this is also marked internal (DOH!).
In short, unless I find another way to "initialize" the MVC framework, my options now are to either:
COMPLETELY duplicate the source code for DefaultControllerFactory and its dependencies, so that I can bypass the original GetReferencedAssemblies() issue. (ugh!)
COMPLETELY replace the MVC distributable with my own build of MVC, based on the MVC source code - with just a couple internal modifiers removed. (double ugh!)
Incidentally, I know that the MvcContrib "TestHelper" has the appearance of accomplishing my goal, but I think it is merely using reflection to find the controller - rather than using the actual IControllerFactory to retrieve a controller type / instance.
A big reason why I want this test capability is that I have made a custom controller factory, based on DefaultControllerFactory, whose behavior I want to verify.
I'm not quite sure what you're trying to accomplish here. If it's just testing your route setup; you're way better off just testing THAT instead of hacking your way into internals. 1st rule of TDD: only test the code you wrote (and in this case that's the routing setup, not the actual route resolving technique done by MVC).
There are tons of posts/blogs about testing a route setup (just google for 'mvc test route'). It all comes down to mocking a request in a httpcontext and calling GetRouteData.
If you really need some ninja skills to mock the buildmanager: there's a way around internal interfaces, which I use for (LinqPad) experimental tests. Most .net assemblies nowadays have the InternalsVisibleToAttribute set, most likely pointing to another signed test assembly. By scanning the target assembly for this attribute and creating an assembly on the fly that matches the name (and the public key token) you can easily access internals.
Mind you that I personally would not use this technique in production test code; but it's a nice way to isolate some complex ideas.
void Main()
{
var bm = BuildManagerMockBase.CreateMock<MyBuildManager>();
bm.FileExists("IsCool?").Dump();
}
public class MyBuildManager : BuildManagerMockBase
{
public override bool FileExists(string virtualPath) { return true; }
}
public abstract class BuildManagerMockBase
{
public static T CreateMock<T>()
where T : BuildManagerMockBase
{
// Locate the mvc assembly
Assembly mvcAssembly = Assembly.GetAssembly(typeof(Controller));
// Get the type of the buildmanager interface
var buildManagerInterface = mvcAssembly.GetType("System.Web.Mvc.IBuildManager",true);
// Locate the "internals visible to" attribute and create a public key token that matches the one specified.
var internalsVisisbleTo = mvcAssembly.GetCustomAttributes(typeof (InternalsVisibleToAttribute), true).FirstOrDefault() as InternalsVisibleToAttribute;
var publicKeyString = internalsVisisbleTo.AssemblyName.Split("=".ToCharArray())[1];
var publicKey = ToBytes(publicKeyString);
// Create a fake System.Web.Mvc.Test assembly with the public key token set
AssemblyName assemblyName = new AssemblyName();
assemblyName.Name = "System.Web.Mvc.Test";
assemblyName.SetPublicKey(publicKey);
// Get the domain of our current thread to host the new fake assembly
var domain = Thread.GetDomain();
var assemblyBuilder = domain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.RunAndSave);
moduleBuilder = assemblyBuilder.DefineDynamicModule("System.Web.Mvc.Test", "System.Web.Mvc.Test.dll");
AppDomain currentDom = domain;
currentDom.TypeResolve += ResolveEvent;
// Create a new type that inherits from the provided generic and implements the IBuildManager interface
var typeBuilder = moduleBuilder.DefineType("Cheat", TypeAttributes.NotPublic | TypeAttributes.Class, typeof(T), new Type[] { buildManagerInterface });
Type cheatType = typeBuilder.CreateType();
// Magic!
var ret = Activator.CreateInstance(cheatType) as T;
return ret;
}
private static byte[] ToBytes(string str)
{
List<Byte> bytes = new List<Byte>();
while(str.Length > 0)
{
var bstr = str.Substring(0, 2);
bytes.Add(Convert.ToByte(bstr, 16));
str = str.Substring(2);
}
return bytes.ToArray();
}
private static ModuleBuilder moduleBuilder;
private static Assembly ResolveEvent(Object sender, ResolveEventArgs args)
{
return moduleBuilder.Assembly;
}
public virtual bool FileExists(string virtualPath) { throw new NotImplementedException(); }
public virtual Type GetCompiledType(string virtualPath) { throw new NotImplementedException(); }
public virtual ICollection GetReferencedAssemblies() { throw new NotImplementedException(); }
public virtual Stream ReadCachedFile(string fileName) { throw new NotImplementedException(); }
public virtual Stream CreateCachedFile(string fileName) { throw new NotImplementedException(); }
}
I am writing a VirtualPathProvider to dynamically load my MVC views, which are located in a different directory. I successfully intercept the call before MVC (in FileExists), but in my VirtualPathProvider, I get the raw, pre-routed url like:
~/Apps/Administration/Account/LogOn
Personally, I know that MVC will look for
~/Apps/Administration/Views/Account/LogOn.aspx
and that I should be reading the file contents from
D:\SomeOtherNonWebRootDirectory\Apps\Administration\Views\Account\LogOn.aspx
but I'd rather not hard code the logic to "add the directory named Views and add aspx to the end".
Where is this logic stored and how can I get it into my virtual path provider?
Thanks. Sorry if I'm not being clear.
Edited
You need to make a class that inherits WebFormViewEngine and sets the ViewLocationFormats property (inherited from VirtualPathProviderViewEngine).
The default values can be found in the MVC source code:
public WebFormViewEngine() {
MasterLocationFormats = new[] {
"~/Views/{1}/{0}.master",
"~/Views/Shared/{0}.master"
};
AreaMasterLocationFormats = new[] {
"~/Areas/{2}/Views/{1}/{0}.master",
"~/Areas/{2}/Views/Shared/{0}.master",
};
ViewLocationFormats = new[] {
"~/Views/{1}/{0}.aspx",
"~/Views/{1}/{0}.ascx",
"~/Views/Shared/{0}.aspx",
"~/Views/Shared/{0}.ascx"
};
AreaViewLocationFormats = new[] {
"~/Areas/{2}/Views/{1}/{0}.aspx",
"~/Areas/{2}/Views/{1}/{0}.ascx",
"~/Areas/{2}/Views/Shared/{0}.aspx",
"~/Areas/{2}/Views/Shared/{0}.ascx",
};
PartialViewLocationFormats = ViewLocationFormats;
AreaPartialViewLocationFormats = AreaViewLocationFormats;
}
You should then clear the ViewEngines.Engines collection and add your ViewEngine instance to it.
As SLaks mentioned above, you need to create a Custom View Engine and add your view-finding logic in the FindView method.
public class CustomViewEngine : VirtualPathProviderViewEngine
{
public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
{
//Set view path
string viewPath = GetCurrentViewPath();
//Set master path (if need be)
string masterPath = GetCurrentMasterPath();
return base.FindView(controllerContext, viewPath, masterPath, useCache);
}
}
In the Application_Start, you can register your View Engine like this:
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new CustomViewEngine());
The answer was that MVC was not finding my controller properly. If MVC does in fact find your controller properly, there should be two requests processed by the VirtualPathProvider:
An initial request with the acutal url requested (ie. http://.../Account/LogOn).
A subsequent FileExists check for http://.../Views/Account/LogOn.aspx, after the request in 1. returns false calling FileExists. This actually retuns the aspx content.