Two weeks ago I've given a talk at DevConf conference about HTTP/2. During that talk I've mentioned protocol based content delivery as potential way to optimize an application for HTTP/2 users, without degradation in performance for HTTP/1 users at the same time. After the talk I was asked for some code examples. I've decided that it's a great opportunity to spin up ASP.NET Core 2.2 preview (which brings HTTP/2 to ASP.NET Core) and play with it.

The idea behind protocol based content delivery is to branch application logic (usually rendering) based on protocol of current request (in ASP.NET Core this information is available through HttpRequest.Protocol property) in order to employ different optimization strategy. In ASP.NET Core MVC there are multiple levels on which we can branch, depending on how precise we want to be and how far we want to separate logic for different protocols. I'll go through those levels, starting from the bottom.

Conditional rendering

The simplest thing that can be done, is putting an if into a view. This will allow for rendering different blocks of HTML for different protocols. In order to avoid reaching to HttpRequest.Protocol property directly from view, protocol check can be exposed through HtmlHelper.

public static class HtmlHelperHttp2Extensions
{
    public static bool IsHttp2(this IHtmlHelper htmlHelper)
    {
        if (htmlHelper == null)
        {
            throw new ArgumentNullException(nameof(htmlHelper));
        }

        return (htmlHelper.ViewContext.HttpContext.Request.Protocol == "HTTP/2");
    }
}

This provides an easy solution for having different bundling strategies for HTTP/2 (in order to make better use of multiplexing).

@if (Html.IsHttp2())
{
    <script src="~/js/core-bundle.min.js" asp-append-version="true"></script>
    <script src="~/js/page-bundle.min.js" asp-append-version="true"></script>
}
else
{
    <script src="~/js/site-bundle.min.js" asp-append-version="true"></script>
}

This works, but in many cases doesn't give the right development time experience. It requires context switching between C# and markup. It breaks standard HTML parsers. This is especially if we start mixing if statements with TagHelpers which have similar purpose.

@if (Html.IsHttp2())
{
    <environment include="Development">
        <script src="~/js/core-bundle.js" asp-append-version="true"></script>
        <script src="~/js/page-bundle.js" asp-append-version="true"></script>
    </environment>
    <environment exclude="Development">
        <script src="~/js/core-bundle.min.js" asp-append-version="true"></script>
        <script src="~/js/page-bundle.min.js" asp-append-version="true"></script>
    </environment>
}
else
{
    <environment include="Development">
        <script src="~/js/site-bundle.js" asp-append-version="true"></script>
    </environment>
    <environment exclude="Development">
        <script src="~/js/site-bundle.min.js" asp-append-version="true"></script>
    </environment>
}

Do you see EnvironmentTagHelper in the code above? It also serves as an if statement, but much cleaner. Wouldn't it be nice to have one for protocol as well? It's quite easy to create it, just couple checks and call to TagHelperOutput.SuppressOutput().

public class ProtocolTagHelper : TagHelper
{
    [HtmlAttributeNotBound]
    [ViewContext]
    public ViewContext ViewContext { get; set; }

    public override int Order => -1000;

    public string Include { get; set; }

    public string Exclude { get; set; }

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

        output.TagName = null;

        string currentProtocol = ViewContext.HttpContext.Request.Protocol;

        bool shouldSuppressOutput = false;

        if (!String.IsNullOrWhiteSpace(Exclude))
        {
            shouldSuppressOutput = Exclude.Trim().Equals(currentProtocol, StringComparison.OrdinalIgnoreCase);
        }

        if (!shouldSuppressOutput && !String.IsNullOrWhiteSpace(Include))
        {
            shouldSuppressOutput = !Include.Trim().Equals(currentProtocol, StringComparison.OrdinalIgnoreCase);
        }

        if (shouldSuppressOutput)
        {
            output.SuppressOutput();
        }
    }
}

This is a very simple version of ProtocolTagHelper, more powerful one can be found here. But even with this simplest version it is possible to write following markup.

<protocol include="HTTP/2">
    <environment include="Development">
        <script src="~/js/core-bundle.js" asp-append-version="true"></script>
        <script src="~/js/page-bundle.js" asp-append-version="true"></script>
    </environment>
    <environment exclude="Development">
        <script src="~/js/core-bundle.min.js" asp-append-version="true"></script>
        <script src="~/js/page-bundle.min.js" asp-append-version="true"></script>
    </environment>
</protocol>
<protocol exclude="HTTP/2">
    <environment include="Development">
        <script src="~/js/site-bundle.js" asp-append-version="true"></script>
    </environment>
    <environment exclude="Development">
        <script src="~/js/site-bundle.min.js" asp-append-version="true"></script>
    </environment>
</protocol>

