VirtualPathProvider in MVC 5 - asp.net-mvc

I can't seem to get a custom VirtualPathProvider working in asp.net MVC 5.
The FileExists method returns true but then the GetFile method isn't called.
I believe this is because IIS takes over the request and does not let .NET handle it.
I have tried setting RAMMFAR and creating a custom handler, as in this solution https://stackoverflow.com/a/12151501/801189 but still no luck. I get a error 404.
My Custom Provider:
public class DbPathProvider : VirtualPathProvider
{
public DbPathProvider() : base()
{
}
private static bool IsContentPath(string virtualPath)
{
var checkPath = VirtualPathUtility.ToAppRelative(virtualPath);
return checkPath.StartsWith("~/CMS/", StringComparison.InvariantCultureIgnoreCase);
}
public override bool FileExists(string virtualPath)
{
return IsContentPath(virtualPath) || base.FileExists(virtualPath);
}
public override VirtualFile GetFile(string virtualPath)
{
return IsContentPath(virtualPath) ? new DbVirtualFile(virtualPath) : base.GetFile(virtualPath);
}
public override CacheDependency GetCacheDependency(string virtualPath, IEnumerable virtualPathDependencies, DateTime utcStart)
{
return null;
}
public override String GetFileHash(String virtualPath, IEnumerable virtualPathDependencies)
{
return Guid.NewGuid().ToString();
}
}
My Custom Virtual File:
public class DbVirtualFile : VirtualFile
{
public DbVirtualFile(string path): base(path)
{
}
public override System.IO.Stream Open()
{
string testPage = "This is a test!";
return new System.IO.MemoryStream(System.Text.ASCIIEncoding.ASCII.GetBytes(testPage));
}
}
web.config handler I have tried to use, without success. It currently gives error 500 :
<system.webServer>
<modules runAllManagedModulesForAllRequests="true">
<remove name="FormsAuthenticationModule" />
</modules>
<handlers>
<add name="ApiURIs-ISAPI-Integrated-4.0"
path="/CMS/*"
verb="GET,HEAD,POST,DEBUG,PUT,DELETE,PATCH,OPTIONS"
type="System.Web.Handlers.TransferRequestHandler"
preCondition="runtimeVersionv4.0" />
</handlers>
If I try to navigate to site.com/CMS/Home/Index, the FileExists method is called but strangely, the virtualPath parameter recieves only ~/CMS/Home.
Adding breakpoints, it seems that for the url site.com/CMS/Home/Index, the FileExists method keeps getting repeatedly called. This may be causing an infinite recursion, giving the internal server error.

It was actually nothing to do with IIS, and in fact confusion on the order of events. It seems I didn't understand that a routed action method must return a view, that the VirtualPathProvider will try to resolve, rather than going to the VirtualPathProvider directly.
I create a simple controller called ContentPagesController with a single GetPage action:
public class ContentPagesController : Controller
{
[HttpGet]
public ActionResult GetPage(string pageName)
{
return View(pageName);
}
}
I then set up my route to serve virtual pages:
routes.MapRoute(
name: "ContentPageRoute",
url: "CMS/{*pageName}",
defaults: new { controller = "ContentPages", action = "GetPage" },
constraints: new { controller = "ContentPages", action = "GetPage" }
);
I register my custom VirtualPathProvider before I register my routes, in globals.asax.cs.
Now suppose I have a page in my database with the relative url /CMS/Home/AboutUs. The pageName parameter will have value Home/AboutUs and the return View() call will instruct the VirtualPathProvider to look for variations of the file ~/Views/ContentPages/Home/AboutUs.cshtml.
A few of the variations it will be look for include:
~/Views/ContentPages/Home/AboutUs.aspx
~/Views/ContentPages/Home/AboutUs.ascx
~/Views/ContentPages/Home/AboutUs.vbhtml
All you now need to do is check the virtualPath that is passed to the GetFiles method, using a database lookup or similar. Here is a simple way:
private bool IsCMSPath(string virtualPath)
{
return virtualPath == "/Views/ContentPages/Home/AboutUs.cshtml" ||
virtualPath == "~/Views/ContentPages/Home/AboutUs.cshtml";
}
public override bool FileExists(string virtualPath)
{
return IsCMSPath(virtualPath) || base.FileExists(virtualPath);
}
public override VirtualFile GetFile(string virtualPath)
{
if (IsCMSPath(virtualPath))
{
return new DbVirtualFile(virtualPath);
}
return base.GetFile(virtualPath);
}
The custom virtual file will be made and returned to the browser in the GetFile method.
Finally, a custom view engine can be created to give different virtual view paths that are sent to VirtualPathProvider.
Hope this helps.

