How to force browser to cache file in IIS? - asp.net

Currently, I append application version to all JavaScript & StyleSheet files to prevent caching in old browsers. It works fine. However, I would like to cache all JavaScript & StyleSheet without any request to web server.
With current setting, web server responses like the following image. I don't want browser to spend time to check ETag for all JavaScript & StyleSheet files.
Here is current setting in web.config
<clientCache cacheControlMode="UseMaxAge" cacheControlMaxAge="365.00:00:00" />

Here is a simple solution with an HttpModule which works across ASP implementations. We used it in a SPA app. It will ask browser to cache certain resources for a year. The home/landing page is an exception and will always be checked using ETag.
Step 1:
The first step you've have already done which is adding the version number in the url of each resource. We do this as an automated step in the build process.
Step 2: Next add a CacheModule class into your app:
public class CacheModule : IHttpModule
{
// extensions to cache, e.g. ".css",".html",".js",".png",".jpg",".gif",".ico",".woff",".eot",".svg",".ttf"
private readonly string[] _extensions = ConfigurationManager.AppSettings["CacheModule_Extensions"].Split(",");
private readonly string[] _exceptions = ConfigurationManager.AppSettings["CacheModule_Exceptions"].Split(",");
public void Dispose() {}
public void Init(HttpApplication context)
{
context.EndRequest += (sender, args) =>
{
var ctx = HttpContext.Current;
var path = ctx.Request.Url.GetComponents(UriComponents.Path, UriFormat.SafeUnescaped);
var isExcept = _exceptions.Any(path.Contains);
ctx.Response.AddHeader("Cache-Control", "private");
if (_extensions.Any(path.Contains) && ! isExcept )
{
ctx.Response.AddHeader("Expires", (DateTime.Now.ToUniversalTime().AddYears(1)).ToString("r"));
}
else if (isExcept)
{
ctx.Response.AddHeader("Expires", (DateTime.Now.ToUniversalTime().AddHours(-1)).ToString("r"));
ctx.Response.AddHeader("Max-Age", "0");
}
};
}
}
Step 3: Finally you put in your config:
<?xml version="1.0"?>
<configuration>
<appSettings>
<!-- resource extensions to cache -->
<add key="CacheModule_Extensions" value=".css,.html,.js,.png,.jpg,.gif,.ico,.woff,.eot,.svg,.ttf" />
<!-- exceptions to caching such as home/landing page e.g. "index.html" or any page/resource with known url that users may enter directly or may be redirected to -->
<add key="CacheModule_Exceptions" value="index.html,buildinfo.html,unsupportedmobilebrowser.html, unsupportedbrowser.html" />
</appSettings>
<system.webServer>
<modules>
<add name="CacheModule" type="MyApp.Caching.CacheModule, MyApp"/>
</modules>
</system.webServer>
</configuration>