Doesn't this look much cleaner than if statement? This is not the only thing that Tag Helper can help with. Another thing one might want to do is applying different CSS class to an element depending on protocol. Different CSS classes may result in loading different sprite files (again to better utilize multiplexing in case of HTTP/2). Here it would be ugly to copy entire element just to have different value in class attribute. Luckily Tag Helpers can target attributes and change values of other attributes.

[HtmlTargetElement(Attributes = "asp-http2-class")]
public class Http2ClassTagHelper : TagHelper
{
    [HtmlAttributeNotBound]
    [ViewContext]
    public ViewContext ViewContext { get; set; }

    [HtmlAttributeName("asp-http2-class")]
    public string Http2Class { get; set; }

    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        if (!String.IsNullOrWhiteSpace(Http2Class) && (ViewContext.HttpContext.Request.Protocol == "HTTP/2"))
        {
            output.Attributes.SetAttribute("class", Http2Class);
        }

        output.Attributes.RemoveAll("asp-http2-class");
    }
}

The above Tag Helper will target any element with asp-http2-class attribute and if protocol of current request is HTTP/2 it will use asp-http2-class attribute value for class attribute value. Below code will render different markup for different protocols.

<h1 class="http1" asp-http2-class="http2">Conditional Rendering</h1>

Thanks to those two Tag Helpers a lot can be achieved, but if there is a lot of differences the code may become unreadable. In such cases cleaner separation is required. In order to achieve that, branching needs to be done at higher level.

View discovery

If views for HTTP/2 and HTTP/1 are significantly different, it would be nice if ASP.NET Core MVC could simply use different view based on protocol. ASP.NET Core MVC determines which view should be used through view discovery process, which can be customized by using a custom IViewLocationExpander. As the name implies, an implementation of IViewLocationExpander can expand list of discovered view locations, for example by appending "-h2" suffix to the ones discovered by default convention.

public class Http2ViewLocationExpander : IViewLocationExpander
{
    public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context,
        IEnumerable<string> viewLocations)
    {
        context.Values.TryGetValue("PROTOCOL_SUFFIX", out string protocolSuffix);

        if (String.IsNullOrWhiteSpace(protocolSuffix))
        {
            return viewLocations;
        }

        return ExpandViewLocationsCore(viewLocations, protocolSuffix);
    }

    private IEnumerable<string> ExpandViewLocationsCore(IEnumerable<string> viewLocations,
        string protocolSuffix)
    {
        foreach (var location in viewLocations)
        {
            yield return location.Insert(location.LastIndexOf('.'), protocolSuffix);
            yield return location;
        }
    }

    public void PopulateValues(ViewLocationExpanderContext context)
    {
        context.Values["PROTOCOL_SUFFIX"] =
            (context.ActionContext.HttpContext.Request.Protocol == "HTTP/2") ? "-h2" : null;
    }
}

An instance of IViewLocationExpander implementation needs to be added to RazorViewEngineOptions.

services.Configure<RazorViewEngineOptions>(options =>
    options.ViewLocationExpanders.Add(new Http2ViewLocationExpander())
);

After that, for requests over HTTP/2, the view locations list might look like below.

/Views/Demo/ViewDiscovery-h2.cshtml
/Views/Demo/ViewDiscovery.cshtml
/Views/Shared/ViewDiscovery-h2.cshtml
/Views/Shared/ViewDiscovery.cshtml
/Pages/Shared/ViewDiscovery-h2.cshtml
/Pages/Shared/ViewDiscovery.cshtml

If a view dedicated for HTTP/2 (with "-h2" suffix) exists, it will be chosen instead of "regular" one. Of course this is only one of possible conventions. There are other options, like for example subfolders.

Action Selection

There is one more level on which one may want to branch - business logic level. If the business logic is supposed to be different depending on protocol, it might be the best to have separated actions. For this purpose ASP.NET Core MVC provides IActionConstraint. All that needs to be implemented is an attribute with an Accept() method, which will return true or false based on current protocol.

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class Http2OnlyAttribute : Attribute, IActionConstraint
{
    public int Order { get; set; }

    public bool Accept(ActionConstraintContext context)
    {
        return (context.RouteContext.HttpContext.Request.Protocol == "HTTP/2");
    }
}

This attribute can be applied to an action in order to make it HTTP/2 only (also a second attribute to make actions HTTP/1 only can be created).

All mentioned mechanism create a comprehensive set of tools to deal with protocol based content delivery in various scenarios. I've gathered them all in a single demo project, which you can find here.

If you've been reading my blog you probably know I like to explore web standards in context of ASP.NET Core. I've written about Push API, Clear Site Data, Server Timing or Client Hints. Recently I've learned that Reporting API has made its way to Chrome 69 as an experimental feature, and I couldn't resist myself.

Reporting API aims at providing a framework which allows web developers to associate reporting endpoints with an origin. Different browser features can use these endpoints to deliver specific but consistent reports. If you are familiar with Content Security Policy, you might be noticing similarity to report-uri directive, as it provides this exact capability for CSP. In fact, Chrome already provides a way for integrating both of them through report-to directive.

