The web socket protocol is currently the most popular one for pushing data to browsers, however it's not the only one. The Server-Sent Events (SSE) is a very interesting alternative which can provide better performance for specific use cases.

What is SSE

The Server-Sent Events is a unidirectional (server to browser) protocol for streaming events. The protocol delivers text-based messages over a long-lived HTTP connection. It also has built in support for events identification, auto-reconnection (with tracking of last received event) and notifications through DOM events. Its biggest advantage is high performance as events can be pushed immediately with minimum overhead (there is an already open HTTP connection waiting, which thanks to text-based messages can utilize HTTP compression mechanisms). A considerable limitation is general lack of support for binary streaming (but JSON or XML will work nicely).

Why use SSE

In general web sockets can do everything that Server-Sent Events can and more as they provide bidirectional communication. There is also broader browser support (93%) for web sockets. So why would one consider the SSE (assuming bidirectional isn't a requirement, or the client to server communication is occasional and can be done in a REST style)? The fact that it runs over a long-lived HTTP connection is the game changer here. In case of web sockets we are talking about custom TCP based protocol which needs to be supported by the server and entire infrastructure (proxies, firewalls etc.), any legacy element along the way may cause an issue. There are no such issues for SSE, anything that speaks HTTP will speak SSE and the aspect of browser support (87%) can be addressed with polyfills. Taking into consideration this and notably lower latency, Server-Sent Events is a very compelling choice for scenarios like stock ticker or notifications.

Bringing SSE to ASP.NET Core

One of key concepts behind ASP.NET Core is modular HTTP request pipeline which can be extended through middlewares, so I'm going to create one for Server-Sent Events. But first some prerequisites are needed.

The middleware will require an abstraction for representing a client. As previously stated SSE runs over a long-lived HTTP connection, which means that channel for communication with client is HttpResponse instance. The abstraction will simply wrap around it.

public class ServerSentEventsClient
{
    private readonly HttpResponse _response;

    internal ServerSentEventsClient(HttpResponse response)
    {
        _response = response;
    }
}

Also there is a need for some kind of service which will serve as bridge between the middleware and the rest of application. Its primary goal will be managing the collection of connected clients. Below is a simple implementation based on ConcurrentDictionary.

public class ServerSentEventsService
{
    private readonly ConcurrentDictionary<Guid, ServerSentEventsClient> _clients = new ConcurrentDictionary<Guid, ServerSentEventsClient>();

    internal Guid AddClient(ServerSentEventsClient client)
    {
        Guid clientId = Guid.NewGuid();

        _clients.TryAdd(clientId, client);

        return clientId;
    }

    internal void RemoveClient(Guid clientId)
    {
        ServerSentEventsClient client;

        _clients.TryRemove(clientId, out client);
    }
}

With those elements in place the middleware can be created. It will have two responsibilities: establishing the connection and cleaning up when client closes the connection.

In order to establish the connection the middleware should inspect the Accept header of incoming request, if its value is text/event-stream it means that client is attempting to open SSE connection. In such case the Content-Type response header should be set to text/event-stream, headers should be send and connection needs to be kept open.

The clean up part requires detecting that client has closed the connection. This can be done by waiting on CancellationToken available through HttpContext.RequestAborted property. An important thing to note here is that closed connection can only be detected when sending new event. This limitation is often being solved by sending dedicated heartbeat event which client should simply ignore.

public class ServerSentEventsMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ServerSentEventsService _serverSentEventsService;

    public ServerSentEventsMiddleware(RequestDelegate next, ServerSentEventsService serverSentEventsService)
    {
        _next = next;
        _serverSentEventsService = serverSentEventsService;
    }

    public Task Invoke(HttpContext context)
    {
        if (context.Request.Headers["Accept"] == "text/event-stream")
        {
            context.Response.ContentType = "text/event-stream";
            context.Response.Body.Flush();

            ServerSentEventsClient client = new ServerSentEventsClient(context.Response);
            Guid clientId = _serverSentEventsService.AddClient(client);

            context.RequestAborted.WaitHandle.WaitOne();

            _serverSentEventsService.RemoveClient(clientId);

            return Task.FromResult(true);
        }
        else
        {
            return _next(context);
        }
    }
}

With the connection management part in place the sending part can be added. The message format in SSE is a very simple one. The basic building blocks of every message are fields which general format looks like this: <FieldName>: <FieldValue>\n. There are three types of fields (well in fact four as there is an additional one for controlling client reconnect interval):

  • id - The identifier of the event.
  • event - The type of the event.
  • data - A single line of data (entire payload of message is represented by one or more adjacent data fields).

Only the data field is required and the entire message is being terminated by additional new line (\n).

public class ServerSentEvent
{
    public string Id { get; set; }

    public string Type { get; set; }

    public IList<string> Data { get; set; }
}

internal static class ServerSentEventsHelper
{
    internal static async Task WriteSseEventAsync(this HttpResponse response, ServerSentEvent serverSentEvent)
    {
        if (!String.IsNullOrWhiteSpace(serverSentEvent.Id))
            await response.WriteSseEventFieldAsync("id", serverSentEvent.Id);

        if (!String.IsNullOrWhiteSpace(serverSentEvent.Type))
            await response.WriteSseEventFieldAsync("event", serverSentEvent.Type);

        if (serverSentEvent.Data != null)
        {
            foreach(string data in serverSentEvent.Data)
                await response.WriteSseEventFieldAsync("data", data);
        }

        await response.WriteSseEventBoundaryAsync();
        response.Body.Flush();
    }