As IIS serves files and watches for changes, IIS will always send re-validation cache header, forcing browser to check for changes. To get rid of this problem, we designed CachedRoute as shown below, however this works well in ASP.NET MVC but with little changes you can implement same in ASP.NET WebForms as well.
This code also gives you benefit of moving your static resources to CDN.
Cached Version URL Prefix
We had to come up with versioning of static content like /cached/version/, this is nothing but just a url prefix for static asset. version can be any random alphanumeric string totally useless, but identifies different version.
Well one of the easiest approach is to use a version key in the URL.
First, create build version in AssemblyInfo.cs
[assembly: AssemblyVersion("1.5.*.*")]
Leave, * as build number replacement, .net compiler will automatically increment with each build.
Or define version in app settings as follow
<appSettings>
<add key="Static-Content-Version" value="1.5.445.55565"/>
<add key="CDNHost" value="cdn1111.cloudfront.net"/>
</appSettings>
// Route configuration
// set CDN if you have
string cdnHost = WebConfigrationManager.AppSettings["CDNHost"];
if(!string.IsEmpty(cdnHost)){
CachedRoute.CDNHost = cdnHost;
}
// get assembly build information
string version = typeof(RouteConfig).Assembly.GetName().Version.ToString();
CachedRoute.CORSOrigins = "*";
CachedRoute.Register(routes, TimeSpam.FromDays(30), version);
Now on each page, reference your static content as,
<script src="#CachedRoute.CachedUrl("/scripts/jquery-1.11.1.js")"></script>
While rendering, your page will be rendered as (without CDN)
<script src="/cached/1.5.445.55565/scripts/jquery-1.11.1.js"></script>
With CDN as
<script
src="//cdn111.cloudfront.net/cached/1.5.445.55565/scripts/jquery-1.11.1.js">
</script>
Putting version in URL path instead of query string makes CDN perform better as query strings can be ignored in CDN configuration (which is usually the default case).
Advantages
If you set version same as Assembly version, it becomes easy to put new build. Otherwise you have to manually change web.config with every time when version changes.
CachedRoute Class from
https://github.com/neurospeech/atoms-mvc.net/blob/master/src/Mvc/CachedRoute.cs
public class CachedRoute : HttpTaskAsyncHandler, IRouteHandler
{
private CachedRoute()
{
// only one per app..
}
private string Prefix { get; set; }
public static string Version { get; private set; }
private TimeSpan MaxAge { get; set; }
public static string CORSOrigins { get; set; }
//private static CachedRoute Instance;
public static void Register(
RouteCollection routes,
TimeSpan? maxAge = null,
string version = null)
{
CachedRoute sc = new CachedRoute();
sc.MaxAge = maxAge == null ? TimeSpan.FromDays(30) : maxAge.Value;
if (string.IsNullOrWhiteSpace(version))
{
version = WebConfigurationManager.AppSettings["Static-Content-Version"];
if (string.IsNullOrWhiteSpace(version))
{
version = Assembly.GetCallingAssembly().GetName().Version.ToString();
}
}
Version = version;
var route = new Route("cached/{version}/{*name}", sc);
route.Defaults = new RouteValueDictionary();
route.Defaults["version"] = "1";
routes.Add(route);
}
public override bool IsReusable
{
get
{
return true;
}
}
public static string CDNHost { get; set; }
public static HtmlString CachedUrl(string p)
{
if (!p.StartsWith("/"))
throw new InvalidOperationException("Please provide full path starting with /");
string cdnPrefix = string.IsNullOrWhiteSpace(CDNHost) ? "" : ("//" + CDNHost);
return new HtmlString(cdnPrefix + "/cached/" + Version + p);
}
public override async Task ProcessRequestAsync(HttpContext context)
{
var Response = context.Response;
Response.Cache.SetCacheability(HttpCacheability.Public);
Response.Cache.SetMaxAge(MaxAge);
Response.Cache.SetExpires(DateTime.Now.Add(MaxAge));
if (CORSOrigins != null)
{
Response.Headers.Add("Access-Control-Allow-Origin", CORSOrigins);
}
string FilePath = context.Items["FilePath"] as string;
var file = new FileInfo(context.Server.MapPath("/" + FilePath));
if (!file.Exists)
{
throw new FileNotFoundException(file.FullName);
}
Response.ContentType = MimeMapping.GetMimeMapping(file.FullName);
using (var fs = file.OpenRead())
{
await fs.CopyToAsync(Response.OutputStream);
}
}
IHttpHandler IRouteHandler.GetHttpHandler(RequestContext requestContext)
{
//FilePath = requestContext.RouteData.GetRequiredString("name");
requestContext.HttpContext.Items["FilePath"] = requestContext.RouteData.GetRequiredString("name");
return (IHttpHandler)this;
}
}
Sample Response Headers for first request
Access-Control-Allow-Origin:*
Cache-Control:public
Content-Length:453
Content-Type:image/png
Date:Sat, 04 Jul 2015 08:04:55 GMT
Expires:Mon, 03 Aug 2015 00:46:43 GMT
Server:Microsoft-IIS/8.5
Via:1.1 ********************************
X-Amz-Cf-Id: ******************************
X-AspNet-Version:4.0.30319
X-AspNetMvc-Version:5.2
X-Cache:Miss from cloudfront
X-Powered-By:ASP.NET
See, there is no ETag, Vary by, Last Modified or validation header and also see explicit Expires header, when you send explicit Expires header, Browser will never try to validate cache.

Related

Asp.net web API CORS series/mysterious issue