This all looks very interesting, so I've quickly run Chrome with the feature enabled and started playing with it.

chrome.exe --enable-features=Reporting

Configuring endpoints

The reporting endpoints are configured through Report-To response header. This header value should be a JSON object which contains a list of endpoints and additional properties. It can be represented by following object.

public class ReportToHeaderValue
{
    [JsonProperty(PropertyName = "group", NullValueHandling = NullValueHandling.Ignore)]
    public string Group { get; set; }

    [JsonProperty(PropertyName = "include_subdomains", NullValueHandling = NullValueHandling.Ignore)]
    public bool? IncludeSubdomains { get; set; }

    [JsonProperty(PropertyName = "max_age")]
    public uint MaxAge { get; set; } = 10886400;

    [JsonProperty(PropertyName = "endpoints")]
    public IList<ReportToEndpoint> Endpoints { get; } = new List<ReportToEndpoint>();

    public override string ToString()
    {
        return JsonConvert.SerializeObject(this);
    }
}

The include_subdomains and max_age properties should be familiar, they are often seen in modern web standards. The include_subdomains is optional and if absent its value defaults to false. The max_age is required, and can also be used to remove specific group by using 0 as value.

The group property allows for logical grouping of endpoints (if it's not provided the group is named default). The group name can be used in places where browser features provide integration with Reporting API in order to make them send reports to different group than default. For example, previously mentioned report-to directive takes group name as parameter.

The most important part are endpoints. First of all the provide URLs, but that's not all.

public struct ReportToEndpoint
{
    [JsonProperty(PropertyName = "url")]
    public string Url { get; }

    [JsonProperty(PropertyName = "priority", NullValueHandling = NullValueHandling.Ignore)]
    public uint? Priority { get; }

    [JsonProperty(PropertyName = "weight", NullValueHandling = NullValueHandling.Ignore)]
    public uint? Weight { get; }

    public ReportToEndpoint(string url, uint? priority = null, uint? weight = null)
        : this()
    {
        Url = url ?? throw new ArgumentNullException(nameof(url));
        Priority = priority;
        Weight = weight;
    }
}

The optional priority and weight properties are there to help browser in selecting to which of the URLs the report should be send. The priority groups URLs into a single failover class while weight tells what fraction of traffic within the failover class should be send to specific URL.

To check the configured groups and failover classes for different origins one can use chrome://net-internals/#reporting.

Chrome Net Internals - Reporting

Receiving reports

Before a report can be received it must be triggered. Previously mentioned CSP violations are easy to cause, but they wouldn't be interesting. Currently Chrome supports couple other report types like deprecations, interventions and Network Error Logging. The deprecations look interesting from development pipeline perspective as they can be used in QA environments to early detect that one of features used by application is about to be removed. For demo purposes attempting a synchronous XHR request is a great example.

function triggerDeprecation() {
    // Synchronous XHR
    var xhr = new XMLHttpRequest();
    xhr.open('GET', '/xhr', false);
    xhr.send();
};

The report consists of type, age, originating URL, user agent and body. The body contains attributes specific for report type.

public class Report
{
    [JsonProperty(PropertyName = "type")]
    public string Type { get; set; }

    [JsonProperty(PropertyName = "age")]
    public uint Age { get; set; }

    [JsonProperty(PropertyName = "url")]
    public string Url { get; set; }

    [JsonProperty(PropertyName = "user_agent")]
    public string UserAgent { get; set; }

    [JsonProperty(PropertyName = "body")]
    public IDictionary<string, object> Body { get; set; }
}

Typically browser queues reports (as they can be triggered in very rapid succession) and sends all of them at later, convenient moment. This means that code receiving the POST request should be expecting a list. One approach can be a middleware (a controller action would also work, but it would require a custom formatter as reports are delivered with media type application/reports+json).

public class ReportToEndpointMiddleware
{
    private readonly RequestDelegate _next;

    public ReportToEndpointMiddleware(RequestDelegate next)
    {
        _next = next ?? throw new ArgumentNullException(nameof(next));
    }

    public async Task Invoke(HttpContext context)
    {
        if (IsReportsRequest(context.Request))
        {
            List<Report> reports = null;

            using (StreamReader requestBodyReader = new StreamReader(context.Request.Body))
            {
                using (JsonReader requestBodyJsonReader = new JsonTextReader(requestBodyReader))
                {
                    JsonSerializer serializer = new JsonSerializer();

                    reports = serializer.Deserialize<List<Report>>(requestBodyJsonReader);
                }
            }

            ...

            context.Response.StatusCode = StatusCodes.Status204NoContent;
        }
        else
        {
            await _next(context);
        }
    }

    private bool IsReportsRequest(HttpRequest request)
    {
        return HttpMethods.IsPost(request.Method)
               && (request.ContentType == "application/reports+json");
    }
}

Last remaining thing is to do something useful with the reports. In simplest scenario they can be logged.

public class ReportToEndpointMiddleware
{
    ...
    private readonly ILogger _logger;

    public ReportToEndpointMiddleware(RequestDelegate next, ILogger<ReportToEndpointMiddleware> logger)
    {
        ...
        _logger = logger;
    }

    public async Task Invoke(HttpContext context)
    {
        if (IsReportsRequest(context.Request))
        {
            ...

            LogReports(reports);

            context.Response.StatusCode = StatusCodes.Status204NoContent;
        }
        else
        {
            await _next(context);
        }
    }

    ...

    private void LogReports(List<Report> reports)
    {
        if (reports != null)
        {
            foreach (Report report in reports)
            {
                switch (report.Type.ToLowerInvariant())
                {
                    case "deprecation":
                        _logger.LogWarning("Deprecation reported for file {SourceFile}"
                            + " (Line: {LineNumber}, Column: {ColumnNumber}): '{Message}'",
                            report.Body["sourceFile"], report.Body["lineNumber"],
                            report.Body["columnNumber"], report.Body["message"]);
                        break;
                    default:
                        _logger.LogInformation("Report of type '{ReportType}' received.", report.Type);
                        break;
                }
            }
        }
    }
}

Conclusion

Reporting API looks really promising. It's early, but it can be already used in your CI pipelines and QA environments to broaden range of issues which are automatically detected. If you want to better understand what Chrome provides, I suggest this article. I have also made my demo available on GitHub.

Some time ago I was working on exposing RethinkDB change feed from ASP.NET Core. Recently I've been asked to prepare a proof-of-concept for achieving same functionality on top of Azure Cosmos DB.

Defining an abstraction

First things first, what is the meaning of "same functionality" here. It's not about a specific use case like my demo application, but about the idea of real-time updates feed. In terms which we developers understand the best, the goal is to implement following interface.

public interface IChangeFeed<T>
{
    T CurrentNewValue { get; }

    Task<bool> MoveNextAsync(CancellationToken cancelToken = default(CancellationToken));
}

Clearly the interface derives from RethinkDB capabilities, so the implementation for it is straightforward.

internal class RethinkDbChangeFeed<T> : IChangeFeed<T>
{
    private readonly Cursor<Change<T>> _cursor;

    public T CurrentNewValue { get { return _cursor.Current.NewValue; } }

    public RethinkDbChangeFeed(Cursor<Change<T>> cursor)
    {
        _cursor = cursor;
    }

    public Task<bool> MoveNextAsync(CancellationToken cancelToken = default(CancellationToken))
    {
        return _cursor.MoveNextAsync(cancelToken);
    }
}

How about Cosmos DB?

Change feed in Cosmos DB

The Azure Cosmos DB has built-in support for change feed, but it's different from RethinkDB. It doesn't have the real-time nature. It's a persisted, sorted list of changed documents which can be read up to current moment and processed. In order to read the change feed one must get the partition key ranges for desired collection, create change feed query for every partition key range, get all responses from every query and iterate over the documents in the responses. To get the real-time feeling the entire process needs to be wrapped in polling pattern based on checkpoints.

The implementation

Knowing how change feed works in Cosmos DB it shouldn't be a surprise that implementation will be more complicated. In general the steps for reading the feed are not hard to implement if the goal is to read it completely. But the desired abstraction is in fact an enumerator. The feed should be represented as endless stream which can be advanced by single item. This calls for some kind of state machine. There is an easy way for implementing state machines in C# - you create a method which returns IEnumerable and use yield return in correct place. This could allow for straightforward implementation, but there is a catch. The reading the feed involves async calls and IAsyncEnumerable is not here yet. The resulting implementation would be blocking, which is not desired (if you are interested in how it would look check out BlockingCosmosDbChangefeed). For a correct asynchronous implementation the "state" needs to be managed manually. The state in this case consist of feed enumerator, query and partition key ranges. Approaching the process "backwards" gives following algorithm:

  1. If there is a change feed enumerator and it can be moved to next value, move and return.
  2. If there is a query and it contains more results, obtain new enumerator from query and return to step 1.
  3. If there are partition key ranges and current range is not the last one, create new query for next range and return to step 2.
  4. If there are partition key ranges and current range is the last one, wait (polling delay).
  5. Read partition key ranges.

This can be wrapped in a loop. Additionally there should be a cancellation token which allows to break that loop. A simple (it will go through unnecessary check while returning to step 2) implementation can look like below.

internal class CosmosDbChangefeed<T> : IChangefeed<T>
{
    ...

    public T CurrentNewValue { get; set; } = default(T);

    ...

    public async Task<bool> MoveNextAsync(CancellationToken cancelToken = default(CancellationToken))
    {
        while (!cancelToken.IsCancellationRequested)
        {
            if (MoveCollectionChangeFeedEnumeratorNext())
            {
                return true;
            }

            if (await ExecuteCollectionChangeFeedQueryNextResultAsync(cancelToken))
            {
                continue;
            }

            if (CreateDocumentChangeFeedQueryForNextPartitionKeyRange(cancelToken))
            {
                continue;
            }

            await WaitForNextPoll(cancelToken);

            await ReadCollectionPartitionKeyRanges(cancelToken);
        }

        return false;
    }
}

Let's go through the steps starting from the last one. Reading partition key ranges is a copy-paste from documentation, the only important thing here is to clear current index.

internal class CosmosDbChangefeed<T> : IChangefeed<T>
{
    ...

    private int _collectionPartitionKeyRangeIndex;
    private List<PartitionKeyRange> _collectionPartitionKeyRanges;

    ...

    private async Task ReadCollectionPartitionKeyRanges(CancellationToken cancelToken)
    {
        if (!cancelToken.IsCancellationRequested)
        {
            List<PartitionKeyRange> collectionPartitionKeyRanges = new List<PartitionKeyRange>();

            string collectionPartitionKeyRangesResponseContinuation = null;
            do
            {
                FeedResponse<PartitionKeyRange> collectionPartitionKeyRangesResponse =
                    await _documentClient.ReadPartitionKeyRangeFeedAsync(_collectionUri, new FeedOptions
                    {
                        RequestContinuation = collectionPartitionKeyRangesResponseContinuation
                    });

                collectionPartitionKeyRanges.AddRange(collectionPartitionKeyRangesResponse);
                collectionPartitionKeyRangesResponseContinuation =
                    collectionPartitionKeyRangesResponse.ResponseContinuation;
            }
            while (collectionPartitionKeyRangesResponseContinuation != null);

            _collectionPartitionKeyRanges = collectionPartitionKeyRanges;
            _collectionPartitionKeyRangeIndex = -1;
        }
    }
}

Creating the query requires passing the partition key range identifier (we need to correctly advance index here and check bounds) and the point from which the feed should be queried. This can be either a beginning of the feed, point in time or checkpoint. All three options can be provided in any combination, in such case the checkpoint is most important and the beginning is least important. This implementation provides the point in time (start of the application) and attempts to provide checkpoint. If the checkpoint is present it will win.

internal class CosmosDbChangefeed<T> : IChangefeed<T>
{
    private static DateTime _startTime = DateTime.Now;

    ...

    private IDocumentQuery<Document> _collectionChangeFeedQuery;
    private readonly Dictionary<string, string> _collectionPartitionKeyRangesCheckpoints =
        new Dictionary<string, string>();

    ...

    private bool CreateDocumentChangeFeedQueryForNextPartitionKeyRange(CancellationToken cancelToken)
    {
        if ((_collectionPartitionKeyRanges != null)
            && ((++_collectionPartitionKeyRangeIndex) < _collectionPartitionKeyRanges.Count)
            && !cancelToken.IsCancellationRequested)
        {
            string collectionPartitionKeyRangeCheckpoint = null;
            _collectionPartitionKeyRangesCheckpoints
                .TryGetValue(_collectionPartitionKeyRanges[_collectionPartitionKeyRangeIndex].Id,
                             out collectionPartitionKeyRangeCheckpoint);

            _collectionChangeFeedQuery = _documentClient.CreateDocumentChangeFeedQuery(_collectionUri,
            new ChangeFeedOptions
            {
                PartitionKeyRangeId = _collectionPartitionKeyRanges[_collectionPartitionKeyRangeIndex].Id,
                RequestContinuation = collectionPartitionKeyRangeCheckpoint,
                MaxItemCount = -1,
                StartTime = _startTime
            });

            return true;
        }

        return false;
    }
}

Obtaining the enumerator from query is done by executing new result. This is also the moment when the checkpoint can stored. The result provides it in form of the last logical sequence number of the document.

internal class CosmosDbChangefeed<T> : IChangefeed<T>
{
    ...

    private IEnumerator<T> _collectionChangeFeedEnumerator;

    ...

    private async Task<bool> ExecuteCollectionChangeFeedQueryNextResultAsync(CancellationToken cancelToken)
    {
        if ((_collectionChangeFeedQuery != null)
            && _collectionChangeFeedQuery.HasMoreResults
            && !cancelToken.IsCancellationRequested)
        {
            FeedResponse<T> collectionChangeFeedResponse =
                await _collectionChangeFeedQuery.ExecuteNextAsync<T>(cancelToken);
            _collectionPartitionKeyRangesCheckpoints
                [_collectionPartitionKeyRanges[_collectionPartitionKeyRangeIndex].Id] =
                collectionChangeFeedResponse.ResponseContinuation;

            _collectionChangeFeedEnumerator = collectionChangeFeedResponse.GetEnumerator();

            return true;
        }

        return false;
    }
}

The enumerator is standard .NET enumerator, we can go through it and dispose it when done.

internal class CosmosDbChangefeed<T> : IChangefeed<T>
{
    ...

    private bool MoveCollectionChangeFeedEnumeratorNext()
    {
        if (_collectionChangeFeedEnumerator != null)
        {
            if (_collectionChangeFeedEnumerator.MoveNext())
            {
                CurrentNewValue = _collectionChangeFeedEnumerator.Current;
                return true;
            }

            _collectionChangeFeedEnumerator.Dispose();
            _collectionChangeFeedEnumerator = null;
        }

        return false;
    }

    ...


}

This is it. I've skipped the wait step, but it's just a Task.Delay with condition. The whole solution can be found here.

Cosmos DB change feed processor

I had a very specific abstraction to implement, this is why I've used the SDK directly. There is a way to avoid the low level details by using change feed processor library. It hides a lot of complexity and allows easy distribution across multiple consumers. All you need to do is provide two implementations (DocumentFeedObserver/IChangeFeedObserver and DocumentFeedObserverFactory/IChangeFeedObserverFactory) and a lease collection (to coordinate multiple clients). It's a great alternative in many cases.

More than a year ago I've written about supporting Server-Sent Events in ASP.NET Core. Since that time I've been maintaining a middleware providing such support. I wasn't doing that just as a hobby, I've been using this middleware in several applications. Somehow all of them were deployed with Kestrel on the edge (internal infrastructure etc.). Only recently I've deployed first real-life application with this middleware to Azure. After few days issues started to appear. Users were stopping receiving updates after some (indeterministic) time. There was also no correlation between users actions and SSE connection drop. I had to look for answer in App Service log. I've found it, a 502.3 coming from IIS. So this was it, but what it was? I had to perform further research to discover the root cause. Long story short, I've learned that ANCM (the module which is used to run Kestrel behind IIS) has a request timeout of 120 seconds, which is measured from last activity. In another words, if there is a long living connection and it doesn't send anything for 2 minutes, it will be killed by ANCM. I needed to mitigate that.

Keepalives to the rescue

There is a known solution for scenarios where we want to prevent a network connection from being teardown due to inactivity - keepalives. The mechanism is simple, the side which wants to uphold the connection sends a control message at predefined intervals. Many protocols have built-in support for keepalives. TCP has optional keepalive which can be enabled per connection. WebSockets provide ping and pong frames for easy on-demand keepalive. Server-Sent Events doesn't provide anything dedicated, but it can be implemented easily.

Implementing keepalives

Keepalives require a long running background task, in case of ASP.NET Core that calls for BackgroundService or IHostedService implementation. I wanted my middleware to target ASP.NET Core 2.0.0 so I went with IHostedService. One special case I needed to handle was having multiple instances of the service. The ServerSentEventsMiddleware can be registered at multiple endpoints and in such scenario it uses different implementations of ServerSentEventsService to isolate those endpoints. This means that every implementation of ServerSentEventsService requires its own instance of IHostedService and separated set of options. Generics to the rescue.

public class ServerSentEventsServiceOptions<TServerSentEventsService>
    where TServerSentEventsService : ServerSentEventsService
{
}
internal class ServerSentEventsKeepaliveService<TServerSentEventsService> : IHostedService, IDisposable
    where TServerSentEventsService : ServerSentEventsService
{
    ...

    private readonly ServerSentEventsServiceOptions<TServerSentEventsService> _options;
    private readonly TServerSentEventsService _serverSentEventsService;

    public ServerSentEventsKeepaliveService(TServerSentEventsService serverSentEventsService,
        IOptions<ServerSentEventsServiceOptions<TServerSentEventsService>> options)
    {
        _options = options?.Value ?? throw new ArgumentNullException(nameof(options));
        _serverSentEventsService = serverSentEventsService;
    }

    ...
}

The simplest thing which can be send as keepalive is a single comment line (for example : KEEPALIVE). This is correct from protocol point of view, requires the smallest amount of data to be send and will be ignored by browser.

public class ServerSentEventsServiceOptions<TServerSentEventsService>
    where TServerSentEventsService : ServerSentEventsService
{
    public int KeepaliveInterval { get; set; }  = 30;
}
internal class ServerSentEventsKeepaliveService<TServerSentEventsService> : IHostedService, IDisposable
    where TServerSentEventsService : ServerSentEventsService
{
    ...

    private readonly static ServerSentEventBytes _keepaliveServerSentEventBytes =
        ServerSentEventsHelper.GetCommentBytes("KEEPALIVE");

    ...

    private async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            await _serverSentEventsService.SendEventAsync(_keepaliveServerSentEventBytes);

            await Task.Delay(TimeSpan.FromSeconds(_options.KeepaliveInterval), stoppingToken);
        }
    }
}

