Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why MVC 5 does not call GetFile Method of my VirtualPathProvider Class?

I am trying to load Razor View from database.

I follow ASP.NET MVC and virtual views and VirtualPathProvider in MVC 5 to do that.

my Code :

VirtualPathProvider :

 public class DbPathProvider : VirtualPathProvider
  {
    public override bool FileExists(string virtualPath)
    {
        var page = FindPage(virtualPath);
        if (page == null)
        {
            return base.FileExists(virtualPath);
        }
        else
        {
            return true;
        }
    }

    public override VirtualFile GetFile(string virtualPath)
    {
        var page = FindPage(virtualPath);
        if (page == null)
        {
            return base.GetFile(virtualPath);
        }
        else
        {
            return new DbVirtualFile(virtualPath, page.PageData.ToArray());
        }
    }

    private SiteModel FindPage(string virtualPath)
    {
        var db = new DatabaseContext();
        var page = db.SiteModels.FirstOrDefault(x => x.SiteName == virtualPath);
        return page;
    }
}

VirtualFile

public class DbVirtualFile : VirtualFile
{
    private byte[] data;

    public DbVirtualFile(string virtualPath, byte[] data)
        : base(virtualPath)
    {
        this.data = data;
    }

    public override System.IO.Stream Open()
    {
        return new MemoryStream(data);
    }
}

Global.asax :

protected void Application_Start()
    {
        HostingEnvironment.RegisterVirtualPathProvider(new DbPathProvider());
        AreaRegistration.RegisterAllAreas();
        FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
        RouteConfig.RegisterRoutes(RouteTable.Routes);
        BundleConfig.RegisterBundles(BundleTable.Bundles);

    }

Action :

public ActionResult Display(string id)
    {
        var db = new DatabaseContext();
        var site = db.SiteModels.FirstOrDefault(x => x.PageName == id);
        if (site == null)
        {
            return RedirectToAction("Index", "Home");
        }
        ViewBag.Body = site.PageContent;
        return View(System.IO.Path.GetFileNameWithoutExtension(site.SiteName));
    }

Data:

enter image description here

Case 1:

When virtualPath value is "/Views/Home/Contact.cshtml" then FileExists method return true and GetFile method is called.

Case 2:

When virtualPath value is "~/Home/Display/ce28bbb6-03cb-4bf4-8820-373890396a90" then FileExists method return true and GetFile method and Display Action is never called. and result is

HTTP Error 404.0 - Not Found The resource you are looking for has been removed, had its name changed, or is temporarily unavailable.

I have no idea about dynamic view. I just read that two article and try to implement it.

Please tell me where I am doing wrong.

I am using MVC 5 and .NET 4.5

like image 463
Hasanuzzaman Avatar asked Jul 19 '14 18:07

Hasanuzzaman


3 Answers

something I should be able to help you with. There is something you are not thinking about which is ok, because I had loads of trouble with this too.