    private static Task WriteSseEventFieldAsync(this HttpResponse response, string field, string data)
    {
        return response.WriteAsync($"{field}: {data}\n");
    }

    private static Task WriteSseEventBoundaryAsync(this HttpResponse response)
    {
        return response.WriteAsync("\n");
    }
}

The above helper can be used in order to expose the send method on the client abstraction.

public class ServerSentEventsClient
{
    ...

    public Task SendEventAsync(ServerSentEvent serverSentEvent)
    {
        return _response.WriteSseEventAsync(serverSentEvent);
    }
}

Last step is exposing send method at the service level - it should perform send for all connected clients.

public class ServerSentEventsService
{
    ...

    public Task SendEventAsync(ServerSentEvent serverSentEvent)
    {
        List<Task> clientsTasks = new List<Task>();
        foreach (ServerSentEventsClient client in _clients.Values)
        {
            clientsTasks.Add(client.SendEventAsync(serverSentEvent));
        }

        return Task.WhenAll(clientsTasks);
    }
}

We can say that this gives us what project managers like to call minimum viable product. After extending pipeline with the middleware and adding service to services collection (as singleton) we can send events from any desired place in the application. In case of a need for exposing more than one endpoint a derived services can be created, added to services collection and passed to the respective middlewares during initialization.

I've made an extended version (support for reconnect interval, extensibility point for auto-reconnect and extensions for service and middleware registration) available on GitHub and as a NuGet package.

SSE at work

I've also created a demo application which utilizes the above components, it can be found here. The application exposes two SSE endpoints:

  • /see-heartbeat which can be "listened" by navigating to /sse-heartbeat-receiver.html. It sends an event every 5s and is implemented through an ugly background thread.
  • /sse-notifications which can be "listened" by navigating to /notifications/sse-notifications-receiver. Sending events to this endpoint can be done by navigating to /notifications/sse-notifications-sender.

It might be a good starting point for those who would like to play with what I've shown here.

This is one of those "I had to explain this couple times already so next time I want something I can redirect people to" kind of post.

What I want to write about is difference in behavior between using new() and DbSet.Create() for instantiating new entities. In order to do this I've created a very simple model and context.

public class Planet
{
    public virtual int Id { get; set; }

    public virtual string Name { get; set; }

    ...

    public virtual ICollection Natives { get; set; }
}

public class Character
{
    public virtual int Id { get; set; }

    public virtual string Name { get; set; }

    ...

    public virtual int HomeworldId { get; set; }

    public virtual Planet Homeworld { get; set; }
}

public interface IStarWarsContext
{
    DbSet Planets { get; set; }

    DbSet Characters { get; set; }

    int SaveChanges();
}

public class StarWarsContext : DbContext, IStarWarsContext
{
    public DbSet Planets { get; set; }

    public DbSet Characters { get; set; }
}

I've also created a very simple view which lists Charactes already present in database and allows for adding new ones.

@using (Html.BeginForm())
{
    <fieldset>
        <legend>New StarWars Character</legend>
        <div>
            @Html.LabelFor(m => m.Name)
        </div>
        <div>
            @Html.TextBoxFor(m => m.Name)
        </div>
        <div>
            @Html.LabelFor(m => m.Homeworld)
        </div>
        <div>
            @Html.DropDownListFor(m => m.Homeworld, Model.Planets)
        </div>
        ...
        <div>
            <input type="submit" value="Add" />
        </div>
    </fieldset>
}
<table>
    <thead>
        <tr>
            <th>Name</th>
            <th>Homeworld</th>
            ...
        </tr>
    </thead>
    <tbody>
        @foreach (Character character in Model.Characters)
        {
            <tr>
                <td>@character.Name</td>
                <td>@character.Homeworld.Name</td>
                ...
            </tr>
        }
    </tbody>
</table>

The view is powered by following ViewModel and controller.

public class StarWarsViewModel
{
    public string Name { get; set; }

    ...

    public int Homeworld { get; set; }

    public IEnumerable Planets { get; set; }

    public IReadOnlyList Characters { get; set; }
}

public class StarWarsController : Controller
{
    private IStarWarsContext _startWarsContext;

    public StarWarsController(IStarWarsContext startWarsContext)
    {
        _startWarsContext = startWarsContext;
    }

    [HttpGet]
    public ActionResult Index()
    {
        return View(GetViewModel());
    }

    [HttpPost]
    public ActionResult Index(StarWarsViewModel viewModel)
    {
        AddCharacter(viewModel);

        return View(GetViewModel());
    }

    private StarWarsViewModel GetViewModel()
    {
        return new StarWarsViewModel
        {
            Planets = _startWarsContext.Planets
                .Select(p => new { p.Id, p.Name })
                .ToList()
                .Select(p => new SelectListItem { Value = p.Id.ToString(), Text = p.Name }),
            Characters = _startWarsContext.Characters.ToList()
        };
    }

    private void AddCharacter(StarWarsViewModel viewModel)
    {
        throw new NotImplementedException();
    }
}