The last thing left to do is tying up ServerSentEventsService registration with ServerSentEventsKeepaliveService registration.

public static IServiceCollection AddServerSentEvents<TIServerSentEventsService, TServerSentEventsService>(
    this IServiceCollection services,
    Action<ServerSentEventsServiceOptions<TServerSentEventsService>> configureOptions)
    where TIServerSentEventsService : class, IServerSentEventsService
    where TServerSentEventsService : ServerSentEventsService, TIServerSentEventsService
{
    ...

    services.AddSingleton<TServerSentEventsService>();
    services.AddSingleton<TIServerSentEventsService>(serviceProvider =>
        serviceProvider.GetService<TServerSentEventsService>());

    services.Configure(configureOptions);
    services.AddSingleton<IHostedService, ServerSentEventsKeepaliveService<TServerSentEventsService>>();

    return services;
}

Sending keepalives when behind ANCM

It's nice when libraries have useful defaults. Keepalives are needed when application runs behind ANCM, but in other scenarios not necessary. It would be great if that could be the default behaviour.

public enum ServerSentEventsKeepaliveMode
{
    Always,
    Never,
    BehindAncm
}
public class ServerSentEventsServiceOptions<TServerSentEventsService>
    where TServerSentEventsService : ServerSentEventsService
{
    public ServerSentEventsKeepaliveMode KeepaliveMode { get; set; } = ServerSentEventsKeepaliveMode.BehindAncm;

    ...
}