Related

Load ASP.NET MVC views and controllers code from database

I have a system in which the end-user is a developer who can create ASP.NET MVC views/controllers and run them on the fly.
Currently, I have two database tables, one to store the view name and code and other to store controller code in C#. I can compile the build an assembly and save a dll file on the server folder.
Step 1: I added a custom controller factory to load my controller from the database, having an area in the project named (QZone).
public class QS_DynamicControllerFactory : DefaultControllerFactory//, IController
{
QS_DBConnection _db = new QS_DBConnection();
public QS_DynamicControllerFactory() { }
public override IController CreateController(RequestContext requestContext, string controllerName)
{
return (requestContext.RouteData.DataTokens["area"] != null &&
requestContext.RouteData.DataTokens["area"].ToString().ToLower() == "qzone") ?
QGetControllerInstance(controllerName) : base.CreateController(requestContext, controllerName);
}
internal IController QGetControllerInstance(string controllerName)
{
//load controller from the database and compile it then return an instance
}
public override void ReleaseController(IController controller)
{
base.ReleaseController(controller);
}
}
Step 2: I created a VirtualPathProvider, VirtualFile
QS_VirtualPathProvider class:
public class QS_VirtualPathProvider : VirtualPathProvider
{
public QDynamicView GetVirtualData(string viewPath)
{
QS_DBConnection _db = new QS_DBConnection();
QDynamicView view = (from v in _db.QDynamicViews
where v.Name.ToLower() == "TestView.cshtml".ToLower()//viewPath.ToLower()
select v).SingleOrDefault();
return view;
}
private bool IsPathVirtual(string virtualPath)
{
var path = (VirtualPathUtility.GetDirectory(virtualPath) != "~/") ? VirtualPathUtility.RemoveTrailingSlash(VirtualPathUtility.GetDirectory(virtualPath)) : VirtualPathUtility.GetDirectory(virtualPath);
if (path.ToLower().Contains("/qzone/"))
return true;
else
return false;
}
public override bool FileExists(string virtualPath)
{
if (IsPathVirtual(virtualPath))
{
QS_VirtualFile file = (QS_VirtualFile)GetFile(virtualPath);
bool isExists = file.Exists;
return isExists;
}
else
return Previous.FileExists(virtualPath);
}
public override VirtualFile GetFile(string virtualPath)
{
if (IsPathVirtual(virtualPath))
{
QDynamicView vw = GetVirtualData(virtualPath);
var bytes = Encoding.ASCII.GetBytes(vw.ViewCode);
return new QS_VirtualFile(virtualPath, bytes);
}
else
return Previous.GetFile(virtualPath);
}
public override CacheDependency GetCacheDependency(string virtualPath, System.Collections.IEnumerable virtualPathDependencies, DateTime utcStart)
{
if (IsPathVirtual(virtualPath))
{
return null;
}
else
return Previous.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart);
}
public override string GetFileHash(string virtualPath, IEnumerable virtualPathDependencies)
{
if (IsPathVirtual(virtualPath))
return Guid.NewGuid().ToString();
return base.GetFileHash(virtualPath, virtualPathDependencies);
}
}
QS_VirtualFile class:
public class QS_VirtualFile : VirtualFile
{
private string content;
private QS_VirtualPathProvider spp;
public bool Exists
{
get { return (content != null); }
}
public QS_VirtualFile(string virtualPath, QS_VirtualPathProvider provider) : base(virtualPath)
{
this.spp = provider;
GetData(virtualPath);
}
public QS_VirtualFile(QDynamicView vw, string virtualPath) : base(virtualPath)
{
content = vw.ViewCode;
}
private byte[] _BinaryContent;
public QS_VirtualFile(string virtualPath, byte[] contents) : base(virtualPath)
{
this._BinaryContent = contents;
}
protected void GetData(string virtualPath)
{
QDynamicView QSView = spp.GetVirtualData(virtualPath);
if (QSView != null)
{
content = QSView.ViewCode;
}
}
public override Stream Open()
{
return new MemoryStream(_BinaryContent);
}
}
Step 3: register the controller factory and the virtual path provider in the in Global.asax** file:
HostingEnvironment.RegisterVirtualPathProvider(new QS_VirtualPathProvider());
ControllerBuilder.Current.SetControllerFactory(new QS_DynamicControllerFactory());
testing the code
in order to test the code above i added a controller named (test) and a view named (testView.cshtml) in the database and requested the url below:
http://localhost:1001/qzone/test/TestView
and I got this error
I guess this mean that the controller factory worked fine but the view was not loaded
Any ideas?
That's because it's looking for your view on the hard drive. The View Engine uses VirtualPathProvidersto resolve your views, so you need to write your own VirtualPathProvider and register it.
You can find the documentation here:
https://learn.microsoft.com/en-us/dotnet/api/system.web.hosting.virtualpathprovider?view=netframework-4.8
Unfortunately, it is way too much code for me to copy here, but you can find a full example there.
Mind you, the example is for .NET 4.8, so if you're using Core, this may not be applicable.

