I am working on a plugin system for Asp.net MVC 2. I have a dll containing controllers and views as embedded resources.
I scan the plugin dlls for controller using StructureMap and I then can pull them out and instantiate them when requested. This works fine. I then have a VirtualPathProvider which I adapted from this post
public class AssemblyResourceProvider : VirtualPathProvider
{
protected virtual string WidgetDirectory
{
get
{
return "~/bin";
}
}
private bool IsAppResourcePath(string virtualPath)
{
var checkPath = VirtualPathUtility.ToAppRelative(virtualPath);
return checkPath.StartsWith(WidgetDirectory, StringComparison.InvariantCultureIgnoreCase);
}
public override bool FileExists(string virtualPath)
{
return (IsAppResourcePath(virtualPath) || base.FileExists(virtualPath));
}
public override VirtualFile GetFile(string virtualPath)
{
return IsAppResourcePath(virtualPath) ? new AssemblyResourceVirtualFile(virtualPath) : base.GetFile(virtualPath);
}
public override CacheDependency GetCacheDependency(string virtualPath, IEnumerable virtualPathDependencies,
DateTime utcStart)
{
return IsAppResourcePath(virtualPath) ? null : base.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart);
}
}
internal class AssemblyResourceVirtualFile : VirtualFile
{
private readonly string path;
public AssemblyResourceVirtualFile(string virtualPath)
: base(virtualPath)
{
path = VirtualPathUtility.ToAppRelative(virtualPath);
}
public override Stream Open()
{
var parts = path.Split('/');
var resourceName = Path.GetFileName(path);
var apath = HttpContext.Current.Server.MapPath(Path.GetDirectoryName(path));
var assembly = Assembly.LoadFile(apath);
return assembly != null ? assembly.GetManifestResourceStream(assembly.GetManifestResourceNames().SingleOrDefault(s => string.Compare(s, resourceName, true) == 0)) : null;
}
}
The VPP seems to be working fine also. The view is found and is pulled out into a stream. I then receive a parse error Could not load type 'System.Web.Mvc.ViewUserControl<dynamic>'. which I can't find mentioned in any previous example of pluggable views. Why would my view not compile at this stage?
Thanks for any help,
Ian
EDIT:
Getting closer to an answer but not quite clear why things aren't compiling. Based on the comments I checked the versions and everything is in V2, I believe dynamic was brought in at V2 so this is fine. I don't even have V3 installed so it can't be that. I have however got the view to render, if I remove the <dynamic> altogether.
So a VPP works but only if the view is not strongly typed or dynamic
This makes sense for the strongly typed scenario as the type is in the dynamically loaded dll so the viewengine will not be aware of it, even though the dll is in the bin. Is there a way to load types at app start? Considering having a go with MEF instead of my bespoke Structuremap solution. What do you think?
The settings that allow parsing of strongly typed views are in ~/Views/Web.Config. When the view engine is using a virtual path provider, it is not in the views folder so doesn't load those settings.
If you copy everything in the pages node of Views/Web.Config to the root Web.Config, strongly typed views will be parsed correctly.
Try to add content of Views/Web.config directly to your main web.config under specific location, e.g. for handling virtual paths like ~/page/xxx. See more details here: http://blog.sergkazakov.com/2011/01/aspnet-strongly-typed-view-and-virtual.html
Is there a way to load types at app start?
Yes, you may use BuildManager.AddReferencedAssembly(assembly), where assembly is the one, containing the requested type. So, once you use MEF, the last should be easy, but beware, everything should be done before Application_Start, so you may wish to use PreApplicationStartMethodAttribute.
Related
I am currently trying to implement Razor Web Pages in older WebForms project, and also, make it possible to render partial views from string (taken from database elsewhere). I've implemented custom VirtualPathProvider with all overrides specified here: ASP.NET MVC and Virtual views and also overwritten these methods:
public override CacheDependency GetCacheDependency(string virtualPath, IEnumerable virtualPathDependencies, DateTime utcStart)
{
if (virtualPath.Contains("RazorMigration.cshtml") && HttpContext.Current.Items.Contains("RazorTestingPage"))
{
return null;
}
return Previous.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart);
}
public override String GetFileHash(String virtualPath, IEnumerable virtualPathDependencies)
{
if (virtualPath.Contains("RazorMigration.cshtml") && HttpContext.Current.Items.Contains("RazorTestingPage"))
{
return Guid.NewGuid().ToString();
}
return Previous.GetFileHash(virtualPath, virtualPathDependencies);
}
Then when I am trying to actually render page (completeTemplate already contains pure HTML with razor already parsed) like this:
var rt = new RouteData();
rt.Values.Add("controller", "WebFormShimController");
var httpCtx = new HttpContextWrapper(System.Web.HttpContext.Current);
var ctx = new ControllerContext(new RequestContext(httpCtx, rt), new WebFormShimController());
try
{
HttpContext.Current.Items.Add("RazorTestingPage", completeTemplate);
IView view = ViewEngines.Engines.FindPartialView(ctx, System.IO.Path.GetFileName("RazorMigration")).View;
ViewContext vctx = new ViewContext(ctx, view,
new ViewDataDictionary { Model = model },
new TempDataDictionary(), httpCtx.Response.Output);
view.Render(vctx, System.Web.HttpContext.Current.Response.Output);
}
I always catch Exception On view.Render line saying this:
c:\Windows\Microsoft.NET\Framework64\v4.0.30319\Temporary ASP.NET Files\root\0e7dfb6a\c63cc9d1\App_Web_razormigration.cshtml.afd51fd3.jujzzimy.0.cs(41): error CS1009: Unrecognized escape sequence
I am not really sure what is the issue here, or where and how this path is constructed. If someone could point me in the right direction I would be very happy as I am trying to get it working for almost a week, but still no success.
EDIT: I found the bug - It was in my encoding when writing to file (so when file was written \NUL characters were added, which confused IIS)
Now I am getting this error:
CS0115: c:\Windows\Microsoft.NET\Framework64\v4.0.30319\Temporary ASP.NET Files\root\0e7dfb6a\c63cc9d1\App_Web_4b7b71f4-87c2-4364-be0e-19ec2a81ceccR.0.cs.Execute()': no suitable method found to override
I am sure I don't have error in my view as it only contains this:
<h1>Hello</h1>
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 have an asp.net web site with our own DbResourceProvider. The plan was to localize pages with regular markup for resources: Text="<%$ Resources: Someword %>".
The provider returns the resourceKey if a resource is missing in the database so missing resources are ok at run-time. However at compile-time missing resources causes build-errors.
Does anyone have a workaround for this?
(I'll add more details about my attempts if needed.)
A collegue pointed out at a solution in the last comment in the blog-post linked to in the question. I don't have access to try it, but this class should solve the problem:
public class MyResources : System.Web.Compilation.IResourceProvider
{
private static string _entryMethod;
System.Resources.IResourceReader System.Web.Compilation.IResourceProvider.ResourceReader { get { throw new NotImplementedException(); } }
object System.Web.Compilation.IResourceProvider.GetObject(string resourceKey, System.Globalization.CultureInfo culture)
{
if (ASPNetCompilerExecutionContext)
return "ASPNetCompilerDesignTimeExecutionContext";
return "TODO: return actual resource";
}
static bool ASPNetCompilerExecutionContext
{
get
{
if (_entryMethod == null)
_entryMethod = (new StackTrace()).GetFrame((new StackTrace()).FrameCount - 1).GetMethod().Name;
if (_entryMethod == "PrecompileApp")
return true;
else
return false;
}
}
}
I guess an even simpler way would be to pretend all resources are present and return "missing" for missing resources.
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.
I have a fairly large ASP MVC application. Instead of having many controllers all in the controller directory I would rather create some hierarchy. So I might have something like
~\Controllers\Security\
~\Controllers\Maintenance\
~\Controllers\Reports\
I would also like to be able to do similar with Views
~\Views\Security\Users\
~\Views\Security\Roles\
~\Views\Maintenance\Customer\
~\Views\Maintenance\Product\
Is this easily done?
I think you're looking for something like what the "master" says in this post:
http://haacked.com/archive/0001/01/01/areas-in-aspnetmvc.aspx
Basically you have to create a ViewEngine to specify where to look for the views. It's a fairly simple code, just don't forget to register it in the global.asax! As for the controller part you'll have to register new routes also in the global.asax.
The concept you're searching for is called "areas", as outlined by Phil Haack here:
http://haacked.com/archive/2008/11/04/areas-in-aspnetmvc.aspx
I would think you'd need to write your own RouteHandler, which shouldn't be too tough.
A quick google search turned up: This blog post detailing it
Have you considered to switch to Features Folder. I've tried it (with little modifications) and it works pretty good.
Described in this post
http://timgthomas.com/2013/10/feature-folders-in-asp-net-mvc/
Code examples are in Jimmiy Bogard's repo
https://github.com/jbogard/presentations/tree/master/putyourcontrollersonadietv2
What you really want here is for your Views folder hierarchy to match the namespace hierarchy of your controllers. You can write a custom ViewEngine to do this fairly easily - see my ControllerPathViewEngine project on GitHub for details.
I've included a snippet of the ControllerPathRazorViewEngine class to outline how it works. By intercepting the FindView / FindPartialView methods and replacing the controller name with a folder path (based on controller namespace and name), we can get it to load views from nested folders within the main Views folder.
public class ControllerPathRazorViewEngine : RazorViewEngine
{
//... constructors etc.
public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
{
return FindUsingControllerPath(controllerContext, () => base.FindView(controllerContext, viewName, masterName, useCache));
}
public override ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache)
{
return FindUsingControllerPath(controllerContext, () => base.FindPartialView(controllerContext, partialViewName, useCache));
}
private ViewEngineResult FindUsingControllerPath(ControllerContext controllerContext, Func<ViewEngineResult> func)
{
string controllerName = controllerContext.RouteData.GetRequiredString("controller");
string controllerPath = controllerPathResolver.GetPath(controllerContext.Controller.GetType());
controllerContext.RouteData.Values["controller"] = controllerPath;
var result = func();
controllerContext.RouteData.Values["controller"] = controllerName;
return result;
}
}