I am facing a CORS policy problem and I do not know how to fix it. I tried many approaches but what makes me crazy is that the service works fine on some devices and I can utilize all its resources and works a little bit on others and does not work at others while the whole devices having the same configuration equally set. To be more clear I am having a Web application based entirely and only on AngularJS 2 and a Web API that exposes a few actions. I installed the IIS and hosted the Web API on the default Web Site, which means it can be called from port 80 for simplicity, and I hosted the Web application using port 4200. Now let's give more detail about my Web API application structure.
EMG.WebApi.Core -> this project is the core project in which I put the controller classes and the web configuration class
EMG.WebApi.WebHost -> this project is used merely for hosting and it contains a reference to the EMG.WebApi.Core project and is the one that contains the Global.asax and within its Application_Start I am calling the Register method of Configuration class of the WebApi.Core and give it as a parameter the GlobalConfiguration object to register my handlers, tracers etc.
using EMG.ElectrometerWS.WebApi.Core;
using System;
using System.Web.Http;
using EMG.ElectrometerWS.WebApi.Core.Configuration;
namespace EMG.ElectrometerWS.WebApi.WebHost
{
public class Global : System.Web.HttpApplication
{
protected void Application_Start(object sender, EventArgs e)
{
WebApiConfig.Register(GlobalConfiguration.Configuration);
GlobalConfiguration.Configuration.EnsureInitialized();
}
}
}
using EMG.ElectrometerWS.WebApi.Core.Handlers;
using EMG.ElectrometerWS.WebApi.Core.Tracer;
using System;
using System.Configuration;
using System.Web.Http;
using System.Web.Http.Cors;
using System.Web.Http.Tracing;
namespace EMG.ElectrometerWS.WebApi.Core.Configuration
{
public static class WebApiConfig
{
...
public static string CorsOrigin
{
get
{
string result =
ConfigurationManager.AppSettings.Get("CorsOrigin");
if (!string.IsNullOrEmpty(result))
{
return result;
}
throw new Exception("AppSetting CorsOrigin not found in
web.config file.");
}
}
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
EnableCorsAttribute enableCors =
new EnableCorsAttribute(CorsOrigin, "*", "*");
config.EnableCors(enableCors);
// Web API routes
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
//config.Routes.MapHttpRoute(
// name: "Secret Api",
// routeTemplate: "secretapi/{controller}/{id}",
// defaults: new { id = RouteParameter.Optional },
// constraints: null,
// handler: new ApiKeyProtectionMessageHandler() {
// InnerHandler = new HttpControllerDispatcher(config)
// });
// Enable ASP.NET Web API tracing
//config.EnableSystemDiagnosticsTracing();
//config.Services.Replace(typeof(ITraceWriter), new
// EntryExitTracer());
//config.Services.Replace(typeof(ITraceWriter), new WebApiTracer());
//config.MessageHandlers.Add(new EmptyPostBodyMessageHandler());
// Message handler to check the performance in production
environment ONLY
config.MessageHandlers.Add(new TracingHandler());
//config.MessageHandlers.Add(new XHttpMethodOverrideHandler());
config.MessageHandlers.Add(new JwtHandler());
}
}
}
EMG.ElectrometerWS.WebApi.WebHost Web.Cofig
<appSettings>
....
<add key="CorsOrigin" value="http://localhost:4200"/>
</appSettings>
What makes me crazy is that everything works fine on my colleague laptop and he can use all the actions. On mine I cannot call some of PUT methods while I can for others on other colleague/testers they can only call GET methods!!! And increases my surprises is that after clearing the browser history/cookies one of those laptops that have only GET methods being called have all things works fine.
What I have tried:
First I added the below code as you can notice above to the configuration class
EnableCorsAttribute enableCors =
new EnableCorsAttribute(CorsOrigin, "*", "*");
config.EnableCors(enableCors);
Creating the following handler and registered it as the first handler before other handlers
public class CorsPreflightHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage
request,
CancellationToken cancellationToken)
{
if (request.Headers.Contains("Origin") && request.Method == HttpMethod.Options)
{
var response = new HttpResponseMessage(HttpStatusCode.OK);
response.Headers.Add("Access-Control-Allow-Origin", "*");
response.Headers.Add("Access-Control-Allow-Methods", "*");
return response;
}
return await base.SendAsync(request, cancellationToken);
}
}
Removing the previous code and configured the CORS using the Web.config file of the WebHost project
<system.webServer>
<httpProtocol>
<customHeaders>
<add name="Access-Control-Allow-Origin" value="http:localhost:4200" />
<add name="Access-Control-Allow-Methods" value="*" />
<add name="Access-Control-Allow-Headers" value="*" />
</customHeaders>
</httpProtocol>
</system.webServer>
Finally removing the web config tags and enabled if on each controller class
[EnableCors(origins: "http://localhost:4200", headers: "*", methods: "*")]
public class XXXController : ApiController
{
Public string Get(int id)
{
return "value";
}
}
The CORS issue seems solved in meantime. I used the first approach by setting an application setting key/value for the allowed Origin then use that key/value to retrieve and register that Origin through a CORS attribute.
<appSettings>
....
<add key="CorsOrigin" value="http://localhost:4200"/>
</appSettings>
public static string CorsOrigin
{
get
{
string result =
ConfigurationManager.AppSettings.Get("CorsOrigin");
if (!string.IsNullOrEmpty(result))
{
return result;
}
throw new Exception("AppSetting CorsOrigin not found in web.config file.");
}
}
public static void Register(HttpConfiguration config)
{
....
EnableCorsAttribute enableCors =
new EnableCorsAttribute(CorsOrigin, "*", "*");
config.EnableCors(enableCors);
}
However, I still do not know what cause the problem from the beginning it may be an IIS issue or missing feature etc.

Override host of webapi odata links

