Prerender Application Level Middleware - ASP.NET HttpModule
In the previous post Use Prerender to improve AngularJS SEO, I have explained different solutions at 3 different levels to implement Prerender. In this post, I will explain how to implement a ASP.NET HttpModule as a application level middleware to implement prerender. Since we call it ASP.NET, so it is applicable for both ASP.NET WebForm and MVC.
Application Level Middleware Architecture
At first, let's review what's the appliaction level middleware solution architecture.

ASP.NET HttpModule - PrerenderHttpModule
From above diagram, the easest way to implement it in ASP.NET is to create a HttpModule. Here I named it as PrerenderHttpModule.
At first, let's create PrerenderHttpModule and register BeginRequest event.
#region Implement IHttpModule
/// <summary>
/// init
/// </summary>
/// <param name="context"></param>
public void Init(HttpApplication context)
{
    context.BeginRequest += context_BeginRequest;
}
/// <summary>
/// dispose
/// </summary>
public void Dispose()
{
}
#endregion
#region Begin Request
protected void context_BeginRequest(object sender, EventArgs e)
{
    try
    {
        Prerender(sender as HttpApplication);
    }
    catch (Exception exception)
    {
        Debug.Write(exception.ToString());
    }
}
#endregion
In PrerenderHttpModule, the major method is Prerender(HttpApplication)
private void Prerender(HttpApplication application)
{
    var httpContext = application.Context;
    var request = httpContext.Request;
    var response = httpContext.Response;
    if (IsValidForPrerenderPage(request))
    {
        // generate URL
        var requestUrl = request.Url.AbsoluteUri;
        // if traffic is forwarded from https://, we convert http:// to https://.
        if (string.Equals(request.Headers[Constants.HttpHeader_XForwardedProto], Constants.HttpsProtocol, StringComparison.InvariantCultureIgnoreCase)
         && requestUrl.StartsWith(Constants.HttpProtocol, StringComparison.InvariantCultureIgnoreCase))
        {
            requestUrl = Constants.HttpsProtocol + requestUrl.Substring(Constants.HttpProtocol.Length);
        }
        var prerenderUrl = $"{Configuration.ServiceUrl.Trim('/')}/{requestUrl}";
        // create request
        var webRequest = (HttpWebRequest)WebRequest.Create(prerenderUrl);
        webRequest.Method = "GET";
        webRequest.UserAgent = request.UserAgent;
        webRequest.AllowAutoRedirect = false;
        webRequest.Headers.Add("Cache-Control", "no-cache");
        webRequest.ContentType = "text/html";
        // Proxy Information
        if (!string.IsNullOrEmpty(Configuration.ProxyUrl) && Configuration.ProxyPort > 0)
            webRequest.Proxy = new WebProxy(Configuration.ProxyUrl, Configuration.ProxyPort);
        // Add token
        if (!string.IsNullOrEmpty(Configuration.Token))
            webRequest.Headers.Add(Constants.HttpHeader_XPrerenderToken, Configuration.Token);
        var webResponse = default(HttpWebResponse);
        try
        {
            // Get the web response and read content etc. if successful
            webResponse = (HttpWebResponse)webRequest.GetResponse();
        }
        catch (WebException e)
        {
            // Handle response WebExceptions for invalid renders (404s, 504s etc.) - but we still want the content
            webResponse = e.Response as HttpWebResponse;
        }
        // write response
        response.StatusCode = (int)webResponse.StatusCode;
        foreach (string key in webResponse.Headers.Keys)
        {
            response.Headers[key] = webResponse.Headers[key];
        }
        using (var reader = new StreamReader(webResponse.GetResponseStream(), DefaultEncoding))
        {
            response.Write(reader.ReadToEnd());
        }
        response.Flush();
        application.CompleteRequest();
    }
}
Also, in order to make the logic flexible and easy to configure, I have add a configuration class and IsValidForPrerenderPage() method
private bool IsValidForPrerenderPage(HttpRequest request)
{
    var userAgent = request.UserAgent;
    var url = request.Url;
    var rawUrl = request.RawUrl;
    var relativeUrl = request.AppRelativeCurrentExecutionFilePath;
    // check if follows google search engine suggestion
    if (request.QueryString.AllKeys.Any(a => a.Equals(Constants.EscapedFragment, StringComparison.InvariantCultureIgnoreCase)))
        return true;
    // check if has user agent
    if (string.IsNullOrEmpty(userAgent))
        return false;
    // check if it's crawler user agent.
    var crawlerUserAgentPattern = Configuration.CrawlerUserAgentPattern ?? Constants.CrawlerUserAgentPattern;
    if (string.IsNullOrEmpty(crawlerUserAgentPattern)
     || !Regex.IsMatch(userAgent, crawlerUserAgentPattern, RegexOptions.IgnorePatternWhitespace))
        return false;
    // check if the extenion matchs default extension
    if (Regex.IsMatch(relativeUrl, DefaultIgnoredExtensions, RegexOptions.IgnorePatternWhitespace))
        return false;
    if (!string.IsNullOrEmpty(Configuration.AdditionalExtensionPattern) && Regex.IsMatch(relativeUrl, Configuration.AdditionalExtensionPattern, RegexOptions.IgnorePatternWhitespace))
        return false;
    if (!string.IsNullOrEmpty(Configuration.BlackListPattern)
      && Regex.IsMatch(rawUrl, Configuration.BlackListPattern, RegexOptions.IgnorePatternWhitespace))
        return false;
    if (!string.IsNullOrEmpty(Configuration.WhiteListPattern)
      && Regex.IsMatch(rawUrl, Configuration.WhiteListPattern, RegexOptions.IgnorePatternWhitespace))
        return true;
    return false;
}
The priority of checking whether is valid for prerender page:
- Constant -> EscapedFragment: _escaped_fragment_
 