The AddCharacter method is the point of interest here. There are two ways to implement it and they will result in a different behavior.

Creating entity with new()

Following first Entity Framework tutorial which pops up on Google will result in code similar to the one below.

private void AddCharacter(StarWarsViewModel viewModel)
{
    Character character = new Character();
    character.Name = viewModel.Name;
    ...
    character.HomeworldId = viewModel.Homeworld;

    _startWarsContext.Characters.Add(character);
    _startWarsContext.SaveChanges();
}

Running this code and adding a new Character will result in NullReferenceException coming from the part of view which generates the table (to be more exact from @character.Homeworld.Name). The reason for the exception is the fact that Entity Framework needs to lazy load the Planet entity but the just added Character entity is not a dynamic proxy so lazy loading doesn't work for it. Only Entity Framework can create a dynamic proxy, but in this scenario there is no way for it to do it - the caller already owns the reference to the entity and it cannot be changed to a different class.

Creating entity with DbSet.Create()

In order to be able create new entities as proper dynamic proxies the DbSet class provides Create method. This method returns new dynamic proxy instance which isn't added or attached to the context. To use it only a single line of code needs to be changed.

private void AddCharacter(StarWarsViewModel viewModel)
{
    Character character = _startWarsContext.Characters.Create();
    character.Name = viewModel.Name;
    ...
    character.HomeworldId = viewModel.Homeworld;

    _startWarsContext.Characters.Add(character);
    _startWarsContext.SaveChanges();
}

After this simple change the code works as expected, related entities are being lazy loaded when needed.

The takeaway

The sample above is built in a way which highlights the difference between new() and DbSet.Create() (it even has an N+1 selects hiding in there for the sake of simplicity). In real life this rarely causes an issue as there is couple other things which can impact the behavior (related entity already present in context or usage of Include() method). But when this causes an issue it's always unexpected and I've seen smart people wrapping they heads around what is going on. It is important to understand the difference and use both mechanisms appropriately (sometimes lack of lazy loading maybe desired).

In my previous post I didn't write much on how Server Push works with caching beside the fact that it does. This can be easily verified by looking at Network tab in Chrome Developer Tools while making subsequent requests to the sample action.

Chrome Developer Tools Network Tab - Warm Server Push

But this isn't the full picture. The chrome://net-internals/#http2 tab can be used in order to get more details. First screenshot below represents the cold scenario while second represents the warm one.

Chrome Net Internals - Cold Server Push

Chrome Net Internals - Warm Server Push

The highlighted column informs that there were resources pushed from server which haven't been used. The "View live HTTP/2 sessions" provides even more detailed debug information, down to the single HTTP/2 frames. Going through those reveals that HTTP2_SESSION_RECV_PUSH_PROMISE, HTTP2_SESSION_RECV_HEADERS and HTTP2_SESSION_RECV_DATA frames are present for both pushed resources. This shouldn't affect latency as the stream carrying HTML is supposed to have the highest priority, which means that server should start sending (and browser receiving) the data as soon as something is available (even if sending/receiving for pushed resources is not finished). On the other hand the bandwidth is being lost and loosing bandwidth often means loosing money. As nobody wants to be loosing money, this is something that needs to be resolved and Cache Digest aims at doing exactly that.

How Cache Digest solves the issue

The Cache Digest specification proposal (in its current shape) uses Golomb-Rice coded Bloom filter in order to provide server with information about the resources which are already contained by client cache. The algorithm uses URLs and ETags for hashing which ensures proper identification (and with correct ETags also freshness) of the resources. The server can query the digest and if the resource which was intended for push is present, the push can be skipped.

There is no native browser support for Cache Digest yet (at least that I know of), but there are two ways in which HTTP/2 servers (http2server, h2o) and third party libraries are trying to provide it:

  • On the client side with usage of Service Workers and Cache API
  • On server side by providing a cookie-based fallback

Implementing Cache Digest with Service Worker and Cache API

One of third party libraries providing Cache Digest support on client side is cache-digest-immutable. It combines Cache Digest with HTTP Immutable Responses proposal in order to avoid overly aggressive caching. This means that only resources with cache-control header containing immutable extension will be considered. In order to add such extension to pushed resource the location and customHeaders elements of web.config can be used.

<?xml version="1.0"?>
<configuration>
  ...
  <location path="content/css/normalize.css">
    <system.webServer>
      <httpProtocol>
        <customHeaders>
          <add name="Cache-Control" value="max-age=31536000, immutable" />
        </customHeaders>
      </httpProtocol>
    </system.webServer>
  </location>
  <location path="content/css/site.css">
    <system.webServer>
      <httpProtocol>
        <customHeaders>
          <add name="Cache-Control" value="max-age=31536000, immutable" />
        </customHeaders>
      </httpProtocol>
    </system.webServer>
  </location>
  ...
</configuration>

Quick look at responses after running the demo application shows that headers are being added properly. Now the Service Worker can be set up accordingly to the instructions. From this point the subsequent requests to the demo application will contain cache-digest header. In case of the demo application its value is CdR2gA; complete. The part before the ; is the digest (Base64 encoded) while the rest contains flags.

The flags can be parsed easily by checking if corresponding constants are present in the header.