I'm using WebAPI 2.2 and Microsoft.AspNet.OData 5.7.0 to create an OData service that supports paging.
When hosted in the production environment, the WebAPI lives on a server that is not exposed externally, hence the various links returned in the OData response such as the #odata.context and #odata.nextLink point to the internal IP address e.g. http://192.168.X.X/<AccountName>/api/... etc.
I've been able to modify the Request.ODataProperties().NextLink by implementing some logic in each and every ODataController method to replace the internal URL with an external URL like https://account-name.domain.com/api/..., but this is very inconvenient and it only fixes the NextLinks.
Is there some way to set an external host name at configuration time of the OData service? I've seen a property Request.ODataProperties().Path and wonder if it's possible to set a base path at the config.MapODataServiceRoute("odata", "odata", GetModel()); call, or in the GetModel() implementation using for instance the ODataConventionModelBuilder?
UPDATE: The best solution I've come up with so far, is to create a BaseODataController that overrides the Initialize method and checks whether the Request.RequestUri.Host.StartsWith("beginning-of-known-internal-IP-address") and then do a RequestUri rewrite like so:
var externalAddress = ConfigClient.Get().ExternalAddress; // e.g. https://account-name.domain.com
var account = ConfigClient.Get().Id; // e.g. AccountName
var uriToReplace = new Uri(new Uri("http://" + Request.RequestUri.Host), account);
string originalUri = Request.RequestUri.AbsoluteUri;
Request.RequestUri = new Uri(Request.RequestUri.AbsoluteUri.Replace(uriToReplace.AbsoluteUri, externalAddress));
string newUri = Request.RequestUri.AbsoluteUri;
this.GetLogger().Info($"Request URI was rewritten from {originalUri} to {newUri}");
This perfectly fixes the #odata.nextLink URLs for all controllers, but for some reason the #odata.context URLs still get the AccountName part (e.g. https://account-name.domain.com/AccountName/api/odata/$metadata#ControllerName) so they still don't work.
Rewriting the RequestUri is sufficient to affect #odata.nextLink values because the code that computes the next link depends on the RequestUri directly. The other #odata.xxx links are computed via a UrlHelper, which is somehow referencing the path from the original request URI. (Hence the AccountName you see in your #odata.context link. I've seen this behavior in my code, but I haven't been able to track down the source of the cached URI path.)
Rather than rewrite the RequestUri, we can solve the problem by creating a CustomUrlHelper class to rewrite OData links on the fly. The new GetNextPageLink method will handle #odata.nextLink rewrites, and the Link method override will handle all other rewrites.
public class CustomUrlHelper : System.Web.Http.Routing.UrlHelper
{
public CustomUrlHelper(HttpRequestMessage request) : base(request)
{ }
// Change these strings to suit your specific needs.
private static readonly string ODataRouteName = "ODataRoute"; // Must be the same as used in api config
private static readonly string TargetPrefix = "http://localhost:8080/somePathPrefix";
private static readonly int TargetPrefixLength = TargetPrefix.Length;
private static readonly string ReplacementPrefix = "http://www.contoso.com"; // Do not end with slash
// Helper method.
protected string ReplaceTargetPrefix(string link)
{
if (link.StartsWith(TargetPrefix))
{
if (link.Length == TargetPrefixLength)
{
link = ReplacementPrefix;
}
else if (link[TargetPrefixLength] == '/')
{
link = ReplacementPrefix + link.Substring(TargetPrefixLength);
}
}
return link;
}
public override string Link(string routeName, IDictionary<string, object> routeValues)
{
var link = base.Link(routeName, routeValues);
if (routeName == ODataRouteName)
{
link = this.ReplaceTargetPrefix(link);
}
return link;
}
public Uri GetNextPageLink(int pageSize)
{
return new Uri(this.ReplaceTargetPrefix(this.Request.GetNextPageLink(pageSize).ToString()));
}
}
Wire-up the CustomUrlHelper in the Initialize method of a base controller class.
public abstract class BaseODataController : ODataController
{
protected abstract int DefaultPageSize { get; }
protected override void Initialize(System.Web.Http.Controllers.HttpControllerContext controllerContext)
{
base.Initialize(controllerContext);
var helper = new CustomUrlHelper(controllerContext.Request);
controllerContext.RequestContext.Url = helper;
controllerContext.Request.ODataProperties().NextLink = helper.GetNextPageLink(this.DefaultPageSize);
}
Note in the above that the page size will be the same for all actions in a given controller class. You can work around this limitation by moving the assignment of ODataProperties().NextLink to the body of a specific action method as follows:
var helper = this.RequestContext.Url as CustomUrlHelper;
this.Request.ODataProperties().NextLink = helper.GetNextPageLink(otherPageSize);
The answer by lencharest is promising, but I found an improvement on his method. Rather than using the UrlHelper, I created a class derived from System.Net.Http.DelegatingHandler. This class is inserted (first) into the message handling pipeline and thus has a crack at altering the incoming HttpRequestMessage. It's an improvement over the above solution because in addition to altering the controller-specific URLs (as the UrlHelper does, e,g, https://data.contoso.com/odata/MyController), it also alters the url that appears as the xml:base in the OData service document (e.g., https://data.contoso.com/odata).
My particular application was to host an OData service behind a proxy server, and I wanted all the URLs presented by the server to be the externally-visible URLs, not the internally-visible ones. And, I didn't want to have to rely on annotations for this; I wanted it to be fully automatic.
The message handler looks like this:
public class BehindProxyMessageHandler : DelegatingHandler
{
protected async override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
var builder = new UriBuilder(request.RequestUri);
var visibleHost = builder.Host;
var visibleScheme = builder.Scheme;
var visiblePort = builder.Port;
if (request.Headers.Contains("X-Forwarded-Host"))
{
string[] forwardedHosts = request.Headers.GetValues("X-Forwarded-Host").First().Split(new char[] { ',' });
visibleHost = forwardedHosts[0].Trim();
}
if (request.Headers.Contains("X-Forwarded-Proto"))
{
visibleScheme = request.Headers.GetValues("X-Forwarded-Proto").First();
}
if (request.Headers.Contains("X-Forwarded-Port"))
{
try
{
visiblePort = int.Parse(request.Headers.GetValues("X-Forwarded-Port").First());
}
catch (Exception)
{ }
}
builder.Host = visibleHost;
builder.Scheme = visibleScheme;
builder.Port = visiblePort;
request.RequestUri = builder.Uri;
var response = await base.SendAsync(request, cancellationToken);
return response;
}
}
You wire the handler up in WebApiConfig.cs:
config.Routes.MapODataServiceRoute(
routeName: "odata",
routePrefix: "odata",
model: builder.GetEdmModel(),
pathHandler: new DefaultODataPathHandler(),
routingConventions: ODataRoutingConventions.CreateDefault()
);
config.MessageHandlers.Insert(0, new BehindProxyMessageHandler());
There is another solution, but it overrides url for the entire context.
What I'd like to suggest is:
Create owin middleware and override Host and Scheme properties inside
Register the middleware as the first one
Here is an example of middleware
public class RewriteUrlMiddleware : OwinMiddleware
{
public RewriteUrlMiddleware(OwinMiddleware next)
: base(next)
{
}
public override async Task Invoke(IOwinContext context)
{
context.Request.Host = new HostString(Settings.Default.ProxyHost);
context.Request.Scheme = Settings.Default.ProxyScheme;
await Next.Invoke(context);
}
}
ProxyHost is the host you want to have. Example: test.com
ProxyScheme is the scheme you want: Example: https
Example of middleware registration
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.Use(typeof(RewriteUrlMiddleware));
var config = new HttpConfiguration();
WebApiConfig.Register(config);
app.UseWebApi(config);
}
}
A couple of years later, using ASP.NET Core, I figured that the easiest way to apply it in my service was to just create a filter that masquerades the host name. (AppConfig is a custom configuration class that contains the host name, among other things.)
public class MasqueradeHostFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
var appConfig = context.HttpContext.RequestServices.GetService<AppConfig>();
if (!string.IsNullOrEmpty(appConfig?.MasqueradeHost))
context.HttpContext.Request.Host = new HostString(appConfig.MasqueradeHost);
}
}
Apply the filter to the controller base class.
[MasqueradeHostFilter]
public class AppODataController : ODataController
{
}
The result is a nicely formatted output:
{ "#odata.context":"https://app.example.com/odata/$metadata" }
Just my two cents.
Using system.web.odata 6.0.0.0.
Setting the NextLink property too soon is problematic. Every reply will then have a nextLink in it. The last page should of course be free of such decorations.
http://docs.oasis-open.org/odata/odata-json-format/v4.0/os/odata-json-format-v4.0-os.html#_Toc372793048 says:
URLs present in a payload (whether request or response) MAY be
represented as relative URLs.
One way that I hope will work is to override EnableQueryAttribute:
public class myEnableQueryAttribute : EnableQueryAttribute
{
public override IQueryable ApplyQuery(IQueryable queryable, ODataQueryOptions queryOptions)
{
var result = base.ApplyQuery(queryable, queryOptions);
var nextlink = queryOptions.Request.ODataProperties().NextLink;
if (nextlink != null)
queryOptions.Request.ODataProperties().NextLink = queryOptions.Request.RequestUri.MakeRelativeUri(nextlink);
return result;
}
}
ApplyQuery() is where the "overflow" is detected. It basically asks for pagesize+1 rows and will set NextLink if the result set contains more than pagesize rows.
At this point it is relatively easy to rewrite NextLink to a relative URL.
The downside is that every odata method must now be adorned with the new myEnableQuery attribute:
[myEnableQuery]
public async Task<IHttpActionResult> Get(ODataQueryOptions<TElement> options)
{
...
}
and other URLs embedded elsewhere remains problematic. odata.context remains a problem. I want to avoid playing with the request URL, because I fail to see how that is maintainable over time.
Your question boils down to controlling the service root URI from within the service itself. My first thought was to look for a hook on the media type formatters used to serialize responses. ODataMediaTypeFormatter.MessageWriterSettings.PayloadBaseUri and ODataMediaTypeFormatter.MessageWriterSettings.ODataUri.ServiceRoot are both settable properties that suggest a solution. Unfortunately, ODataMediaTypeFormatter resets these properties on every call to WriteToStreamAsync.
The work-around is not obvious, but if you dig through the source code you'll eventually reach a call to IODataPathHandler.Link. A path handler is an OData extension point, so you can create a custom path handler that always returns an absolute URI which begins with the service root you desire.
public class CustomPathHandler : DefaultODataPathHandler
{
private const string ServiceRoot = "http://example.com/";
public override string Link(ODataPath path)
{
return ServiceRoot + base.Link(path);
}
}
And then register that path handler during service configuration.
// config is an instance of HttpConfiguration
config.MapODataServiceRoute(
routeName: "ODataRoute",
routePrefix: null,
model: builder.GetEdmModel(),
pathHandler: new CustomPathHandler(),
routingConventions: ODataRoutingConventions.CreateDefault()
);