MVC 5 VirtualPathProvider not working as expected

I've created the following virtual path provider to load views from a DB and when the view doesn't exist on disk, I am seeing my DB method in be called and return true for the FileExists method. After that, no other methods are called and the page returns as a 404. Views that are on disk are still rendering fine. The DB call GetByVirtualPath just returns a views content. I have validated that this object is hydrated with data.
VirtualPathProvider
public class CMSVirtualPathProvider : VirtualPathProvider
{
public override bool FileExists(string virtualPath)
{
return base.FileExists(virtualPath) || MVCViewVersion.GetByVirtualPath(virtualPath) != null;
}
public override VirtualFile GetFile(string virtualPath)
{
if (base.FileExists(virtualPath))
{
return base.GetFile(virtualPath);
}
else
{
return new CMSVirtualFile(virtualPath, this);
}
}
public override string GetFileHash(string virtualPath, IEnumerable virtualPathDependencies)
{
if (base.FileExists(virtualPath))
{
return base.GetFileHash(virtualPath, virtualPathDependencies);
}
else
{
#if DEBUG
return null;
#else
return string.Format("{0}{1}", virtualPath, DateTime.UtcNow.ToString("dd HH"));
#endif
}
}
public override CacheDependency GetCacheDependency(string virtualPath, IEnumerable virtualPathDependencies, DateTime utcStart)
{
if (!base.FileExists(virtualPath))
{
return null;
}
return Previous.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart);
}
}
VirtualFile
This class is never hit.
public class CMSVirtualFile : VirtualFile
{
private CMSVirtualPathProvider _ParentProvider;
public CMSVirtualFile(string virtualPath, CMSVirtualPathProvider parentProvider)
: base(virtualPath)
{
_ParentProvider = parentProvider;
}
public override System.IO.Stream Open()
{
string Content = string.Empty;
MVCViewVersion Version = MVCViewVersion.GetByVirtualPath(this.VirtualPath);
if (Version != null)
{
Content = Version.Content;
}
return new MemoryStream(ASCIIEncoding.Default.GetBytes(Content));
}
}
In the global.asax, I added the following link in the Application_Start method.
HostingEnvironment.RegisterVirtualPathProvider(new CMSVirtualPathProvider());
I assume something has changed as this code works on my previous implementation for MVC4. I can't put my finger on what I am doing incorrectly.
Found the issue. This was interesting. When the FileExists check occurs the virtualPath is passed like this:
~/Views/Home/Index.cshml
When the GetFile is called the virtualPath is:
/Views/Home/Index.cshtml
This causes the DB query to try and pull using the wrong virtual path from the DB which returns a null value. This then throws a 404. What a simple fix for a nightmare to find problem.

