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,
1 |
public
void
ConfigureServices(IServiceCollection
services)
|
2 |
{
|
3 |
services.AddWebPages();
|
AddWebPages is just an extension method and here is the implementation,
01 |
public
static
class
WebPagesServiceCollectionExtensions
|
02 |
{
|
03 |
public
static
void
AddWebPages(this
IServiceCollection services)
|
04 |
{
|
05 |
if
(services == null)
|
06 |
{
|
07 |
throw
new
ArgumentNullException(nameof(services));
|
08 |
}
|
09 |
services.AddSingleton<RazorViewToStringRenderer>();
|
10 |
}
|
11 |
}
|
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,
01 |
public
class
RazorViewToStringRenderer
|
02 |
{
|
03 |
private
IRazorViewEngine _viewEngine;
|
04 |
private
ITempDataProvider _tempDataProvider;
|
05 |
private
IServiceProvider _serviceProvider;
|
06 |
private
IHttpContextAccessor _contextAccessor;
|
07 |
08 |
public
RazorViewToStringRenderer(
|
09 |
IRazorViewEngine viewEngine,
|
10 |
ITempDataProvider tempDataProvider,
|
11 |
IServiceProvider serviceProvider,
|
12 |
IHttpContextAccessor contextAccessor)
|
13 |
{
|
14 |
_viewEngine = viewEngine;
|
15 |
_tempDataProvider = tempDataProvider;
|
16 |
_serviceProvider = serviceProvider;
|
17 |
_contextAccessor = contextAccessor;
|
18 |
}
|
19 |
20 |
public
string
RenderViewToString(string
path)
|
21 |
{
|
22 |
var actionContext = GetActionContext();
|
23 |
24 |
var viewEngineResult =
_viewEngine.FindView(actionContext,
path);
|
25 |
26 |
if
(!viewEngineResult.Success)
|
27 |
{
|
28 |
throw
new
InvalidOperationException(string.Format("Couldn't find view '{0}'", path));
|
29 |
}
|
30 |
31 |
var view = viewEngineResult.View;
|
32 |
33 |
using
(var output = new
StringWriter())
|
34 |
{
|
35 |
var viewContext = new
ViewContext(
|
36 |
actionContext,
|
37 |
view,
|
38 |
new
ViewDataDictionary(
|
39 |
metadataProvider: new
EmptyModelMetadataProvider(),
|
40 |
modelState: new
ModelStateDictionary())
|
41 |
{
|
42 |
},
|
43 |
new
TempDataDictionary(
|
44 |
new
HttpContextAccessor { HttpContext =
actionContext.HttpContext},
|
45 |
_tempDataProvider),
|
46 |
output,
|
47 |
new
HtmlHelperOptions());
|
48 |
49 |
view.RenderAsync(viewContext).GetAwaiter().GetResult();
|
50 |
51 |
return
output.ToString();
|
52 |
}
|
53 |
}
|
54 |
55 |
private
ActionContext GetActionContext()
|
56 |
{
|
57 |
_contextAccessor.HttpContext.RequestServices
= _serviceProvider;
|
58 |
return
new
ActionContext(_contextAccessor.HttpContext, new
RouteData(), new
ActionDescriptor());
|
59 |
}
|
60 |
}
|
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,
1 |
................................
|
2 |
app.UseWebPages();
|
3 |
app.UseMvc(routes =>
|
4 |
................................
|
5 |
................................
|
Note that we are adding app.UseWebPages before app.UseMvc because route order matters. Here is the implementation of UseWebPages extension method,
1 |
public
static
class
WebPagesApplicationBuilderExtensions
|
2 |
{
|
3 |
public
static
IApplicationBuilder UseWebPages(this
IApplicationBuilder app)
|
4 |
{
|
5 |
var renderer =
app.ApplicationServices.GetRequiredService<RazorViewToStringRenderer>();
|
6 |
var applicationEnvironment =
app.ApplicationServices.GetRequiredService<IApplicationEnvironment>();
|
7 |
return
app.UseRouter(new
WebPagesRouter(applicationEnvironment,
renderer));
|
8 |
}
|
9 |
}
|
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,
01 |
public
class
WebPagesRouter : IRouter
|
02 |
{
|
03 |
private
IApplicationEnvironment
_applicationEnvironment;
|
04 |
private
RazorViewToStringRenderer _renderer;
|
05 |
06 |
public
WebPagesRouter(IApplicationEnvironment
applicationEnvironment
|
07 |
,RazorViewToStringRenderer renderer)
|
08 |
{
|
09 |
_applicationEnvironment =
applicationEnvironment;
|
10 |
_renderer = renderer;
|
11 |
}
|
12 |
13 |
public
VirtualPathData
GetVirtualPath(VirtualPathContext
context)
|
14 |
{
|
15 |
return
null;
|
16 |
}
|
17 |
18 |
public
async Task RouteAsync(RouteContext
context)
|
19 |
{
|
20 |
var path =
context.HttpContext.Request.Path.ToString().TrimStart('/');
|
21 |
if
(Regex.IsMatch(path, "^([\\w]+/)*[\\w]+[.]cshtml$"))
|
22 |
{
|
23 |
var filePath =
Path.Combine(_applicationEnvironment.ApplicationBasePath,
path);
|
24 |
context.IsHandled = true;
|
25 |
if
(!File.Exists(filePath))
|
26 |
{
|
27 |
context.HttpContext.Response.StatusCode =
404;
|
28 |
return;
|
29 |
}
|
30 |
var contents =
_renderer.RenderViewToString("~/"+ path);
|
31 |
await
context.HttpContext.Response.WriteAsync(contents);
|
32 |
}
|
33 |
|
34 |
}
|
35 |
}
|
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),
01 |
@inject
Microsoft.AspNet.Hosting.IHostingEnvironment
env
|
02 |
@{
|
03 |
// Working with numbers
|
04 |
var a = 4;
|
05 |
var b = 5;
|
06 |
var theSum = a + b;
|
07 |
08 |
// Working with characters (strings)
|
09 |
var technology = "ASP.NET";
|
10 |
var product = "Web Pages";
|
11 |
12 |
// Working with objects
|
13 |
var rightNow = DateTime.Now;
|
14 |
}
|
15 |
<!DOCTYPE html>
|
16 |
<html
lang="en">
|
17 |
<head>
|
18 |
<title>Testing Razor Syntax</title>
|
19 |
<meta
charset="utf-8"
/>
|
20 |
<style>
|
21 |
body {
|
22 |
font-family: Verdana;
|
23 |
margin-left: 50px;
|
24 |
margin-top: 50px;
|
25 |
}
|
26 |
27 |
div {
|
28 |
border: 1px solid black;
|
29 |
width: 50%;
|
30 |
margin: 1.2em;
|
31 |
padding: 1em;
|
32 |
}
|
33 |
34 |
span.bright {
|
35 |
color: red;
|
36 |
}
|
37 |
</style>
|
38 |
</head>
|
39 |
<body>
|
40 |
<h1>Testing Razor Syntax</h1>
|
41 |
<form
method="post">
|
42 |
43 |
<div>
|
44 |
<p>The value of <em>a</em> is @a. The value of <em>b</em> is @b.
|
45 |
<p>The sum of <em>a</em> and <em>b</em> is <strong>@theSum</strong>.</p>
|
46 |
<p>The product of <em>a</em> and <em>b</em> is <strong>@(a * b)</strong>.</p>
|
47 |
</div>
|
48 |
49 |
<div>
|
50 |
<p>The technology is @technology, and the
product is @product.</p>
|
51 |
<p>Together they are <span
class="bright">@(technology + " " + product)</span></p>
|
52 |
</div>
|
53 |
54 |
<div>
|
55 |
<p>The current date and time is:
@rightNow</p>
|
56 |
<p>The URL of the current page path
is<br
/><br
/><code>@Context.Request.Path</code></p>
|
57 |
<p>The app web root path is<br
/><br
/><code>@env.WebRootPath</code></p>
|
58 |
</div>
|
59 |
60 |
</form>
|
61 |
</body>
|
62 |
</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.