Session access disable for asp.net bundle requests

I am using asp.net and MVC4 for my application and also i am using bundling for css and js files.
When i look into my trace files, i observed that all my bundle requests are using session. i.e, all bundle requests are passing through sessionstatemodule.
My trace looks like below.
155. NOTIFY_MODULE_START ModuleName="Session", Notification="REQUEST_ACQUIRE_STATE", fIsPostNotification="false" 06:59:43.480
156. AspNetPipelineEnter Data1="System.Web.SessionState.SessionStateModule" 06:59:43.480
157. AspNetSessionDataBegin 06:59:43.480
158. AspNetSessionDataEnd 06:59:43.996
159. AspNetPipelineLeave Data1="System.Web.SessionState.SessionStateModule" 06:59:43.996
160. NOTIFY_MODULE_COMPLETION ModuleName="Session", Notification="REQUEST_ACQUIRE_STATE", fIsPostNotificationEvent="false", CompletionBytes="0", ErrorCode="The operation completed successfully. (0x0)" 06:59:43.996
161. NOTIFY_MODULE_END ModuleName="Session", Notification="REQUEST_ACQUIRE_STATE", fIsPostNotificationEvent="false", NotificationStatus="NOTIFICATION_CONTINUE" 06:59:43.996
I think i dont need session access for my bundle requests. How can i disable the session access for my bundle requests?
If I understand your question correctly you want to disable session state for your static resources. For this you can do two things:
1) Disable SessionState for Controller
For that you need to import System.Web.SessionState namespace and you then decorate your controller with following line of code:
[SessioState(SessionStateBehavior.Disabled)
public class HomeController: Controller
{
}
For more information you can visit following link:
Controlling Session State Behavior
2) Create Static Resources
IIS Setup:
Create two website in your inetpub directory.
www.domain.com // For main site
static.domain.com // Foe static resources
Now point them to same physical directory i.e.
C:\inetpub\www.domain.com
Redirect domain.com to www.domain.com
Redirect any domain.com request to www.domain.com.
so that any domain.com request must be redirected to www.domain.com
because cookie set for domain.com will also be shared by all sub
domain including static.domain.com hence it is much important steps
** Code changes**
Add below code in your web.config file:
<appSettings>
<add key="StaticSiteName" value="static.domain.com"/>
<add key="StaticDomain" value="http://static.domain.com"/>
<add key="MainDomain" value="http://www.domain.com"/>
</appSettings>
use PreApplicationStartMethod and Microsoft.Web.Infrastructure to dynamically register HTTP module in pre-application startup stage
public class PreApplicationStart
{
public static void Start()
{
string strStaticSiteName = ConfigurationManager.AppSettings["StaticSiteName"];
string strCurrentSiteName = HostingEnvironment.SiteName;
if (strCurrentSiteName.ToLower() == strStaticSiteName.ToLower())
{
DynamicModuleUtility.RegisterModule(typeof(StaticResource));
}
}
}
public class StaticResource : IHttpModule
{
public void Init(HttpApplication context)
{
context.BeginRequest += new EventHandler(context_BeginRequest);
}
void context_BeginRequest(object sender, EventArgs e)
{
HttpContext context = HttpContext.Current;
string strUrl = context.Request.Url.OriginalString.ToLower();
//HERE WE CAN CHECK IF REQUESTED URL IS FOR STATIC RESOURCE OR NOT
if (strUrl.Contains("Path/To/Static-Bundle/Resource") == false)
{
string strMainDomain = ConfigurationManager.AppSettings["MainDomain"];
context.Response.Redirect(strMainDomain);
}
}
public void Dispose()
{
}
}
Add an Extension Method
public static class Extensions
{
public static string StaticContent(this UrlHelper url, string contentPath)
{
string strStaticDomain = ConfigurationManager.AppSettings["StaticDomain"];
return contentPath.Replace("~", strStaticDomain);
}
}
Now use #Url.StaticContent() from view so that it will render static resource url with static.domain.com whether it is image, script, CSS, or bundles or wherever we want to refer cookieless domain. for e.g.
<link href="#Url.StaticContent("~/Content/Site.css")" rel="Stylesheet" />
<script src="#Url.StaticContent("~/Scripts/jquery-1.7.1.js")" type="text/javascript"></script>
<script src="#Url.StaticContent("~/bundles/jquery")" type="text/javascript"></script>
<img src="#Url.StaticContent("~/Images/heroAccent.png")" alt="" />
Visit below link for full information as article is pretty big:
Cookie less domain for bundling and static resources
Hope this will help you to achieve your goal.