How to add bundles with dynamic content to asp.net web optimization

I'm using SignalR, which maps to asp.net application on virtual path "~/signalr".
SignalR dynamically creates javascript proxy hubs on app start with virtual path "~/signalr/hubs".
So the url "[http://myapp]/signalr/hubs" is where dynamic javascript content is.
How can I add this virtual path to Asp.Net Web Optimization Bundling?
Asp.Net Web Optimization Framework starting from 1.1 supports VirtuPathProvider's:
ASP.NET bundling/minification: including dynamically generated Javascript
Actually I don't understand how to use these VPP's. Could somebody explain in details or better give an example?
Integration of dynamic content into the bundling process requires the following steps:
Writing the logic that requests / builds the required content. For SignalR you could use this code snippet:
public static string GetSignalRContent()
{
var resolver = new DefaultHubManager(new DefaultDependencyResolver());
var proxy = new DefaultJavaScriptProxyGenerator(resolver, new NullJavaScriptMinifier());
return proxy.GenerateProxy("/signalr");
}
Implement a virtual path provider that wraps the existing one and intercept all virtual paths that should deliver the dynamic content (just "~/signalr/hubs" in your case).
public class CustomVirtualPathProvider : VirtualPathProvider
{
public CustomActionVirtualPathProvider(VirtualPathProvider virtualPathProvider)
{
// Wrap an existing virtual path provider
VirtualPathProvider = virtualPathProvider;
}
protected VirtualPathProvider VirtualPathProvider { get; set; }
public override string CombineVirtualPaths(string basePath, string relativePath)
{
return VirtualPathProvider.CombineVirtualPaths(basePath, relativePath);
}
public override bool DirectoryExists(string virtualDir)
{
return VirtualPathProvider.DirectoryExists(virtualDir);
}
public override bool FileExists(string virtualPath)
{
if (virtualPath == "~/signalr/hubs")
{
return true;
}
return VirtualPathProvider.FileExists(virtualPath);
}
public override CacheDependency GetCacheDependency(string virtualPath, IEnumerable virtualPathDependencies, DateTime utcStart)
{
// BaseClass can't create a CacheDependency for your content, remove it
// You could also add your own CacheDependency and aggregate it with the base dependency
List<string> virtualPathDependenciesCopy = virtualPathDependencies.Cast<string>().ToList();
virtualPathDependenciesCopy.Remove("~/signalr/hubs");
return VirtualPathProvider.GetCacheDependency(virtualPath, virtualPathDependenciesCopy, utcStart);
}
public override string GetCacheKey(string virtualPath)
{
return VirtualPathProvider.GetCacheKey(virtualPath);
}
public override VirtualDirectory GetDirectory(string virtualDir)
{
return VirtualPathProvider.GetDirectory(virtualDir);
}
public override VirtualFile GetFile(string virtualPath)
{
if (virtualPath == "~/signalr/hubs")
{
return new CustomVirtualFile(virtualPath,
new MemoryStream(Encoding.Default.GetBytes(GetSignalRContent())));
}
return VirtualPathProvider.GetFile(virtualPath);
}
public override string GetFileHash(string virtualPath, IEnumerable virtualPathDependencies)
{
return VirtualPathProvider.GetFileHash(virtualPath, virtualPathDependencies);
}
public override object InitializeLifetimeService()
{
return VirtualPathProvider.InitializeLifetimeService();
}
}
public class CustomVirtualFile : VirtualFile
{
public CustomVirtualFile (string virtualPath, Stream stream)
: base(virtualPath)
{
Stream = stream;
}
public Stream Stream { get; private set; }
public override Stream Open()
{
return Stream;
}
}
Register your virtual path provider:
public static void RegisterBundles(BundleCollection bundles)
{
// Set the virtual path provider
BundleTable.VirtualPathProvider =
new CustomVirtualPathProvider(BundleTable.VirtualPathProvider);
Bundle include = new Bundle("~/bundle")
.Include("~/Content/static.js")
.Include("~/signalr/hubs");
bundles.Add(include);
}
For some samples of virtual path providers + bundling, see Bundling and Minification and Embedded Resources or Bundling Dynamic Generated Controller / Action Content for example.
I'm not sure whether there is a way to do that, but another alternative is to generate the /signalr/hubs javascript at build time. That way you can just bundle the generated js file.
See the "How to create a physical file for the SignalR generated proxy" section in http://www.asp.net/signalr/overview/signalr-20/hubs-api/hubs-api-guide-javascript-client.

