Sitecore and caching control - asp.net-web-api

I am working on this Sitecore project and am using WebApi to perform some service calls. My methods are decorated with CacheOutput information like this:
[HttpGet]
[CacheOutput(ClientTimeSpan = 3600, ServerTimeSpan = 3600)]
I am testing these calls using DHC app on Google Chrome. I am sure that the ClientTimespan is set correctly but the response headers that i am getting back are not what i am expecting. I would expect that Cache-Control would have a max-age of 1hour as set by the ClientTimespan attribute but instead it is set to private.
I have been debugging everything possible and t turns out that Sitecore may be intercepting the response and setting this header value to private. I have also added the service url to the sitecore ignored url prefixes configuration but no help .
Does anyone have an idea how I can make Sitecore NOT change my Cache-Control headers?

This is default MVC behaviour and not directly related to Sitecore / Web API.
You can create a custom attribute that sets the Cache-Control header:
public class CacheControl : System.Web.Http.Filters.ActionFilterAttribute
{
public int MaxAge { get; set; }
public CacheControl()
{
MaxAge = 3600;
}
public override void OnActionExecuted(HttpActionExecutedContext context)
{
context.Response.Headers.CacheControl = new CacheControlHeaderValue()
{
Public = true,
MaxAge = TimeSpan.FromSeconds(MaxAge)
};
base.OnActionExecuted(context);
}
}
Which enables you to add the [CacheControl(MaxAge = n)] attribute to your methods.
Code taken from: Setting HTTP cache control headers in WebAPI (answer #2)
Or you can apply it globally throughout the application, as explained here: http://juristr.com/blog/2012/10/output-caching-in-aspnet-mvc/

Related

Set JSON CamelCase per Web API request

Web API uses the Json.Net formatter to serialise its JSON responses which allows you to customise the format of the generated JSON very easily for the entire application at startup using:
config.Formatters.JsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
This allows you resolve the issues between C# syntax preferring PascalCase and javascript based clients preferring camelCase. However setting this globally on the API without taking into consideration who the client request is actually coming from seems to assume that an API will only have 1 type of client and whatever you set for your API is just the way it has to be.
With multiple client types for my API's (javascript, iOS, Android, C#), I'm looking for a way to set the Json.Net SerializerSettings per request such that the client can request their preferred format by some means (perhaps a custom header or queryString param) to override the default.
What would be the best way to set per-request Json.Net SerializerSettings in Web API?
With a bit of help from Rick Strahl's blog post on creating a JSONP media type formatter, I have come up with a solution that allows the API to dynamically switch from camelCase to PascalCase based on the client request.
Create a MediaTypeFormatter that derives from the default JsonMediaTypeFormatter and overrides the GetPerRequestFormatterInstance method. This is where you can implement your logic to set your serializer settings based on the request.
public class JsonPropertyCaseFormatter : JsonMediaTypeFormatter
{
private readonly JsonSerializerSettings globalSerializerSettings;
public JsonPropertyCaseFormatter(JsonSerializerSettings globalSerializerSettings)
{
this.globalSerializerSettings = globalSerializerSettings;
SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/json"));
SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/javascript"));
}
public override MediaTypeFormatter GetPerRequestFormatterInstance(
Type type,
HttpRequestMessage request,
MediaTypeHeaderValue mediaType)
{
var formatter = new JsonMediaTypeFormatter
{
SerializerSettings = globalSerializerSettings
};
IEnumerable<string> values;
var result = request.Headers.TryGetValues("X-JsonResponseCase", out values)
? values.First()
: "Pascal";
formatter.SerializerSettings.ContractResolver =
result.Equals("Camel", StringComparison.InvariantCultureIgnoreCase)
? new CamelCasePropertyNamesContractResolver()
: new DefaultContractResolver();
return formatter;
}
}
Note that I take a JsonSerializerSettings argument as a constructor param so that we can continue to use WebApiConfig to set up whatever other json settings we want to use and have them still applied here.
To then register this formatter, in your WebApiConfig:
config.Formatters.JsonFormatter.SerializerSettings.Converters.Add(new StringEnumConverter());
config.Formatters.JsonFormatter.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
config.Formatters.JsonFormatter.SerializerSettings.DateTimeZoneHandling = DateTimeZoneHandling.Local;
config.Formatters.Insert(0,
new JsonPropertyCaseFormatter(config.Formatters.JsonFormatter.SerializerSettings));
Now requests that have a header value of X-JsonResponseCase: Camel will receive camel case property names in the response. Obviously you could change that logic to use any header or query string param you like.

Handling batched sendgrid events using the asp.net web api

I'm attempting to use an asp.net web api application to handle batched SendGrid events and I've run into a stumbling block due to the way SendGrid handles the content type header of the post it sends.
From their documentation:
Batched event POSTs have a content-type header of application/json,
and contain exactly one JSON string per line, with each line
representing one event. Please note that currently the POST headers
define this post as application/json, though it’s not; each line is a
valid JSON string, but the overall POST body is not.
So, given a controller:
public class SendGridController : ApiController
{
// POST api/values
public void Post([FromBody]string value)
{
// do something with value
}
}
Making a post to it as SendGrid does will result in "value" being null.
string URI = "http://localhost:3018/api/sendgrid/";
string myParameters =
#"={""email"":""foo#bar.com"",""timestamp"":1322000095,""user_id"":""6"",""event"":""bounced""}
{""email"":""foo#bar.com"",""timestamp"":1322000096,""user_id"":""9"",""event"":""bounced""}";
using (var wc = new System.Net.WebClient())
{
wc.Headers[System.Net.HttpRequestHeader.ContentType] = "application/json"; // I work fine if "application/x-www-form-urlencoded" is used.
wc.UploadString(URI, myParameters);
}
If I change the content type in my client example to "application/x-www-form-urlencoded", everything works as expected.
Is there an easy way for me to override this convention such that I can handle the badly formed "json" that sendgrid provides as a string in my controller method?
Ok, I finally figured it out. The trick was to remove the "value" param and work with the request object directly.
So something like:
public class SendGridController : ApiController
{
// POST api/values
public void Post()
{
var value = Request.Content.ReadAsStringAsync().Result;
// do something with value
}
}

Returning 'other than HTML' formatted errors for AJAX calls to Web API

Investigating the Web API as part of an MVC 4 project as an alternative way to provide an AJAX-based API. I've extended AuthorizeAttribute for the MVC controllers such that, if an AJAX request is detected, a JSON-formatted error is returned. The Web API returns errors as HTML. Here's the AuthorizeAttribute that I'm using with the MVC controllers:
public class AuthorizeAttribute: System.Web.Mvc.AuthorizeAttribute
{
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
base.HandleUnauthorizedRequest(filterContext);
filterContext.Result = new RedirectToRouteResult(
new RouteValueDictionary
{
{ "area", "" },
{ "controller", "Error" },
{ "action", ( filterContext.HttpContext.Request.IsAjaxRequest() ? "JsonHttp" : "Http" ) },
{ "id", "401" },
});
}
}
How could I reproduce this to provide equivalent functionality for the Web API?
I realize that I need to extend System.Web.Http.AuthorizeAttribute instead of System.Web.Mvc.AuthorizeAttribute but this uses an HttpActionContext rather than an AuthorizationContext and so I'm stuck by my limited knowledge of the Web API and the seemingly incomplete documentation on MSDN.
Am I even correct in thinking that this would be the correct approach?
Would appreciate any guidance.
To get the equivalent functionality in a Web API filter you can set the HttpActionContext.Response property to an instance of HttpResponseMessage that has the right redirect status code and location header:
protected override void HandleUnauthorizedRequest(HttpActionContext actionContext) {
var response = new HttpResponseMessage(HttpStatusCode.Redirect);
response.Headers.Location = new Uri("my new location");
actionContext.Response = response;
}
I would very much go with Marcin's answer - at the end of the day, he has written the code!
All I would add is that as Marcin is saying, your best bet is to have a dedicated controller to return the errors as appropriate - rather than setting the response code 401 with JSON content in the attribute.
The main reason is that Web API does the content-negotiation for you and if you want to do it yourself (see if you need to serve JSON or HTML) you lose all that functionality.

How do I set a response cookie on HttpReponseMessage?

I would like to create a demo login service in web api and need to set a cookie on the response. How do I do that? Or are there any better way to do authorization?
Add a reference to System.Net.Http.Formatting.dll and use the AddCookies extension method defined in the HttpResponseHeadersExtensions class.
Here is a blog post describing this approach, and the MSDN topic.
If that assembly isn't an option for you, here's my older answer from before this was an option:
Older answer follows
I prefer an approach that stays within the realm of HttpResponseMessage without bleeding into the HttpContext which isn't as unit testable and does not always apply depending on the host:
/// <summary>
/// Adds a Set-Cookie HTTP header for the specified cookie.
/// WARNING: support for cookie properties is currently VERY LIMITED.
/// </summary>
internal static void SetCookie(this HttpResponseHeaders headers, Cookie cookie) {
Requires.NotNull(headers, "headers");
Requires.NotNull(cookie, "cookie");
var cookieBuilder = new StringBuilder(HttpUtility.UrlEncode(cookie.Name) + "=" + HttpUtility.UrlEncode(cookie.Value));
if (cookie.HttpOnly) {
cookieBuilder.Append("; HttpOnly");
}
if (cookie.Secure) {
cookieBuilder.Append("; Secure");
}
headers.Add("Set-Cookie", cookieBuilder.ToString());
}
Then you can include a cookie in the response like this:
HttpResponseMessage response;
response.Headers.SetCookie(new Cookie("name", "value"));
You can add the cookie to the HttpContext.Current.Response.Cookies collection.
var cookie = new HttpCookie("MyCookie", DateTime.Now.ToLongTimeString());
HttpContext.Current.Response.Cookies.Add(cookie);

Handling URL's with AppHarbor without Modifying All My Controllers

I'm trying to host an MVC 3 application (FunnelWeb) on AppHarbor. For a reason that's still not clear to me, when my route is only a Controller+Action (e.g. mysite/admin is Admin+Index and mysite/login is Admin+login) everything works fine, but if I have anything else in the route (e.g. a variable like {*page}) my URL will be mysite:12345/mypage (where 12345 is a port number assigned by AppHarbor and mypage is the name of the page I'm requesting). This makes the request fail as the port 12345 is not publicly exposed.
AppHarbor uses load balancing to distribute the request between multiple IIS's. This is their way of doing stuff and this is why internally the requests are routed to some non-standard ports. I don't have a problem with that, but I have problem with MVC that tries to route me to that internal URL.
I'm not pointing fingers here; it's nobody's fault :) so let's move to the question:
Why there is a difference between requesting a route with Controller+Action only and requesting a route with a variable like {*page}? Be technical please :)
Here is an example of how to handle requests in AppHarbor, however, it seems that it requires me to modify all my controllers (OMG). Is there any way to implement this without modifying my controllers?
Any other suggestions are welcomed :)
Thanks in advance.
UPDATE: Coincidentally, the behaviour that I observed matches the conclusion that I reached. However, the issue has nothing to do with ASP.Net MVC routing. The short story is, FunnelWeb forces lowercase URL's, so, whenever it receives a request to a resource it convert it to lowercase, if needed, and issue a 301 response. The problem is, when creating the URL for the 301 response, the request URL (absolute URL) is now the URL used when the request made from the load balancer to IIS and not the one made from the client; hence, the request fails.
This is known issue with FunnelWeb url generation on AppHarbor. When using standard MVC methods to generate relative URLs, this is not a problem. AppHarbor has a short guide and sample on how the generate public URLs in the knowledge base.
It's possible that the following is now all you need:
<appSettings>
<!-- AppHarbor Setting to stop AppHb load balancer internal port numbers from showing up in URLs-->
<add key="aspnet:UseHostHeaderForRequestUrl" value="true" />
</appSettings>
This is noted as an update on AppHarbor's support page at http://support.appharbor.com/kb/getting-started/workaround-for-generating-absolute-urls-without-port-number
MSDN says the following about UseHostHeaderForRequestUrl:
aspnet:UseHostHeaderForRequestUrl - If this value attribute is false [default], the Url property is dynamically built from the host, port, and path provided by the web server. If this value attribute is true, the Url property is dynamically built by using the host and port provided by the incoming "Host" header and the path provided by the web server.
There is a way, but it requires a couple of classes.
When ASP.NET MVC registers a route, it defines a route handler. This route handler returns a HTTP handler that handles the request. If you use a custom route handler that returns a custom HTTP handler, you can rewrite the HTTP context by using a couple decorator classes.
Start by creating a HttpContextProxy and HttpRequestProxy that derives from the base classes and wraps all methods and properties to an inner instance. I've made the hard work available.
Next create the decorators, first the HTTP context decorator:
using System.Web;
public class HttpContextDecorator : HttpContextProxy
{
public HttpContextDecorator(HttpContextBase innerHttpContext)
: base(innerHttpContext)
{
}
public override HttpRequestBase Request
{
get
{
return new HttpRequestDecorator(base.Request);
}
}
}
The HTTP request decorator:
using System;
using System.Web;
public class HttpRequestDecorator : HttpRequestProxy
{
public HttpRequestDecorator(HttpRequestBase innerHttpRequest)
: base(innerHttpRequest)
{
}
public override bool IsSecureConnection
{
get
{
return string.Equals(Headers["X-Forwarded-Proto"], "https", StringComparison.OrdinalIgnoreCase);
}
}
public override Uri Url
{
get
{
var url = base.Url;
var urlBuilder = new UriBuilder(url);
if (IsSecureConnection)
{
urlBuilder.Port = 443;
urlBuilder.Scheme = "https";
}
else
{
urlBuilder.Port = 80;
}
return urlBuilder.Uri;
}
}
public override string UserHostAddress
{
get
{
const string forwardedForHeader = "HTTP_X_FORWARDED_FOR";
var forwardedFor = ServerVariables[forwardedForHeader];
if (forwardedFor != null)
{
return forwardedFor;
}
return base.UserHostAddress;
}
}
}
As mentioned, you also need to override the MVC classes - here the HTTP handler:
using System;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
public class CustomMvcHandler : MvcHandler
{
public CustomMvcHandler(RequestContext requestContext)
: base(requestContext)
{
requestContext.HttpContext = new HttpContextDecorator(requestContext.HttpContext);
}
protected override IAsyncResult BeginProcessRequest(HttpContextBase httpContext, AsyncCallback callback, object state)
{
httpContext = new HttpContextDecorator(httpContext);
return base.BeginProcessRequest(httpContext, callback, state);
}
protected override void ProcessRequest(HttpContextBase httpContext)
{
httpContext = new HttpContextDecorator(httpContext);
base.ProcessRequest(httpContext);
}
}
Then the route handler:
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
public class CustomMvcRouteHandler : MvcRouteHandler
{
protected override IHttpHandler GetHttpHandler(RequestContext requestContext)
{
return new CustomMvcHandler(requestContext);
}
}
Finally, you'll need to replace the associated handler for all registered routes (or map them properly from the beginning):
var routes = RouteTable.Routes.OfType<Route>().Where(x => x.RouteHandler is MvcRouteHandler);
foreach (var route in routes)
{
route.RouteHandler = new CustomMvcRouteHandler();
}

Resources