The only requirement is ability to detect the ANCM. I had hard time finding out how to do that, luckily David Fowler gave me a hand. This allowed me to add a check to StartAsync.

internal class ServerSentEventsKeepaliveService<TServerSentEventsService> : IHostedService, IDisposable
    where TServerSentEventsService : ServerSentEventsService
{
    ...

    public Task StartAsync(CancellationToken cancellationToken)
    {
        if ((_options.KeepaliveMode == ServerSentEventsKeepaliveMode.Always)
            || ((_options.KeepaliveMode == ServerSentEventsKeepaliveMode.BehindAncm) && IsBehindAncm()))
        {
            _executingTask = ExecuteAsync(_stoppingCts.Token);

            if (_executingTask.IsCompleted)
            {
                return _executingTask;
            }
        }

        return Task.CompletedTask;
    }

    ...

    private static bool IsBehindAncm()
    {
        return !String.IsNullOrEmpty(Environment.GetEnvironmentVariable("ASPNETCORE_PORT"))
            && !String.IsNullOrEmpty(Environment.GetEnvironmentVariable("ASPNETCORE_APPL_PATH"))
            && !String.IsNullOrEmpty(Environment.GetEnvironmentVariable("ASPNETCORE_TOKEN"));
    }
}

This gave me exactly what I wanted and hopefully made my library more useful for others.