ASP.NET MVC - compression + caching

I've seen a number of options for adding GZIP/DEFLATE compression to ASP.Net MVC output, but they all seem to apply the compression on-the-fly.. thus do not take advange of caching the compressed content.
Any solutions for enabling caching of the compressed page output? Preferably in the code, so that the MVC code can check if the page has changed, and ship out the precompressed cached content if not.
This question really could apply to regular asp.net as well.
[Compress]
[OutputCache(Duration = 600, VaryByParam = "*", VaryByContentEncoding="gzip;deflate")]
public ActionResult Index()
{
return View();
}
Use caching options using attributes (for MVC), and do not think about compression since IIS/IISExpress automatically compresses your output if you enable it.
the way it works, mvc does not enable caching of individual fragments or parts of output (partial content caching). if you want this, consider using a service like CloudFlare (is there any other like CF?). it automatically caches your output and caches fragments of your output and provides many other performance and security improvements, all without a change in your code.
If this is not an option for you, then you still may use IISpeed (it is a IIS port of Google's mod_pagespeed). It provides some interesting settings like whitespace removal, inline css and js compression, js file merge and many other.
Both CF and IISpeed does not care how your site is built, they work on http/html level, so they both work on MVC, Classic ASP.NET, php or even raw html files.
You can create a attribute like
public class EnableCompressionAttribute : ActionFilterAttribute
{
const CompressionMode Compress = CompressionMode.Compress;
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
HttpRequestBase request = filterContext.HttpContext.Request;
HttpResponseBase response = filterContext.HttpContext.Response;
string acceptEncoding = request.Headers["Accept-Encoding"];
if (acceptEncoding == null)
return;
else if (acceptEncoding.ToLower().Contains("gzip"))
{
response.Filter = new GZipStream(response.Filter, Compress);
response.AppendHeader("Content-Encoding", "gzip");
}
else if (acceptEncoding.ToLower().Contains("deflate"))
{
response.Filter = new DeflateStream(response.Filter, Compress);
response.AppendHeader("Content-Encoding", "deflate");
}
}
}
Add entry in Global.asax.cs
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new EnableCompressionAttribute());
}
Then you can use this attribute as:
[EnableCompression]
public ActionResult WithCompression()
{
ViewBag.Content = "Compressed";
return View("Index");
}
You can download working example from Github:
https://github.com/ctesene/TestCompressionActionFilter
This link seems fairly close to what you require. It caches compressed dynamically generated pages. Although the example uses Web forms, It can be adapted to MVC by using an OutputCache attribute
[OutputCache(Duration = 600, VaryByParam = "*", VaryByContentEncoding="gzip;deflate")]
You could create a Cache Attribute:
public class CacheAttribute : ActionFilterAttribute
{
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
HttpCachePolicyBase cache = filterContext.HttpContext.Response.Cache;
if (Enabled)
{
cache.SetExpires(System.DateTime.Now.AddDays(30));
}
else
{
cache.SetCacheability(HttpCacheability.NoCache);
cache.SetNoStore();
}
}
public bool Enabled { get; set; }
public CacheAttribute()
{
Enabled = true;
}
}
See Improving performance with output caching for a full introduction on the subject. The main recommendation is to use the [OutputCache] attribute on the Action to which caching should be applied.
use namespace
using System.Web.Mvc;
using System.IO.Compression;
create ClassName.cs in you main project
public class CompressAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var _encodingsAccepted = filterContext.HttpContext.Request.Headers["Accept-Encoding"];
if (string.IsNullOrEmpty(_encodingsAccepted)) return;
_encodingsAccepted = _encodingsAccepted.ToLowerInvariant();
var _response = filterContext.HttpContext.Response;
if(_response.Filter == null) return;
if (_encodingsAccepted.Contains("deflate"))
{
_response.AppendHeader("Content-encoding", "deflate");
_response.Filter = new DeflateStream(_response.Filter, CompressionMode.Compress);
}
else if (_encodingsAccepted.Contains("gzip"))
{
_response.AppendHeader("Content-encoding", "gzip");
_response.Filter = new GZipStream(_response.Filter, CompressionMode.Compress);
}
}
}
--- and add in global.asax.cs
GlobalFilters.Filters.Add(new CompressAttribute());

