Adding Web Pages in ASP.NET Core
Introduction:
Last November, ASP.NET team showed us the future of ASP.NET Web Pages (if you haven't watched yet, you really should). Currently, Web Pages in ASP.NET Core is not supported but hopefully soon. I was trying to add and run a simple web page (cshtml page) in ASP.NET Core. I was able to run a web page using some very simple tricks. In this article, I will show you how I did this. But please note that, these are just my dirty tricks and by no mean a fully functional web pages support.
Description:
Note that I am using RC1 at the time of writing. Let's add web pages in ConfigureServices method,
public void ConfigureServices(IServiceCollection services) { services.AddWebPages();
AddWebPages is just an extension method and here is the implementation,
public static class WebPagesServiceCollectionExtensions { public static void AddWebPages(this IServiceCollection services) { if (services == null) { throw new ArgumentNullException(nameof(services)); } services.AddSingleton<RazorViewToStringRenderer>(); } }
We are registering RazorViewToStringRenderer class with the built-in IOC. This class is slightly modified version of a sample in ASP.NET Github's Entropy repo. Here is the full class,
public class RazorViewToStringRenderer { private IRazorViewEngine _viewEngine; private ITempDataProvider _tempDataProvider; private IServiceProvider _serviceProvider; private IHttpContextAccessor _contextAccessor; public RazorViewToStringRenderer( IRazorViewEngine viewEngine, ITempDataProvider tempDataProvider, IServiceProvider serviceProvider, IHttpContextAccessor contextAccessor) { _viewEngine = viewEngine; _tempDataProvider = tempDataProvider; _serviceProvider = serviceProvider; _contextAccessor = contextAccessor; } public string RenderViewToString(string path) { var actionContext = GetActionContext(); var viewEngineResult = _viewEngine.FindView(actionContext, path); if (!viewEngineResult.Success) { throw new InvalidOperationException(string.Format("Couldn't find view '{0}'", path)); } var view = viewEngineResult.View; using (var output = new StringWriter()) { var viewContext = new ViewContext( actionContext, view, new ViewDataDictionary( metadataProvider: new EmptyModelMetadataProvider(), modelState: new ModelStateDictionary()) { }, new TempDataDictionary( new HttpContextAccessor { HttpContext = actionContext.HttpContext}, _tempDataProvider), output, new HtmlHelperOptions()); view.RenderAsync(viewContext).GetAwaiter().GetResult(); return output.ToString(); } } private ActionContext GetActionContext() { _contextAccessor.HttpContext.RequestServices = _serviceProvider; return new ActionContext(_contextAccessor.HttpContext, new RouteData(), new ActionDescriptor()); } }
The RenderViewToString method take the path of web-page as parameter and then parse/execute the page and return the result in string. Next, we need to add a router to execute the web page only if the request url path ends with cshtml. Add these lines inside Startup.Configure method,
................................ app.UseWebPages(); app.UseMvc(routes => ................................ ................................
Note that we are adding app.UseWebPages before app.UseMvc because route order matters. Here is the implementation of UseWebPages extension method,
public static class WebPagesApplicationBuilderExtensions { public static IApplicationBuilder UseWebPages(this IApplicationBuilder app) { var renderer = app.ApplicationServices.GetRequiredService<RazorViewToStringRenderer>(); var applicationEnvironment = app.ApplicationServices.GetRequiredService<IApplicationEnvironment>(); return app.UseRouter(new WebPagesRouter(applicationEnvironment, renderer)); } }
In above class we are just getting instance of RazorViewToStringRenderer and IApplicationEnvironment from IOC, passing it to WebPagesRouter class constructor which is simply a custom router. This router will check whether the request url ends with cshtml or not. Here is the WebPagesRouter class,
public class WebPagesRouter : IRouter { private IApplicationEnvironment _applicationEnvironment; private RazorViewToStringRenderer _renderer; public WebPagesRouter(IApplicationEnvironment applicationEnvironment ,RazorViewToStringRenderer renderer) { _applicationEnvironment = applicationEnvironment; _renderer = renderer; } public VirtualPathData GetVirtualPath(VirtualPathContext context) { return null; } public async Task RouteAsync(RouteContext context) { var path = context.HttpContext.Request.Path.ToString().TrimStart('/'); if (Regex.IsMatch(path, "^([\\w]+/)*[\\w]+[.]cshtml$")) { var filePath = Path.Combine(_applicationEnvironment.ApplicationBasePath, path); context.IsHandled = true; if (!File.Exists(filePath)) { context.HttpContext.Response.StatusCode = 404; return; } var contents = _renderer.RenderViewToString("~/"+ path); await context.HttpContext.Response.WriteAsync(contents); } } }
This class is just a custom route. The route will check whether the request url path ends with .cshtml or not using regex (which only allow \w word in name and individual paths, change it if you wanna). If not matched then do nothing (means allow other router to participate). If matched then it will check whether file exist or not. If not exist then it will return 404 response otherwise it will call RazorViewToStringRenderer.RenderViewToString method to get the processed html and then write this result to response. Note that we used ~ above, so the file should exist at application root level (or inside a sub-folder of application root). Now let's add a WebPage.cshtml file at application root folder and add these lines there (taken from this link),
@inject Microsoft.AspNet.Hosting.IHostingEnvironment env @{ // Working with numbers var a = 4; var b = 5; var theSum = a + b; // Working with characters (strings) var technology = "ASP.NET"; var product = "Web Pages"; // Working with objects var rightNow = DateTime.Now; } <!DOCTYPE html> <html lang="en"> <head> <title>Testing Razor Syntax</title> <meta charset="utf-8" /> <style> body { font-family: Verdana; margin-left: 50px; margin-top: 50px; } div { border: 1px solid black; width: 50%; margin: 1.2em; padding: 1em; } span.bright { color: red; } </style> </head> <body> <h1>Testing Razor Syntax</h1> <form method="post"> <div> <p>The value of <em>a</em> is @a. The value of <em>b</em> is @b. <p>The sum of <em>a</em> and <em>b</em> is <strong>@theSum</strong>.</p> <p>The product of <em>a</em> and <em>b</em> is <strong>@(a * b)</strong>.</p> </div> <div> <p>The technology is @technology, and the product is @product.</p> <p>Together they are <span class="bright">@(technology + " " + product)</span></p> </div> <div> <p>The current date and time is: @rightNow</p> <p>The URL of the current page path is<br /><br /><code>@Context.Request.Path</code></p> <p>The app web root path is<br /><br /><code>@env.WebRootPath</code></p> </div> </form> </body> </html>
Note that I have also added @inject to show you that it will also work here. Now just run the app and navigate to WebPage.cshtml you will see the rendered html.
Summary:
In this article, I showed how to add web pages in ASP.NET Core. This was done in quick time without taking lot of consideration but you can use the above trick to make it more effective. BTW, the trick allow you to add web pages in your application quickly without adding any controller.