For the past last posts I was looking at different mechanisms hiding under the hood of ASP.NET Core WebHooks. They were all part of processing which happens before a mapped action is executed. When the action is about to be executed, there is one more thing which needs to be done - binding the parameters. This will be the subject of fifth post in this series:

ASP.NET Core WebHooks are capable of binding several different parameters thanks to WebHookBindingInfoProvider application model. Let's take a look at it.

Built-in binding capabilities

The WebHookBindingInfoProvider application model has single responsibility, it sets the binding information for parameters. It iterates through all action parameters, skip those which already have binding information (for example in result of using model binding attributes) and checks if parameter meet conditions for one of the four supported:

  • If the parameter name is receiver, receiverName or webHookReceiver (case ignored) and its type is String, it will be set to bind the receiver name from path.
  • If the parameter name is id or receiverId and its type is String, it will be set to bind the id from path.
  • If the parameter name is action, actions, actionName, actionNames, event, events, eventName or eventNames and its type is String or IEnumerable<String> implementation, it will be set to bind the event from path.
  • If the parameter name is data it will be set to bind from body as it would be decorated with FromFormAttribute (if the body type in metadata has been set to WebHookBodyType.Form) or FromBodyAttribute (if the body type in metadata has been set to WebHookBodyType.Json or WebHookBodyType.Xml).