var compareInfo = CultureInfo.InvariantCulture.CompareInfo;
var compareOptions = CompareOptions.IgnoreCase;

bool reset = (compareInfo.IndexOf(cacheDigestHeaderValue, "RESET", compareOptions) >= 0);
bool complete = (compareInfo.IndexOf(cacheDigestHeaderValue, "COMPLETE", compareOptions) >= 0);
bool validators = (compareInfo.IndexOf(cacheDigestHeaderValue, "VALIDATORS", compareOptions) >= 0);
bool stale = (compareInfo.IndexOf(cacheDigestHeaderValue, "STALE", compareOptions) >= 0);

My first attempt to decode the digest has failed miserably with an awful exception. The reason is that the padding is being truncated and it must be added back.

int separatorIndex = cacheDigestHeaderValue.IndexOf(DigestValueSeparator);
string digestValueBase64 = cacheDigestHeaderValue.Substring(0, separatorIndex);

int neededPadding = (digestValueBase64.Length % 4);
if (neededPadding > 0)
{
    digestValueBase64 += new string('=', 4 - neededPadding);
}
byte[] digestValue = Convert.FromBase64String(digestValueBase64);

Now the digest query algorithm can be implemented. The algorithm assumes going through digest value bit by bit so first step was changing array of bytes into array of bools.

bool[] bitArray = new bool[digestValue.Length * 8];
int bitArrayIndex = bitArray.Length - 1;

for (int byteIndex = digestValue.Length - 1; byteIndex  >= 0; byteIndex --)
{
    byte digestValueByte = digestValue[byteIndex];
    for (int byteBitIndex = 0; byteBitIndex < 8; byteBitIndex++)
    {
        bitArray[bitArrayIndex--] = ((digestValueByte % 2 == 0) ? false : true);
        digestValueByte = (byte)(digestValueByte >> 1);
    }
}

The algorithm also requires reading a series of bits as integer in few places, below method takes care of that.

private static uint ReadUInt32FromBitArray(bool[] bitArray, int starIndex, int length)
{
    uint result = 0;

    for (int bitIndex = starIndex; bitIndex < (starIndex + length); bitIndex++)
    {
        result <<= 1;
        if (bitArray[bitIndex])
        {
            result |= 1;
        }
    }

    return result;
}

With this method count of URLs and probability can be easily retrieved.

// Read the first 5 bits of digest-value as an integer;
// let N be two raised to the power of that value.
int count = (int)Math.Pow(2, ReadUInt32FromBitArray(digestValueBitArray, 0, 5));

// Read the next 5 bits of digest-value as an integer;
// let P be two raised to the power of that value.
int log2Probability = (int)ReadUInt32FromBitArray(digestValueBitArray, 5, 5);
uint probability = (uint)Math.Pow(2, log2Probability);

The part which reads hashes requires keeping in mind that there might be additional 0 bits at the end, so there is a risk of going over the array without proper checks. I've decided to keep the hashes in HashSet for quicker lookups later.

HashSet hashes = new HashSet();

// Let C be -1.
long hash = -1;

int hashesBitIndex = 10;
while (hashesBitIndex < bitArray.Length)
{
    // Read ‘0’ bits until a ‘1’ bit is found; let Q bit the number of ‘0’ bits.
    uint q = 0;
    while ((hashesBitIndex < bitArray.Length) && !bitArray[hashesBitIndex])
    {
        q++;
        hashesBitIndex++;
    }

    if ((hashesBitIndex + log2Probability) < bitArray.Length)
    {
        // Discard the ‘1’.
        hashesBitIndex++;

        // Read log2(P) bits after the ‘1’ as an integer. Let R be its value.
        uint r = ReadUInt32FromBitArray(bitArray, hashesBitIndex, log2Probability);

        // Let D be Q * P + R.
        uint d = (q * probability) + r;

        // Increment C by D + 1.
        hash = hash + d + 1;

        hashes.Add((uint)hash);
        hashesBitIndex += log2Probability;
    }
}

Last thing which is needed to query the digest value is the ability to calculate a hash of given URL (and optionally ETag). The important thing here is that DataView used by Service Worker in order to convert SHA-256 to integer is internally using Big-endian byte order so the implementation must accommodate for that.

// URL should be properly percent-encoded (RFC3986).
string key = url;

// If validators is true and ETag is not null.
if (validators && !String.IsNullOrWhiteSpace(entityTag))
{
    // Append ETag to key.
    key += entityTag;
}

// Let hash-value be the SHA-256 message digest (RFC6234) of key, expressed as an integer.
byte[] hash = new SHA256Managed().ComputeHash(Encoding.UTF8.GetBytes(key));
uint hashValue = BitConverter.ToUInt32(hash, 0);
if (BitConverter.IsLittleEndian)
{
    hashValue = (hashValue & 0x000000FFU) << 24 | (hashValue & 0x0000FF00U) << 8
                | (hashValue & 0x00FF0000U) >> 8 | (hashValue & 0xFF000000U) >> 24;
}

// Truncate hash-value to log2(N*P) bits.
int hashValueLength = (int)Math.Log(count * probability, 2);
hashValue = (hashValue >> (_hashValueLengthUpperBound - hashValueLength))
            & (uint)((1 << hashValueLength) - 1);