User Agent: Setting -> CrawlerUserAgentPattern
(google)|(bing)|(Slurp)|(DuckDuckBot)|(YandexBot)|(baiduspider)|(Sogou)|(Exabot)|(ia_archiver)|(facebot)|(facebook)|(twitterbot)|(rogerbot)|(linkedinbot)|(embedly)|(quora)|(pinterest)|(slackbot)|(redditbot)|(Applebot)|(WhatsApp)|(flipboard)|(tumblr)|(bitlybot)|(Discordbot)
- Constant -> DefaultIgnoredExtensions
\\.vxml|js|css|less|png|jpg|jpeg|gif|pdf|doc|txt|zip|mp3|rar|exe|wmv|doc|avi|ppt|mpg|mpeg|tif|wav|mov|psd|ai|xls|mp4|m4a|swf|dat|dmg|iso|flv|m4v|torrent - Setting -> AdditionalExtensionPattern
 - Setting -> BlackListPattern
 - Setting -> WhiteListPattern
 - At last, return false.
 
Register PrerenderHttpModule
Generally, we have two different ways to register a HttpModule in ASP.NET,
- Use HttpModule configuration in Web.config
 - Use DynamicModuleUtility (Microsoft.Web.Infrastructure.dll)
 
Here, I have added the logic to support both, and I have added a app setting to control which one we want to use. By default, it will use DynamicModuleUtility, as we don't need to configure HttpModule in web.config, it's automatical.
Note, in order to use this HttpModule, please configure your IIS Application Pool to integrated mode.
<!--If it's false, please configure http module for UsePrestartForPrenderModule-->
<add key="UsePrestartForPrenderModule" value="true"/>
1. Use DynamicModuleUtility (Microsoft.Web.Infrastructure.dll)
This is the default configuration.
- Configre UsePrestartForPrerenderModule to true or remove this setting
 
<!--If it's false, please configure http module for UsePrestartForPrenderModule-->
<add key="UsePrestartForPrenderModule" value="true"/>
- Add reference for Microsoft.Web.Infrastructure.dll
 - Add a static class PrerenderPreApplicationStart for assembly prestart. We need to use static method in static class.
 
public static class PrerenderPreApplicationStart
{
    public const string StartMethodName = "Prestart";
    static bool UsePrestart = !bool.FalseString.Equals(ConfigurationManager.AppSettings[Constants.AppSetting_UsePrestartForPrenderModule], StringComparison.InvariantCultureIgnoreCase);
    /// <summary>
    /// used to configure for PreApplicationStart.
    /// i.e. [assembly: PreApplicationStartMethod(typeof(PrerenderPreApplicationStart), "Start")]
    /// </summary>
    public static void Prestart()
    {
        if (UsePrestart)
        {
            DynamicModuleUtility.RegisterModule(typeof(PrerenderHttpModule));
        }
    }
}
- Register PreApplicationStartMethodAttribute in AssemblyInfo.cs
 