If the parameter haven't meet conditions for any of the supported ones, the WebHookBindingInfoProvider will check if given receiver has defined additional action parameters by implementing IWebHookBindingMetadata as part of its metadata. If yes, and the parameter name matches one of them, it will set binding information based on associated WebHookParameter instance. This way receiver can ensure automatic binding for parameters coming from request headers, query parameters or route values.

If all of the above fails, one final attempt to set binding information will be made. If the parameter type is compatible with IFormCollection, JToken or XElement it will be treated as data regardless of its name.

Customizing binding

There are two things regarding binding which one might want to customize: changing the way in which request body is handled and supporting more fancy additional parameters than IWebHookBindingMetadata allows.

The only way to customize the built in behaviour is to use the same extension mechanism which defines it - application model. Thankfully all the application models used by ASP.NET Core WebHooks have exposed order, so one can be placed after them.

internal class WebSubBindingInfoProvider : IApplicationModelProvider
{

    public static int Order => WebHookBindingInfoProvider.Order + 10;

    int IApplicationModelProvider.Order => Order;

    public void OnProvidersExecuted(ApplicationModelProviderContext context)
    { }

    public void OnProvidersExecuting(ApplicationModelProviderContext context)
    { }
}

In case of the WebSub receiver I'm working on, the body type is not known upfront. I wanted to remove the body type validation. To achieve that I needed to remove WebHookVerifyBodyTypeFilter added by WebHookActionModelFilterProvider for actions decorated with WebSubWebHookAttribute.