I've combined all this logic into CacheDigestHashAlgorithm, CacheDigestValue and CacheDigestHeaderValue classes, which I've used in PushPromiseAttribute in order to conditionally push resources.

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, ...)]
public class PushPromiseAttribute : FilterAttribute, IActionFilter
{
    ...

    public void OnActionExecuting(ActionExecutingContext filterContext)
    {
        ...
        HttpRequestBase request = filterContext.HttpContext.Request;
        CacheDigestHeaderValue cacheDigest = (request.Headers["Cache-Digest"] != null) ?
            new CacheDigestHeaderValue(request.Headers["Cache-Digest"]) : null;

        IEnumerable pushPromiseContentPaths = _pushPromiseTable.GetPushPromiseContentPaths(
            filterContext.ActionDescriptor.ControllerDescriptor.ControllerName,
            filterContext.ActionDescriptor.ActionName);

        foreach (string pushPromiseContentPath in pushPromiseContentPaths)
        {
            string pushPromiseContentUrl = GetAbsolutePushPromiseContentUrl(
                filterContext.RequestContext,
                pushPromiseContentPath);

            if (!(cacheDigest?.QueryDigest(pushPromiseContentUrl) ?? false))
            {
                filterContext.HttpContext.Response.PushPromise(pushPromiseContentPath);
            }
        }
    }
}

The easiest way to verify the functionality is to debug the demo application or check "View live HTTP/2 sessions" in chrome://net-internals/#http2 tab. After initial request the Service Worker will take over fetching of the resources by adding cache-digest header to the subsequent requests and serving cached resources with help of Cache API. On the server side the cache-digest header will be picked up and push properly skipped.

Chrome Developer Tools Network Tab - Service Worker

Chrome Net Internals - Service Worker

The idea behind the cookie-based fallback is very simple - the server generates the cache digest by itself and passes it between requests as a cookie. Important difference is that only the digest value can be stored (as cookies doesn't allow semicolon and white spaces in values) so server needs to assume the flags. The algorithm for generating the digest value is similar to the one for reading it, so I'll skip it here (for interested ones it is splited into CacheDigestValue.FromUrls and CacheDigestValue.ToByteArray methods) and move on to changes in PushPromiseAttribute.

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, ...)]
public class PushPromiseAttribute : FilterAttribute, IActionFilter
{
    ...

    public bool UseCookieBasedCacheDigest { get; set; }

    public uint CacheDigestProbability { get; set; }
    ...

    public void OnActionExecuting(ActionExecutingContext filterContext)
    {
        ...
        HttpRequestBase request = filterContext.HttpContext.Request;

        CacheDigestHeaderValue cacheDigest = null;
        if (UseCookieBasedCacheDigest)
        {
            cacheDigest = (request.Cookies["Cache-Digest"] != null) ?
                new CacheDigestHeaderValue(
                    CacheDigestValue.FromBase64String(request.Cookies["Cache-Digest"].Value)) : null;
        }
        else
        {
            cacheDigest = (request.Headers["Cache-Digest"] != null) ?
                new CacheDigestHeaderValue(request.Headers["Cache-Digest"]) : null;
        }

        IDictionary cacheDigestUrls = new Dictionary();

        IEnumerable pushPromiseContentPaths = _pushPromiseTable.GetPushPromiseContentPaths(
            filterContext.ActionDescriptor.ControllerDescriptor.ControllerName,
            filterContext.ActionDescriptor.ActionName);

        foreach (string pushPromiseContentPath in pushPromiseContentPaths)
        {
            string pushPromiseContentUrl = GetAbsolutePushPromiseContentUrl(
                filterContext.RequestContext,
                pushPromiseContentPath);

            if (!(cacheDigest?.QueryDigest(absolutePushPromiseContentUrl) ?? false))
            {
                filterContext.HttpContext.Response.PushPromise(pushPromiseContentPath);
            }
            cacheDigestUrls.Add(absolutePushPromiseContentUrl, null);
        }

        if (UseCookieBasedCacheDigest)
        {
            HttpCookie cacheDigestCookie = new HttpCookie("Cache-Digest")
            {
                Value = CacheDigestValue.FromUrls(cacheDigestUrls, CacheDigestProbability)
                    .ToBase64String(),
                Expires = DateTime.Now.AddYears(1)
            };
            filterContext.HttpContext.Response.Cookies.Set(cacheDigestCookie);
        }
    }
}

There shouldn't be nothing surprising here, but the code exposes couple of limitations when cookie-based fallback is being used.