Embedded razor views

Recently, I read a post where the author describes how we can compile razor views into separate libraries. I would like to ask, is it possible to embed views in libraries without compiling? And then, add custom VirtualPathProvider to read the views.
You can use my EmbeddedResourceVirtualPathProvider which can be installed via Nuget. It loads resources from referenced assemblies, and also can be set to take dependencies on the source files during development so you can update views without needing a recompile.
In your "shell" MVC project's Global.asax Application_Start register your custom VirtualPathProvider:
HostingEnvironment.RegisterVirtualPathProvider(new CustomVirtualPathProvider());
The actual implementation would be more complex than this because you would likely do some interface-based, reflection, database lookup, etc as a means of pulling metadata, but this would be the general idea (assume you have another MVC project named "AnotherMvcAssembly" with a Foo controller and the Index.cshtml View is marked as an embedded resource:
public class CustomVirtualPathProvider : VirtualPathProvider {
public override bool DirectoryExists(string virtualDir) {
return base.DirectoryExists(virtualDir);
}
public override bool FileExists(string virtualPath) {
if (virtualPath == "/Views/Foo/Index.cshtml") {
return true;
}
else {
return base.FileExists(virtualPath);
}
}
public override System.Web.Caching.CacheDependency GetCacheDependency(string virtualPath, System.Collections.IEnumerable virtualPathDependencies, DateTime utcStart) {
if (virtualPath == "/Views/Foo/Index.cshtml") {
Assembly asm = Assembly.Load("AnotherMvcAssembly");
return new CacheDependency(asm.Location);
}
else {
return base.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart);
}
}
public override string GetCacheKey(string virtualPath) {
return base.GetCacheKey(virtualPath);
}
public override VirtualDirectory GetDirectory(string virtualDir) {
return base.GetDirectory(virtualDir);
}
public override VirtualFile GetFile(string virtualPath) {
if (virtualPath == "/Views/Foo/Index.cshtml") {
return new CustomVirtualFile(virtualPath);
}
else {
return base.GetFile(virtualPath);
}
}
}
public class CustomVirtualFile : VirtualFile {
public CustomVirtualFile(string virtualPath) : base(virtualPath) { }
public override System.IO.Stream Open() {
Assembly asm = Assembly.Load("AnotherMvcAssembly");
return asm.GetManifestResourceStream("AnotherMvcAssembly.Views.Foo.Index.cshtml");
}
}

Limiting HTTP verbs on every action