How do I define custom web.config sections with potential child elements and attributes for the properties?

The web applications I develop often require co-dependent configuration settings and there are also settings that have to change as we move between each of our environments.
All our settings are currently simple key-value pairs but it would be useful to create custom config sections so that it is obvious when two values need to change together or when the settings need to change for an environment.
What's the best way to create custom config sections and are there any special considerations to make when retrieving the values?
Using attributes, child config sections and constraints
There is also the possibility to use attributes which automatically takes care of the plumbing, as well as providing the ability to easily add constraints.
I here present an example from code I use myself in one of my sites. With a constraint I dictate the maximum amount of disk space any one user is allowed to use.
MailCenterConfiguration.cs:
namespace Ani {
public sealed class MailCenterConfiguration : ConfigurationSection
{
[ConfigurationProperty("userDiskSpace", IsRequired = true)]
[IntegerValidator(MinValue = 0, MaxValue = 1000000)]
public int UserDiskSpace
{
get { return (int)base["userDiskSpace"]; }
set { base["userDiskSpace"] = value; }
}
}
}
This is set up in web.config like so
<configSections>
<!-- Mailcenter configuration file -->
<section name="mailCenter" type="Ani.MailCenterConfiguration" requirePermission="false"/>
</configSections>
...
<mailCenter userDiskSpace="25000">
<mail
host="my.hostname.com"
port="366" />
</mailCenter>
Child elements
The child xml element mail is created in the same .cs file as the one above. Here I've added constraints on the port. If the port is assigned a value not in this range the runtime will complain when the config is loaded.
MailCenterConfiguration.cs:
public sealed class MailCenterConfiguration : ConfigurationSection
{
[ConfigurationProperty("mail", IsRequired=true)]
public MailElement Mail
{
get { return (MailElement)base["mail"]; }
set { base["mail"] = value; }
}
public class MailElement : ConfigurationElement
{
[ConfigurationProperty("host", IsRequired = true)]
public string Host
{
get { return (string)base["host"]; }
set { base["host"] = value; }
}
[ConfigurationProperty("port", IsRequired = true)]
[IntegerValidator(MinValue = 0, MaxValue = 65535)]
public int Port
{
get { return (int)base["port"]; }
set { base["port"] = value; }
}
Use
To then use it practically in code, all you have to do is instantiate the MailCenterConfigurationObject, this will automatically read the relevant sections from web.config.
MailCenterConfiguration.cs
private static MailCenterConfiguration instance = null;
public static MailCenterConfiguration Instance
{
get
{
if (instance == null)
{
instance = (MailCenterConfiguration)WebConfigurationManager.GetSection("mailCenter");
}
return instance;
}
}
AnotherFile.cs
public void SendMail()
{
MailCenterConfiguration conf = MailCenterConfiguration.Instance;
SmtpClient smtpClient = new SmtpClient(conf.Mail.Host, conf.Mail.Port);
}
Check for validity
I previously mentioned that the runtime will complain when the configuration is loaded and some data does not comply to the rules you have set up (e.g. in MailCenterConfiguration.cs). I tend to want to know these things as soon as possible when my site fires up. One way to solve this is load the configuration in _Global.asax.cx.Application_Start_ , if the configuration is invalid you will be notified of this with the means of an exception. Your site won't start and instead you will be presented detailed exception information in the Yellow screen of death.
Global.asax.cs
protected void Application_ Start(object sender, EventArgs e)
{
MailCenterConfiguration.Instance;
}
Quick'n Dirty:
First create your ConfigurationSection and ConfigurationElement classes:
public class MyStuffSection : ConfigurationSection
{
ConfigurationProperty _MyStuffElement;
public MyStuffSection()
{
_MyStuffElement = new ConfigurationProperty("MyStuff", typeof(MyStuffElement), null);
this.Properties.Add(_MyStuffElement);
}
public MyStuffElement MyStuff
{
get
{
return this[_MyStuffElement] as MyStuffElement;
}
}
}
public class MyStuffElement : ConfigurationElement
{
ConfigurationProperty _SomeStuff;
public MyStuffElement()
{
_SomeStuff = new ConfigurationProperty("SomeStuff", typeof(string), "<UNDEFINED>");
this.Properties.Add(_SomeStuff);
}
public string SomeStuff
{
get
{
return (String)this[_SomeStuff];
}
}
}
Then let the framework know how to handle your configuration classes in web.config:
<configuration>
<configSections>
<section name="MyStuffSection" type="MyWeb.Configuration.MyStuffSection" />
</configSections>
...
And actually add your own section below:
<MyStuffSection>
<MyStuff SomeStuff="Hey There!" />
</MyStuffSection>
Then you can use it in your code thus:
MyWeb.Configuration.MyStuffSection configSection = ConfigurationManager.GetSection("MyStuffSection") as MyWeb.Configuration.MyStuffSection;
if (configSection != null && configSection.MyStuff != null)
{
Response.Write(configSection.MyStuff.SomeStuff);
}
The custom configuration are quite handy thing and often applications end up with a demand for an extendable solution.
For .NET 1.1 please refer the article https://web.archive.org/web/20211027113329/http://aspnet.4guysfromrolla.com/articles/020707-1.aspx
Note: The above solution works for .NET 2.0 as well.
For .NET 2.0 specific solution, please refer the article https://web.archive.org/web/20210802144254/https://aspnet.4guysfromrolla.com/articles/032807-1.aspx
You can accomplish this with Section Handlers. There is a basic overview of how to write one at http://www.codeproject.com/KB/aspnet/ConfigSections.aspx however it refers to app.config which would be pretty much the same as writing one for use in web.config. This will allow you to essentially have your own XML tree in the config file and do some more advanced configuration.
The most simple method, which I found, is using appSettings section.
Add to Web.config the following:
<appSettings>
<add key="MyProp" value="MyVal"/>
</appSettings>
Access from your code
NameValueCollection appSettings = ConfigurationManager.AppSettings;
string myPropVal = appSettings["MyProp"];

Resources