First of all there is no way to recreate the full list of pushed resources from the digest value. The code above creates new cookie value during every request which is good enough if we always attempt to push the same resources, but if we needs to push something different for a specific action we will loose the information about resources which are not related to that action. One possible optimization here would be to store in digest only resources which are supposed to be pushed almost always and in case of actions which require smaller subset keep the information that all the needed resources were already in the digest (which means that value doesn't have to be changed). Another approach would be to keep a registry of every resource pushed for given session or client, but with such detailed registry using additionally a cookie becomes questionable.

The other thing are checks for resources freshness. The above implementation works well if names can be used for freshness control (for example they contain versions) but in other cases we will not know that a resource has expired. In such scenarios the validators flag should be set to true and ETags provided.

Final thoughts

The Cache Digest seems to be the missing piece which allows fine tuning of HTTP/2 Server Push. There is a number of open issues around the proposal so things might change, but the idea is already being adopted by some of the Web Servers with HTTP/2 support. When the proposal will start closing to its final shape hopefully browsers will start providing native support.

One of the new features in HTTP/2 is Server Push. It allows the server to send resources to the browser without having to wait for the browser to request it. Normally the browser requests needed resources after receiving and parsing the HTML. That creates cost of additional Round Trip Times. If we push critical CSS and JS during the initial request the cost of additional Round Trip Times can be minimized. Also the pushed resources can be cached (which was impossible in cases where inlining was being used for the same purpose).

I don't intend to provide comprehensive information regarding HTTP/2 Server Push here as there is a lot of resources on the web doing that already, all I want is to play a little with Server Push in context of ASP.NET MVC powered by IIS. For that purposes I have created a demo application consisting of following model, view and controller.

public interface IStarWarsCharacters
{
    IEnumerable<string> GetCharacters();
}

public class LazyStarWarsCharacters : IStarWarsCharacters
{
    public IEnumerable<string> GetCharacters()
    {
        foreach(Character character in StarWarsContext.Characters)
        {
            yield return String.Format("{0} ({1})", character.Name,
                character.HomeworldId.HasValue ? StarWarsContext.Planets.First(p => p.Id == character.HomeworldId.Value).Name : "N/A");
            System.Threading.Thread.Sleep(10);
        }
    }
}
@model IStarWarsCharacters
<!DOCTYPE html>
<html>
    <head>
        <link rel="stylesheet" href="~/content/normalize.css">
        <link rel="stylesheet" href="~/content/site.css">
    </head>
    <body>
        <ul>
            @foreach (string starWarsCharacter in Model.GetCharacters())
            {
                <li>@starWarsCharacter</li>
            }
        </ul>
        ...
    </body>
</html>
public class DemoController : Controller
{
    [ActionName("server-push")]
    public ActionResult ServerPush()
    {
        return View("ServerPush", new LazyStarWarsCharacters());
    }
}

For this demo application I've captured the basic timings as visible on screen shot below.

Chrome Developer Tools Network Tab - No Server Push

Proof of Concept HtmlHelper

The IIS built in support for Server Push is exposed to the ASP.NET through HttpResponse.PushPromise method which takes virtual path to the resource which is supposed to be pushed. My first idea for using this method in ASP.NET MVC was HtmlHelper. The idea is simple, whenever a link element is being rendered the helper registers the push as well.

public static class PushPromiseExtensions
{
    public static IHtmlString PushPromiseStylesheet(this HtmlHelper htmlHelper, string contentPath)
    {
        UrlHelper urlHelper = new UrlHelper(htmlHelper.ViewContext.RequestContext);

        htmlHelper.ViewContext.RequestContext.HttpContext.Response.PushPromise(contentPath);

        TagBuilder linkTagBuilder = new TagBuilder("link");
        linkTagBuilder.Attributes.Add("rel", "stylesheet");
        linkTagBuilder.Attributes.Add("href", urlHelper.Content(contentPath));

        return new HtmlString(linkTagBuilder.ToString());
    }
}

The only thing which needs to be done is replacing the links to the resources which should be pushed with calls to the helper.

@model IStarWarsCharacters
<!DOCTYPE html>
<html>
    <head>
        @Html.PushPromiseStylesheet("~/content/normalize.css")
        @Html.PushPromiseStylesheet("~/content/site.css")
    </head>
    <body>
        <ul>
            @foreach (string starWarsCharacter in Model.GetCharacters())
            {
                <li>@starWarsCharacter</li>
            }
        </ul>
        ...
    </body>
</html>

Below screen shot represents timings after the change.

Chrome Developer Tools Network Tab - Server Push During Render (Lazy Data Access)

Even with local connection and small resources the difference is visible. The browser receives initial part of push early which results in very short resource retrieval time.

"When to push?" and "What to push?" are very important

To be honest I've cheated - I've prepared the demo application in a way which supported my initial idea. The model has been prepared so the time expensive processing is being moved to the render phase. Building web applications like that is very good practice but often not possible. Usually the data are being grabbed from database, processed and then passed to the view. Below model is closer to that scenario.

public class EagerStarWarsCharacters : IStarWarsCharacters
{
    IEnumerable<string> _characters;

    public EagerStarWarsCharacters()
    {
        List<string> characters = new List<string>();

        foreach (Character character in StarWarsContext.Characters)
        {
            characters.Add(String.Format("{0} ({1})", character.Name,
                character.HomeworldId.HasValue ? StarWarsContext.Planets.First(p => p.Id == character.HomeworldId.Value).Name : "N/A"));
            System.Threading.Thread.Sleep(10);
        }

        _characters = characters;
    }

    public IEnumerable<string> GetCharacters()
    {
        return _characters;
    }
}

Small change to the action will show the impact on timings.

[ActionName("server-push")]
public ActionResult ServerPush()
{
    return View("ServerPush", new EagerStarWarsCharacters());
}

Chrome Developer Tools Network Tab - Server Push During Render (Eager Data Access)

Because the helper is being executed after the time consuming processing and nothing interrupts the view during writing to the response stream, the push is being send after the HTML. We can still see gain when it comes to the single resources retrieval times (no Round Trip Times) but the overall improvement depends on what is being pushed. When large resources (especially in high numbers) are being pushed, a situation can be reached where classic approach with browser utilizing multiple parallel connections turns out to be more efficient.

Moving to ActionFilter

Knowing that pushing too late might be an issue I've started thinking on a way to push as soon as possible. An ActionFilter was a natural choice. First I needed some kind of registration table with mappings between actions and resources to be pushed. I've also needed to be able to get all resources for given action quickly. A simple abstraction over nested dictionary seemed good enough to start with.

public class PushPromiseTable
{
    private readonly IDictionary<string, IDictionary<string, ICollection<string>>> _pushPromiseTable =
        new Dictionary<string, IDictionary<string, ICollection<string>>>();

    public void MapPushPromise(string controller, string action, string contentPath)
    {
        if (!_pushPromiseTable.ContainsKey(controller))
        {
            _pushPromiseTable.Add(controller, new Dictionary<string, ICollection<string>>());
        }

        if (!_pushPromiseTable[controller].ContainsKey(action))
        {
            _pushPromiseTable[controller].Add(action, new List<string>());
        }

        _pushPromiseTable[controller][action].Add(contentPath);
    }

    internal IEnumerable<string> GetPushPromiseContentPaths(string controller, string action)
    {
        IEnumerable<string> pushPromiseContentPaths = Enumerable.Empty<string>();

        if (_pushPromiseTable.ContainsKey(controller))
        {
            if (_pushPromiseTable[controller].ContainsKey(action))
            {
                pushPromiseContentPaths = _pushPromiseTable[controller][action];
            }
        }

        return pushPromiseContentPaths;
    }
}

The attribute implementation was pretty straight forward. All that needed to be done was getting resources from the registration table based on controller and action, and then pushing them all.

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
public class PushPromiseAttribute : FilterAttribute, IActionFilter
{
    private PushPromiseTable _pushPromiseTable;

    public PushPromiseAttribute(PushPromiseTable pushPromiseTable)
    {
        if (pushPromiseTable == null)
        {
            throw new ArgumentNullException(nameof(pushPromiseTable));
        }

        _pushPromiseTable = pushPromiseTable;
    }

    public void OnActionExecuted(ActionExecutedContext filterContext)
    { }

    public void OnActionExecuting(ActionExecutingContext filterContext)
    {
        if (filterContext == null)
        {
            throw new ArgumentNullException(nameof(filterContext));
        }

        IEnumerable<string> pushPromiseContentPaths = _pushPromiseTable.GetPushPromiseContentPaths(
            filterContext.ActionDescriptor.ControllerDescriptor.ControllerName,
            filterContext.ActionDescriptor.ActionName);

        foreach (string pushPromiseContentPath in pushPromiseContentPaths)
        {
            filterContext.HttpContext.Response.PushPromise(pushPromiseContentPath);
        }
    }
}

The only thing left to do was registering the ActionFilter with proper configuration and reverting view to its original form.

PushPromiseTable pushPromiseTable = new PushPromiseTable();
pushPromiseTable.MapPushPromise("Demo", "server-push", "~/content/normalize.css");
pushPromiseTable.MapPushPromise("Demo", "server-push", "~/content/site.css");

GlobalFilters.Filters.Add(new PushPromiseAttribute(pushPromiseTable));
@model IStarWarsCharacters
<!DOCTYPE html>
<html>
    <head>
        <link rel="stylesheet" href="~/content/normalize.css">
        <link rel="stylesheet" href="~/content/site.css">
    </head>
    <body>
        <ul>
            @foreach (string starWarsCharacter in Model.GetCharacters())
            {
                <li>@starWarsCharacter</li>
            }
        </ul>
        ...
    </body>
</html>

Thanks to the usage of ActionFilter the improvement is visible in timings again.

Chrome Developer Tools Network Tab - ActionFilter Server Push

What about ASP.NET Core

Unfortunately similar mechanisms are not available in ASP.NET Core yet as the API is not available. There are issues created for both Kestrel and HTTP abstractions to provide support but there are no information on planed delivery.

Some time ago I've written about HtmlHelper providing support for Content Security Policy. The solution presented worked well, but it required quite nasty markup and usage of <text> pseudo element.

@using (Html.BeginCspScript())
{
    <text>
    $(document).ready(function () {
        ...
    });
    </text>
}

ASP.NET Core MVC has introduced a new way for server-side code to participate in rendering HTML elements - TagHelpers. The key aspect of TagHelpers is that they attach to HTML elements and allow for modifying them. This is exactly the functionality which should allow improving the markup of previously mentioned solution, so I've decided to try creating one. I've started with class inheriting from TagHelper.

public class ContentSecurityPolicyTagHelper : TagHelper
{
    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
    }
}