I think the issue you have here is knowing in what order everything fires. This is what happens:

  1. First the VirtualPathProvider FileExists method is called and this is where you are doing your magic to find the page. My methods are slightly different to yours, here is an example:

    public IList<Page> Pages
    {
        get
        {
            using (var uow = new UnitOfWork<SkipstoneContext>())
            {
                var companyService = new CompanyService(uow);
                var company = companyService.GetTenant();
    
                if (company != null)
                {
                    var pageService = new PageService(uow, company.Id);
    
                    return pageService.GetPublished();
                }
            }
    
            return null;
        }
    }
    
    public override bool FileExists(string virtualPath)
    {
        if (IsVirtualPath(virtualPath))
        {
            if (FindPage(virtualPath) != null)
            {
                var file = (PageVirtualFile)GetFile(virtualPath);
                return file.Exists;
            }
        }
    
        return Previous.FileExists(virtualPath);
    }
    
    public override VirtualFile GetFile(string virtualPath)
    {
        if (IsVirtualPath(virtualPath))
        {
            var page = FindPage(virtualPath);
            if (page != null)
            {
                var decodedString = Uri.UnescapeDataString(page.ViewData);
                var bytes = Encoding.ASCII.GetBytes(decodedString);
    
                return new PageVirtualFile(virtualPath, bytes);
            }
        }
    
        return Previous.GetFile(virtualPath);
    }
    
    public override System.Web.Caching.CacheDependency GetCacheDependency(string virtualPath, System.Collections.IEnumerable virtualPathDependencies, DateTime utcStart)
    {
        if (IsVirtualPath(virtualPath))
            return null;
    
        return Previous.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart);
    }
    
    public override string GetFileHash(string virtualPath, System.Collections.IEnumerable virtualPathDependencies)
    {
        if (IsVirtualPath(virtualPath))
            return Guid.NewGuid().ToString();
    
        return Previous.GetFileHash(virtualPath, virtualPathDependencies);
    }
    
    private Page FindPage(string virtualPath)
    {
        var virtualName = VirtualPathUtility.GetFileName(virtualPath);
        var virtualExtension = VirtualPathUtility.GetExtension(virtualPath);
    
        try
        {
            if (Pages != null)
            {
                var id = Convert.ToInt32(virtualName.Replace(virtualExtension, ""));
                var page = Pages.Where(model => model.Id == id && model.Extension.Equals(virtualExtension, StringComparison.OrdinalIgnoreCase)).SingleOrDefault();
    
                return page;
            }
        }
        catch(Exception ex)
        {
            // Do nothing
        }
    
        return null;
    }
    
    private bool IsVirtualPath(string virtualPath)
    {
        var path = (VirtualPathUtility.GetDirectory(virtualPath) != "~/") ? VirtualPathUtility.RemoveTrailingSlash(VirtualPathUtility.GetDirectory(virtualPath)) : VirtualPathUtility.GetDirectory(virtualPath);
        if (path.Equals("~/Views/Routing", StringComparison.OrdinalIgnoreCase) || path.Equals("/Views/Routing", StringComparison.OrdinalIgnoreCase))
            return true;
        else
            return false;
    }
    
  2. Now, if this page is served from the database it is a virtual page, so the next thing that happens is that it calls your MvcHandler (in my handler I have these functions)

    protected override IAsyncResult BeginProcessRequest(HttpContextBase httpContext, AsyncCallback callback, object state)
    {
        var vpp = new SkipstoneVirtualPathProvider(); // Create an instance of our VirtualPathProvider class
        var requestContext = ((MvcHandler)httpContext.Handler).RequestContext; // Get our request context
        var path = requestContext.HttpContext.Request.Url.AbsolutePath; // Get our requested path
        var pages = vpp.Pages; // Get all the published pages for this company
    
        if (pages != null && !string.IsNullOrEmpty(path)) // If we have any pages and we have a path
        {
            var page = this.MatchPage(pages, path);
            if (page != null) // If we find the page
            {
                requestContext.RouteData.Values["controller"] = "Routing"; // Set the controller
                requestContext.RouteData.Values["action"] = "Index"; // And the action
            }
        }
    
        return base.BeginProcessRequest(httpContext, callback, state);
    }
    
    private Page MatchPage(IList<Page> pages, string path)
    {
        if (path == "/" || !path.EndsWith("/"))
        {
            var page = pages.Where(model => model.Path.Equals(path, StringComparison.OrdinalIgnoreCase)).SingleOrDefault(); // Try to match the path first
            if (page == null) // If we have no page, then get the directory and see if that matches any page
            {
                path = VirtualPathUtility.RemoveTrailingSlash(VirtualPathUtility.GetDirectory(path));
                page = pages.Where(model => model.Path.Equals(path, StringComparison.OrdinalIgnoreCase)).SingleOrDefault();
            }
    
            return page; // return our page or null if no page exists
        }
    
        return null; // return null if anything fails
    }
    
  3. Just in case, when you create your MvcHandler if you have not already done so, it must be registered in RouteConfig like this:

        // default MVC route
        routes.MapRoute(
            name: "Default",
            url: "{controller}/{action}/{id}",
            defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
        ).RouteHandler = new SkipstoneRouteHandler();
    
  4. In my MvcHandler I get my page and if it matches any virtual page I move to a RoutingController

    public ActionResult Index()
    {
        using (var uow = new UnitOfWork<SkipstoneContext>())
        {
            var userId = User.Identity.GetUserId();
            var service = new PageService(uow, this.CompanyId);
            var userService = new UserService(uow, this.CompanyId);
            var user = userService.Get(userId);
    
            var fileName = service.View(Request.Url, user);
    
            if (!fileName.StartsWith(@"/") && !fileName.StartsWith("~/"))
                return View(fileName); // Show the page
            else
                return Redirect(fileName); // Redirect to our page
        }
    }
    

just for your sake I will show the service.View method

    /// <summary>
    /// Views the CMS page
    /// </summary>
    /// <param name="uri">The current Request.Url</param>
    /// <param name="user">The current User</param>
    /// <returns>The filename of the requested page</returns>
    public string View(Uri uri, User user)
    {
        var path = uri.AbsolutePath; // Get our Requested Url
        var queryString = uri.Query;
        var pages = this.GetPublished();
        var page = pages.Where(model => model.Path.Equals(path, StringComparison.OrdinalIgnoreCase)).SingleOrDefault(); // Try to get the page

        if (page == null) // If our page is null
            page = pages.Where(model => model.Path.Equals(VirtualPathUtility.RemoveTrailingSlash(VirtualPathUtility.GetDirectory(path)))).SingleOrDefault(); // try to get the page based off the directory

        if (page == null) // If the page is still null, then it doesn't exist
            throw new HttpException(404, "This page has been deleted or removed."); // Throw the 404 error

        if (page.Restricted && user == null) // If our page is restricted and we are not logged in
            return "~/Account/LogOn?ReturnUrl=" + page.Path + queryString; // Redirect to the login page

        if (user != null && page.Restricted)
        {
            if (PageService.IsForbidden(page, user))
                throw new HttpException(401, "You do not have permission to view this page."); // Throw 401 error
        }

        return Path.GetFileNameWithoutExtension(page.Id.ToString());
    }
  1. Then we are taken back to our VirtualPathProvider and the process begins again, but this time the virtual path will look something like this: "~/Views/Routing/1331.aspx"
  2. If you notice, my FindPage method also looks for an extension match which I store in the database too: enter image description here
  3. Then GetFile will be called and as long as you have followed my path, you should return the VirtualFile

I really hope that helps :)

like image 116
r3plica Avatar answered Nov 09 '22 12:11

r3plica


I have same issue. Razor view engine not call GetFile if you virtualPath is not *.cshtml format. You need generate {Guid}.cshtml path format

like image 20
ZOXEXIVO Avatar answered Nov 09 '22 14:11

ZOXEXIVO


WARNING: ASP.NET 4 (MVC5) relative!

Despite the number of answers and a selected one, I will share my experience.

FileExists(string virtualPath)

gets called twice, once with a app relative path ("~/bla-bla") and once more with absolute one ("/bla-bla"). In my case the second one (the abs. path) fails. Seems like in ASP.NET 3 this was OK, however ASP.NET 4 (MVC5) checks it too. And requires it to return true. As a result

GetFile(string virtualPath) 

doesn't get called.

For those who need to implement such behaviour VirtualPathUtility offers ToAbsolute and ToAppRelative convenience methods.

HTH

like image 26
Alexander Christov Avatar answered Nov 09 '22 12:11

Alexander Christov