[assembly: PreApplicationStartMethodAttribute(typeof(PrerenderPreApplicationStart), PrerenderPreApplicationStart.StartMethodName)]
Once the ASP.NET application loads this assembly, it will trigger PrerenderPreApplicationStart.Prestart() method, then registers the PrerenderHttpModule.
2. Use HttpModule configuration in Web.config
This is a very common and easy way, what we need to do is to:
- Configure UsePrestartForPrerenderModule to false
 
<!--If it's false, please configure http module for UsePrestartForPrenderModule-->
<add key="UsePrestartForPrenderModule" value="false"/>
- Configure PrerenderHttpModule
 
<system.webServer>
  <validation validateIntegratedModeConfiguration="false"/>
  <modules runAllManagedModulesForAllRequests="false">
    <!--Configure PrerenderHttpModule when UsePrestartForPrenderModule is false; -->
    <add name="prerender" type="DotNetOpen.PrerenderModule.PrerenderHttpModule, DotNetOpen.PrerenderModule" />
    <remove name="FormsAuthentication"/>
  </modules>
</system.webServer>
Configuration Section in Web.config
I have added a configuration section PrerenderConfigurationSection for prerender options
- Declare the configuration section in web.config section group.
 
  <configSections>
    <!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
    <section name="prerender" type="DotNetOpen.PrerenderModule.Configuration.PrerenderConfigurationSection, DotNetOpen.PrerenderModule"/>
    <section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false"/>
  </configSections>
- Configure options.
 
  <!--prerender settings-->
  <!--CrawlerUserAgentPattern: "(google)|(bing)|(Slurp)|(DuckDuckBot)|(YandexBot)|(baiduspider)|(Sogou)|(Exabot)|(ia_archiver)|(facebot)|(facebook)|(twitterbot)|(rogerbot)|(linkedinbot)|(embedly)|(quora)|(pinterest)|(slackbot)|(redditbot)|(Applebot)|(WhatsApp)|(flipboard)|(tumblr)|(bitlybot)|(Discordbot)"-->
  <!--WhiteListPattern, BlackListPattern: will check raw URL, which includes query string-->
  <!--AdditionalExtensionPattern: will only check extension-->
  <prerender ServiceUrl="http://localhost:3000"
             Token=""
             WhiteListPattern=""
             BlackListPattern=""
             AdditionalExtensionPattern=""
             ProxyUrl=""
             ProxyPort="80"/>
You can go to my github wiki page to get more details about each option: Configuration & Check Priority
Nuget Package
I have created a nuget package, which is very convenient if you don't want to dive deep into the source code.
Install Nuget Package in your project.
Visual Studio -> Tools -> Nuget Package Manager -> Package Manager Console.
Install-Package DotNetOpen.PrerenderModule
If you want to take a look more detail about this package, you can go https://www.nuget.org/packages/DotNetOpen.PrerenderModule/
There are several versions
- Version 1.01-1.0.2, they are for .NET Framework 4.6.2
 - From Version 1.0.2, I have changed the .NET Framework version from 4.6.2 to 4.0, so that more people can use it. Which means the latest Nuget Package is using .NET Framework 4.0
 
Register PrerenderHttpModule and configure options for prerender service.
I have fully documented how to do this in my github wiki page, you can go there take a look.
Done, try it out.
Github Project
I also have created a github project to host all source code includes sample code for testing: https://github.com/dingyuliang/prerender-dotnet, in this project, it includes ASP.NET HttpModule, ASP.NET Core Middleware, IIS Configuration 3 different solution.
For ASP.NET HttpModule, you can go to https://github.com/dingyuliang/prerender-dotnet/tree/master/src/DotNetPrerender
Prerender Related
- Use Prerender to improve AngularJS SEO
 - Setup Prerender Service for JavaScript SEO
 - Prerender Implementation Best Practice
 - Prerender Application Level Middleware - ASP.NET HttpModule
 - Prerender Application Level Middleware - ASP.NET Core Middleware
 
------------------------------------------------------------------------------------------------
Prerender Application Level Middleware - ASP.NET HttpModule的更多相关文章
- Prerender Application Level Middleware - ASP.NET Core Middleware
		
In the previous post Use Prerender to improve AngularJS SEO, I have explained different solutions at ...
 - Examining Application Startup in ASP.NET 5
		