internal class WebSubBindingInfoProvider : IApplicationModelProvider
{
    public void OnProvidersExecuted(ApplicationModelProviderContext context)
    {
        ...

        for (int controllerIndex = 0; controllerIndex < context.Result.Controllers.Count;
             controllerIndex++)
        {
            ControllerModel controller = context.Result.Controllers[controllerIndex];
            for (int acionIndex = 0; acionIndex < controller.Actions.Count; acionIndex++)
            {
                ActionModel action = controller.Actions[acionIndex];

                WebSubWebHookAttribute attribute =
                    action.Attributes.OfType<WebSubWebHookAttribute>().FirstOrDefault();
                if (attribute == null)
                {
                    continue;
                }

                RemoveWebHookVerifyBodyTypeFilter(action);
            }
        }
    }

    ...

    private static void RemoveWebHookVerifyBodyTypeFilter(ActionModel action)
    {
        IList<IFilterMetadata> filters = action.Filters;

        int webHookVerifyBodyTypeFilterIndex = 0;
        for (; webHookVerifyBodyTypeFilterIndex < filters.Count; webHookVerifyBodyTypeFilterIndex++)
        {
            if (filters[webHookVerifyBodyTypeFilterIndex] is WebHookVerifyBodyTypeFilter)
            {
                break;
            }
        }

        if (webHookVerifyBodyTypeFilterIndex < filters.Count)
        {
            filters.RemoveAt(webHookVerifyBodyTypeFilterIndex);
        }
    }
}

But spinning up application model only to remove an attribute would be a waste of its power. It can be used to provide automatic support for more parameters. I wanted to automatically bind parameters which represent a subscription (WebSubSubscription) related to the request. At the time of model binding custom filters which are part of my receiver have already cached subscription in HttpContext.Items, so it would be best to get it from there. The easiest way to bind parameters from non-standard places is to use a model binder.

internal class WebSubSubscriptionModelBinder : IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        ...

        IDictionary<object, object> items = bindingContext.HttpContext.Items;

        if (items.ContainsKey("WebSubSubscription"))
        {
            bindingContext.Result = ModelBindingResult.Success(items["WebSubSubscription"]);
        }
        else
        {
            bindingContext.Result = ModelBindingResult.Failed();
        }

        return Task.CompletedTask;
    }
}

Now it is enough to find parameters with desired type and set its binding source and binder type.

internal class WebSubBindingInfoProvider : IApplicationModelProvider
{
    public void OnProvidersExecuted(ApplicationModelProviderContext context)
    {
        ...

        for (int controllerIndex = 0; controllerIndex < context.Result.Controllers.Count;
             controllerIndex++)
        {
            ControllerModel controller = context.Result.Controllers[controllerIndex];
            for (int acionIndex = 0; acionIndex < controller.Actions.Count; acionIndex++)
            {
                ...
                AddParametersBindingInfos(action);
            }
        }
    }

    ...

    private void AddParametersBindingInfos(ActionModel action)
    {
        for (int parameterIndex = 0; parameterIndex < action.Parameters.Count; parameterIndex++)
        {
            ParameterModel parameter = action.Parameters[parameterIndex];

            if (typeof(WebSubSubscription) == parameter.ParameterType)
            {
                if (parameter.BindingInfo == null)
                {
                    parameter.BindingInfo = new BindingInfo();
                }

                parameter.BindingInfo.BindingSource = BindingSource.ModelBinding;
                parameter.BindingInfo.BinderType = typeof(HttpContextItemsModelBinder);
                parameter.BindingInfo.BinderModelName = "WebSubSubscription";
            }
        }
    }
}

This pattern is very flexible and allows handling anything for which model binder can be implemented.

Parsing the request body at will

There is one more thing worth mentioning. What if you don't want to use model binding for your action or you want to parse the request body in a filter prior to model binding? ASP.NET Core WebHooks got you covered by providing IWebHookRequestReader service. You can obtain its instance from the DI and call ReadAsFormDataAsync or ReadBodyAsync to get the parsed request body. Also the usage of this service makes it safe for body to be read multiple times.

Altogether ASP.NET Core WebHooks should handle any typical data out-of-the-box but can be also bend to handle more unusual scenarios.

Older Posts