The Process method is the one which will be executed when there will be an HTML element matching the TagHelper. The default matching is based on a naming convention which looks for TagHelper by class name (without the TagHelper suffix). This behavior can be adjusted with help of HtmlTargetElementAttribute. A single instance of HtmlTargetElementAttribute is being treated as logical "and" of all conditions while multiple instances are treated as logical "or". In this case I've decided that it would be nice to support two custom elements (<csp-script> and <csp-style>) as well as standard <script> and <style> elements with asp-csp attribute.

[HtmlTargetElement("csp-style")]
[HtmlTargetElement("style", Attributes = "asp-csp")]
[HtmlTargetElement("csp-script")]
[HtmlTargetElement("script", Attributes = "asp-csp")]
public class ContentSecurityPolicyTagHelper : TagHelper
{
    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
    }
}

The custom elements need to be translated into standard ones as soon as processing starts. Also the custom attribute should be removed.

[HtmlTargetElement("csp-style")]
[HtmlTargetElement("style", Attributes = "asp-csp")]
[HtmlTargetElement("csp-script")]
[HtmlTargetElement("script", Attributes = "asp-csp")]
public class ContentSecurityPolicyTagHelper : TagHelper
{
    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        SetTagName(output);

        output.Attributes.RemoveAll("asp-csp");
    }

    private void SetTagName(TagHelperOutput output)
    {
        if (output.TagName == "csp-style")
        {
            output.TagName = "style";
        }
        else if (output.TagName == "csp-script")
        {
            output.TagName = "script";
        }
    }
}