Is it a good practice to limit the available HTTP verbs for every action? My code is cleaner without [HttpGet], [HttpPost], [HttpPut], or [HttpDelete] decorating every action, but it might also be less robust or secure. I don't see this done in many tutorials or example code, unless the verb is explicitly required, like having two "Create" actions where the GET version returns a new form and the POST version inserts a new record.
Personally I try to respect RESTful conventions and specify the HTTP verb except for the GET actions which don't modify any state on the server thus allowing them to be invoked with any HTTP verb.
Yes, I believe it's a good practice to limit your actions only to the appropriate HTTP method it's supposed to handle, this will keep bad requests out of your system, reduce the effectiveness of possible attacks, improve the documentation of your code, enforce a RESTful design, etc.
Yes, using the [HttpGet], [HttpPost] .. attributes can make your code harder to read, specially if you also use other attributes like [OutputCache], [Authorize], etc.
I use a little trick with a custom IActionInvoker, instead of using attributes I prepend the HTTP method to the action method name, e.g.:
public class AccountController : Controller {
protected override IActionInvoker CreateActionInvoker() {
return new HttpMethodPrefixedActionInvoker();
}
public ActionResult GetLogOn() {
...
}
public ActionResult PostLogOn(LogOnModel model, string returnUrl) {
...
}
public ActionResult GetLogOff() {
...
}
public ActionResult GetRegister() {
...
}
public ActionResult PostRegister(RegisterModel model) {
...
}
[Authorize]
public ActionResult GetChangePassword() {
...
}
[Authorize]
public ActionResult PostChangePassword(ChangePasswordModel model) {
...
}
public ActionResult GetChangePasswordSuccess() {
...
}
}
Note that this doesn't change the action names, which are still LogOn, LogOff, Register, etc.
Here's the code:
using System;
using System.Collections.Generic;
using System.Web.Mvc;
public class HttpMethodPrefixedActionInvoker : ControllerActionInvoker {
protected override ActionDescriptor FindAction(ControllerContext controllerContext, ControllerDescriptor controllerDescriptor, string actionName) {
var request = controllerContext.HttpContext.Request;
string httpMethod = request.GetHttpMethodOverride()
?? request.HttpMethod;
// Implicit support for HEAD method.
// Decorate action with [HttpGet] if HEAD support is not wanted (e.g. action has side effects)
if (String.Equals(httpMethod, "HEAD", StringComparison.OrdinalIgnoreCase))
httpMethod = "GET";
string httpMethodAndActionName = httpMethod + actionName;
ActionDescriptor adescr = base.FindAction(controllerContext, controllerDescriptor, httpMethodAndActionName);
if (adescr != null)
adescr = new ActionDescriptorWrapper(adescr, actionName);
return adescr;
}
class ActionDescriptorWrapper : ActionDescriptor {
readonly ActionDescriptor wrapped;
readonly string realActionName;
public override string ActionName {
get { return realActionName; }
}
public override ControllerDescriptor ControllerDescriptor {
get { return wrapped.ControllerDescriptor; }
}
public override string UniqueId {
get { return wrapped.UniqueId; }
}
public ActionDescriptorWrapper(ActionDescriptor wrapped, string realActionName) {
this.wrapped = wrapped;
this.realActionName = realActionName;
}
public override object Execute(ControllerContext controllerContext, IDictionary<string, object> parameters) {
return wrapped.Execute(controllerContext, parameters);
}
public override ParameterDescriptor[] GetParameters() {
return wrapped.GetParameters();
}
public override object[] GetCustomAttributes(bool inherit) {
return wrapped.GetCustomAttributes(inherit);
}
public override object[] GetCustomAttributes(Type attributeType, bool inherit) {
return wrapped.GetCustomAttributes(attributeType, inherit);
}
public override bool Equals(object obj) {
return wrapped.Equals(obj);
}
public override int GetHashCode() {
return wrapped.GetHashCode();
}
public override ICollection<ActionSelector> GetSelectors() {
return wrapped.GetSelectors();
}
public override bool IsDefined(Type attributeType, bool inherit) {
return wrapped.IsDefined(attributeType, inherit);
}
public override string ToString() {
return wrapped.ToString();
}
}
}
You don't need to specify the HttpGet, all others you do need

Resources