By Steve Smith June 23, 2015 ASP.NET 5 differs from previous versions of ASP.NET in many ways. Gone ...
 - ASP.NET (HttpModule,HttpHandler)
		
asp.net 事件模型机制 ----------------------- 一 客户的请求页面由aspnet_isapi.dll这个动态连接库来处理,把请求的aspx文件发送给CLR进行编译执行,然 ...
 - ASP.NET HttpModule URL 重写 (一) 【Z】
		
大家好,又来和大家见面了,此次给大家带来的URL重写,关于URL重写是什么,有什么好处,如何重写,今天我和大家一起分享一下我的经验 一.URL重写 URL重写就是首先获得一个进入的URL请求然后把它重 ...
 - [转]Session and application state in ASP.NET Core
		
本文转自:https://docs.microsoft.com/en-us/aspnet/core/fundamentals/app-state By Rick Anderson and Steve ...
 - asp.net httpmodule 访问页面控件 备忘
		
用到的时候发现还得找代码,存一个例子方便自己和他人修改: using System; using System.Data; using System.Configuration; using Syst ...
 - Patterns for application development with ASP.NET Core
		
此文章翻译自 NDC { London } 16-20 January 2017 上, Damian Edwards和David Fowler的演讲,如果翻译不周,请大家指出错误. Logging 生 ...
 - asp.net HttpModule和HttpHandler
		
ASP.Net处理Http Request时,使用Pipeline(管道)方式,由各个HttpModule对请求进行处理,然后到达 HttpHandler,HttpHandler处理完之后,仍经过Pi ...
 - 使用Azure Application Insignhts监控ASP.NET Core应用程序
		
Application Insignhts是微软开发的一套监控程序.他可以对线上的应用程序进行全方位的监控,比如监控每秒的请求数,失败的请求,追踪异常,对每个请求进行监控,从http的耗时,到SQL查 ...
 
随机推荐
- BS中保存参数
			
开发中经常需要将值存起来,当点击某一项时以便知道点击了哪一项. 一:应用JS页面跳转(牛腩中讲到) HTML: <td class="txt c"><a href ...
 - 阿里云64位centos6.3系统上编译安装redis
			
环境 系统:阿里云64位centos 6.3 [rao@AY~]$ cat /etc/issue CentOS release 6.3 (Final) Kernel \r on an \m [rao@ ...
 - 解决ping 127.0.0.1不通的问题
			
用树莓派放在家里当pt下载器,一直挺惬意的,因为没有公网ip用vps和frp配置代理,偶尔ssh上去看看,一段时间也用得好好的. 可是最近这几天,在办公室ssh上去死活连不上. 于是回去后开始折腾,局 ...
 - 转:mac 设置root 密码
			
终端中输:sudo passwd rootpasswd root是修改root的命令,unix下sudo是以当前用户的身份执行root的命令,以避免输入root的密码但是sudo依赖于配置文件/etc ...
 - maven install时报错Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:2.12.4:test
			
事故现场: 解决办法: 一是命令行, mvn clean package -Dmaven.test.skip=true 二是写入pom文件, <plugin> <groupId> ...
 - js中,“\”转义字符的使用
			
(1)针对双引号“”的使用: html= "<a href=\"javascript:void(0)\" onclick=\"getSubContent( ...
 - 学习EF之CodeFirst一
			
最近将花点时间学习EF相关知识,通过文章来进行一个完整的学习,Code First是由先有代码后生成数据库:将通过一实例来进行学习:我们简单分为三层,其中DataLibrary为EF上下文处理层,Mo ...
 - 使用python进行图像处理-调整图片大小
			
python有一个图像处理库——PIL,可以处理图像文件.PIL提供了功能丰富的方法,比如格式转换.旋转.裁剪.改变尺寸.像素处理.图片合并等等等等,非常强大. 举个简单的例子,调整图片的大小: im ...
 - 不吐不快之EJB演练——开篇概述
			
EJB(Enterprise Java Bean)是J2EE规范的重要核心,它是一个用户分布式业务应用的标准服务端组件模型,它是一种能够高速开发大规模企业应用的组件体系结构.上面这样官方的解释可能对于 ...
 - Solr 缓存配置
			
http://www.blogjava.net/xiaohuzi2008/archive/2012/12/03/392376.html