Now the actual Content Security Policy related processing can be added. The ContentSecurityPolicyAttribute from previous post is putting all the relevant information into HttpContext.Items from which I needed to retrieve them. ASP.NET Core MVC provides ViewContextAttribute which informs the framework that property should be set with the current ViewContext when TagHelper is being created, this is how access to HttpContext can be achieved.

...
public class ContentSecurityPolicyTagHelper : TagHelper
{
    [ViewContext]
    public ViewContext ViewContext { get; set; }

    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        ...
    }

    ...
}

After retrieving the needed values from the HttpContext support for nonce can be provided without much effort.

...
public class ContentSecurityPolicyTagHelper : TagHelper
{
    private static IDictionary<string, string> _inlineExecutionContextKeys =
        new Dictionary<string, string>
        {
            { "script", "ScriptInlineExecution" },
            { "style", "StyleInlineExecution" }
        };

    ...

    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        ...

        ContentSecurityPolicyInlineExecution currentInlineExecutionPolicy =
            (ContentSecurityPolicyInlineExecution)ViewContext
            .HttpContext.Items[_inlineExecutionContextKeys[output.TagName]];

        if (currentInlineExecutionPolicy == ContentSecurityPolicyInlineExecution.Nonce)
        {
            output.Attributes.Add("nonce", (string)ViewContext.HttpContext.Items["NonceRandom"]);
        }
        else if (currentInlineExecutionPolicy == ContentSecurityPolicyInlineExecution.Hash)
        {
        }
    }

    ...
}

For the hash support the content of the element is needed. The TagHelper provides a GetChildContentAsync and GetContent methods for this purpose. In order to decide which one to use a check should be done to see if the content has been modified by other TagHelper (in another words if the content should be accessed directly or from the buffer). After obtaining the content the hash can be calculated and added to the list which ContentSecurityPolicyAttribute will use later to generate the response headers. As the content is being obtained by asynchronous method the asynchronous version of Process method must be used.

...
public class ContentSecurityPolicyTagHelper : TagHelper
{
    private static IDictionary<string, string> _inlineExecutionContextKeys =
        new Dictionary<string, string>
        {
            { "script", "ScriptInlineExecution" },
            { "style", "StyleInlineExecution" }
        };

    private static IDictionary<string, string> _hashListBuilderContextKeys =
        new Dictionary<string, string>
        {
            { "script", "ScriptHashListBuilder" },
            { "style", "StyleHashListBuilder" }
        };

    ...

    public override async Task Process(TagHelperContext context, TagHelperOutput output)
    {
        ...

        ContentSecurityPolicyInlineExecution currentInlineExecutionPolicy =
            (ContentSecurityPolicyInlineExecution)ViewContext
            .HttpContext.Items[_inlineExecutionContextKeys[output.TagName]];

        if (currentInlineExecutionPolicy == ContentSecurityPolicyInlineExecution.Nonce)
        {
            output.Attributes.Add("nonce", (string)ViewContext.HttpContext.Items["NonceRandom"]);
        }
        else if (currentInlineExecutionPolicy == ContentSecurityPolicyInlineExecution.Hash)
        {
            string content = output.Content.IsModified ?
                output.Content.GetContent() : (await output.GetChildContentAsync()).GetContent();

            content = content.Replace("\r\n", "\n");
            byte[] contentHashBytes = SHA256.Create().ComputeHash(Encoding.UTF8.GetBytes(content));
            string contentHash = Convert.ToBase64String(contentHashBytes);

            ((StringBuilder)ViewContext.HttpContext.Items[_hashListBuilderContextKeys[output.TagName]])
                .AppendFormat(" 'sha256-{0}'", contentHash);
        }
    }

    ...
}

After registering new TagHelper with addTagHelper directive it can be used through new custom elements or by simply adding the attribute to the standard ones.

<script asp-csp>
    $(document).ready(function() {
        ...
    });
</script>

The full version of code can be found at GitHub and it is ready to use together with ContentSecurityPolicyAttribute as part of Lib.AspNetCore.Mvc.Security.

Older Posts