Previous month:
February 2017

March 2017

ASP.NET Core Web API Performance - Throughput for Upload and Download

After working with the new ASP.NET Core server Kestrel and the HttpClient for a while in a number of projects I run into some performance issues. Actually, it was a throughput issue.
It took me some time to figure out whether it is the server or the client responsible for the problems. And the answer is: both.

Here are some hints to get more out of your web applications and Web APIs.

The code for my test server and client are on GitHub: https://github.com/PawelGerr/AspNetCorePerformance

In the following sections we will download and upload data using different schemes, storages and parameters measuring the throughput.

Download data via HTTP

Nothing special, we download a 20 MB file from the server using the default FileStreamResult:

[HttpGet("Download")]
public IActionResult Download()
{
    return File(new MemoryStream(_bytes), "application/octet-stream");
}

The throughput on my machine is 140 MB/s.
For the next test we are using a CustomFileResult with increased buffer size of 64 KB and suddenly get a throughput of 200 MB/s.

Upload multipart/form-data via HTTP

The ASP.NET Core introduced a new type IFormFile that enables us to receive multipart/form-data without any manual work. For that we create a new model with a property of type IFormFile and use this model as an argument of a Web API method.

public class UploadMultipartModel
{
    public IFormFile File { get; set; }
    public int SomeValue { get; set; }
}

-------------

[HttpPost("UploadMultipartUsingIFormFile")]
public async Task<IActionResult> UploadMultipartUsingIFormFile(UploadMultipartModel model)
{
    var bufferSize = 32 * 1024;
    var totalBytes = await Helpers.ReadStream(model.File.OpenReadStream(), bufferSize);

    return Ok();
}

-------------

public static async Task<int> ReadStream(Stream stream, int bufferSize)
{
    var buffer = new byte[bufferSize];

    int bytesRead;
    int totalBytes = 0;

    do
   {
       bytesRead = await stream.ReadAsync(buffer, 0, bufferSize);
        totalBytes += bytesRead;
    } while (bytesRead > 0);
    return totalBytes;
}

Using the IFormFile to transfer 20 MB we get a pretty bad throughput of 30 MB/s. Luckily we got another means to get the content of a multipart/form-data request, the MultipartReader.
Having the new reader we are able to improve the throughput up to 350 MB/s.

[HttpPost("UploadMultipartUsingReader")]
public async Task<IActionResult> UploadMultipartUsingReader()
{
    var boundary = GetBoundary(Request.ContentType);
    var reader = new MultipartReader(boundary, Request.Body, 80 * 1024);

    var valuesByKey = new Dictionary<string, string>();
    MultipartSection section;

    while ((section = await reader.ReadNextSectionAsync()) != null)
    {
        var contentDispo = section.GetContentDispositionHeader();

        if (contentDispo.IsFileDisposition())
       {
            var fileSection = section.AsFileSection();
            var bufferSize = 32 * 1024;
            await Helpers.ReadStream(fileSection.FileStream, bufferSize);
        }
        else if (contentDispo.IsFormDisposition())
        {
            var formSection = section.AsFormDataSection();
            var value = await formSection.GetValueAsync();
            valuesByKey.Add(formSection.Name, value);
        }
    }

    return Ok();
}

private static string GetBoundary(string contentType)
{
    if (contentType == null)
        throw new ArgumentNullException(nameof(contentType));

    var elements = contentType.Split(' ');
    var element = elements.First(entry => entry.StartsWith("boundary="));
    var boundary = element.Substring("boundary=".Length);

    boundary = HeaderUtilities.RemoveQuotes(boundary);

    return boundary;
}

Uploading data via HTTPS

In this use case we will upload 20 MB using different storages (memory vs file system) and different schemes (http vs https).

The code for uploading data:

var stream = readFromFs
    ? (Stream) File.OpenRead(filePath)
    : new MemoryStream(bytes);

var bufferSize = 4 * 1024; // default

using (var content = new StreamContent(stream, bufferSize))
{
    using (var response = await client.PostAsync("Upload", content))
    {
        response.EnsureSuccessStatusCode();
    }
}

Here are the throughput numbers:

  • HTTP + Memory: 450 MB/s
  • HTTP + File System: 110 MB
  • HTTPS + Memory: 300 MB/s
  • HTTPS + File System: 23 MB/s

Sure, the file system is not as fast as the memory but my SSD is not that slow to get just 23 MB/s .... let's increase the buffer size instead of using the default value of 4 KB.

  • HTTPS + Memory + 64 KB: 300 MB/s
  • HTTPS + File System + 64 KB: 200 MB/s
  • HTTPS + File System + 128 KB: 250 MB/s

With bigger buffer size we get huge improvements when reading from slow storages like the file system.

Another hint: Setting the Content-Length on the client yields better overall performance.

Summary

When I startet to work on the performance issues my first thought was that Kestrel is to blame because it had not enough time to mature yet.  I even tried to place IIS in front of Kestrel so that IIS is responsible for HTTPS stuff and Kestrel for the rest. The improvements are not worth of mentioning. After adding a bunch of trace logs, measuring time on the client and server, switching between schemes and storages I realized that the (mature) HttpClient is causing issues as well and one of the major problem were the default values like the buffer size.

 


Entity Framework Core Migrations: Assembly Version Mismatch

If you have switched your .NET Core project from xproj to csproj (MSBuild) and updated the nuget packages then you may run into an issue when executing some of the dotnet ef-commands.

I got the following error after executing dotnet ef migrations list:

Could not load file or assembly 'Microsoft.EntityFrameworkCore, Version=1.1.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60' or one of its dependencies. The located assembly's manifest definition does not match the assembly reference. (Exception from HRESULT: 0x80131040)

The problem is that some of my (3rd party) dependencies are using version 1.1.0 and the others version 1.1.1. In a classic .NET 4.6 project we use assembly redirects to solve this kind of problems and the in .NET Core we do the same ...

Just create an app.config file with the following content:

<?xml version="1.0" encoding="utf-8"?>

<configuration>
    <runtime>
        <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
            <dependentAssembly>
                <assemblyIdentity name="Microsoft.EntityFrameworkCore" culture="neutral" publicKeyToken="adb9793829ddae60" />
                <bindingRedirect oldVersion="0.0.0.0-1.1.1.0" newVersion="1.1.1.0" />"
            </dependentAssembly>
            <dependentAssembly>
                <assemblyIdentity name="Microsoft.EntityFrameworkCore.Relational" culture="neutral" publicKeyToken="adb9793829ddae60" />
                <bindingRedirect oldVersion="0.0.0.0-1.1.1.0" newVersion="1.1.1.0" />"
            </dependentAssembly>
            <dependentAssembly>
                <assemblyIdentity name="Microsoft.Extensions.Logging.Abstractions" culture="neutral" publicKeyToken="adb9793829ddae60" />
                <bindingRedirect oldVersion="0.0.0.0-1.1.1.0" newVersion="1.1.1.0" />"
            </dependentAssembly>
        </assemblyBinding>
    </runtime>
</configuration>

If you still getting errors than make sure you have the following items in your csproj-file

<ItemGroup>
    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="1.1.0">
        <PrivateAssets>All</PrivateAssets>
    </PackageReference>
    <DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="1.0.0" />
</ItemGroup>

 


Strongly-typed Configuration for .NET Core - with full Dependency Injection support

Configuration is one of the most prominent cornerstones in software systems, and especially in distributed systems. And it has been a point for discussions in .NET for quite some time.

In one of our projects we have built a solution that lets different applications in different companies exchange data, although being behind firewalls, using the open source Relay Server. But, to our surprise, one of the features our customer was amazed about was the library I've developed to make configuration easier to handle.

In Thinktecture.Configuration I've taken over the ideas, generalized them and added new features.

The basic idea is that .NET developers should be able to deal with configuration data by just using arbitrary classes.

The library consists of 3 main components: IConfigurationLoader, IConfigurationProvider and IConfigurationSelector. The IConfigurationLoader loads data from storage, e.g. JSON from the file system. The IConfigurationProvider uses IConfigurationSelector to select the correct piece of data like a JSON property and to convert this data to requested configuration.

Architecture

In short, the features of the lib are:

  • The configuration (i.e. the type being used by the application)
    • should be type-safe
    • can be an abstract type (e.g. an interface)
    • don't have to have property setters or other kind of metadata just to be deserializable
    • can be and have properties that are virtually as complex as needed
    • can be injected via DI (dependency injection)
    • can have dependencies that are injected via DI (i.e. via constructor injection)
    • can be changed by "overrides"
  • The usage of the configuration in a developer's code base should be free of any stuff from the configuration library
  • The extension of a configuration by new properties or changing the structure of it should be a one-liner

Use Cases

In this post I'm going to show the capabilities of the library by illustrating it with a few examples. In this concrete example I'am using a JSON file containing the configuration values (with Newtosonf.Json in the background) and Autofac for DI.

But the library is not limited to these. The hints are at the end of this post if you want to use a different DI framework, other storage than the file system (i.e. JSON files) or not use JSON altogether.

The first use case is a bit lengthy to explain the basics. The others will just point out specific features.

Want to see the code? Go to Thinktecture.Configuration

Nuget: Install-Package Thinktecture.Configuration.JsonFile.Autofac

1. One file containing one or more configurations

Shown features in this example:

  • one JSON file
  • multiple configurations
  • a configuration doesn't have to be on the root of the JSON file
  • a configuration has dependencies known by DI
  • a configuration gets injected in a component like any other dependency

We start with 2 simple configuration types.

public interface IMyConfiguration
{
    string Value { get; }
}

public interface IOtherConfiguration
{
    TimeSpan Value { get; }  
}

The configuration IMyConfiguration is required by our component MyComponent.

public class MyComponent
{
    public MyComponent(IMyConfiguration config)
    {
    }
}

 Configuration file configuration.json

{
    "My":
    {
        "Config": { value: "content" }
    },
    "OtherConfig": { value: "00:00:05" }
}

Now let's setup the code in the executing assembly and configure DI to make MyComponent resolvable along with IMyConfiguration .

var builder = new ContainerBuilder();
builder.RegisterType<MyComponent>().AsSelf();

// IFile is required by JsonFileConfigurationLoader to access the file system
// For more info: https://www.nuget.org/packages/Thinktecture.IO.FileSystem.Abstractions/
builder.RegisterType<FileAdapter>().As<IFile>().SingleInstance();

// register so-called configuration provider that operates on "configuration.json"
builder.RegisterJsonFileConfigurationProvider("./configuration.json");

// register the configuration.
// "My.Config" is the (optional) path into the config JSON structure because our example configuration does not start at the root of the JSON
builder.RegisterJsonFileConfiguration<MyConfiguration>("My.Config")
    .AsImplementedInterfaces() // i.e. as IMyConfiguration
    .SingleInstance(); // The values won't change in my example

// same difference with IOtherConfiguration
builder.RegisterJsonFileConfiguration<OtherConfiguration>("OtherConfig")
    .AsImplementedInterfaces();

var container = builder.Build();

 The concrete types MyConfiguration and OtherConfiguration are, as often when working with abstractions, used with DI only. Apart from that, these types won't show up at any other places. The type MyConfiguration has a dependency IFile that gets injected during deserialization.

public class MyConfiguration : IMyConfiguration
{
    public string Value { get; set; }

    public MyConfiguration(IFile file)
    {
        ...
    }
}

public class OtherConfiguration : IOtherConfiguration
{
    public TimeSpan Value { get; set; }  
}

 The usage is nothing special

// IMyConfiguration gets injected into MyComponent
var component = container.Resolve<MyComponent>();

// we can resolve IMyConfiguration directly if we want to
var config = container.Resolve<IMyConfiguration>();

2. Nesting

Shown features in this use case:

  • one of the properties of a configuration type is a complex type
  • complex property type can be instantiated by Newtonsoft.Json or DI
  • complex property can be resolved directly if required

In this example IMyConfiguration has a property that is not of a simple type. The concrete types implementing IMyConfiguration and IMyClass consist of property getters and setters only thus left out for brevity.

public interface IMyConfiguration
{
    string Value { get; }
    IMyClass MyClassValue { get; }
}

public interface IMyClass
{
    int Value { get; }  
}

The JSON file looks as following:

{
    "Value": "content",
    "MyClassValue": { "Value": 42 }
}

Having a complex property we can decide whether the type IMyClass is going to be instantiated by Newtonsoft.Json or DI.

With just the following line the type IMyClass is not introduced to the configuration library and is going to be instantiated by Newtonsoft.Json.

builder.RegisterJsonFileConfiguration<MyConfiguration>()
    .AsImplementedInterfaces()
    .SingleInstance();

With the following line we introduce the type to the config lib and DI but the instance of IMyClass cannot be resolved directly.

builder.RegisterJsonFileConfigurationType<MyClass>();

Should IMyClass be resolvable directly then we can use the instance created along with IMyConfiguration or let new instance be created.

// option 1: use the property of IMyConfiguration
builder.Register(context => context.Resolve<IMyConfiguration>().MyClassValue)
    .AsImplementedInterfaces()
    .SingleInstance();

// option 2: let create a new instance
builder.RegisterJsonFileConfiguration<MyClass>("MyClassValue")
    .AsImplementedInterfaces()
    .SingleInstance();

 3. Multiple JSON files

The configurations can be loaded from more than one file.

Configuration types are

public interface IMyConfiguration
{
    string Value { get; }
}

public interface IOtherConfiguration
{
    TimeSpan Value { get; }  
}

File myConfiguration.json

{
    "Value": "content"
}

File otherConfiguration.json

{
    "Value": "00:00:05"
}

Having two files we need a means to distinguish between them when registering the configurations. In this case we use RegisterKeyedJsonFileConfigurationProvider that returns a key that will be passed to RegisterJsonFileConfiguration.

var providerKey = builder.RegisterKeyedJsonFileConfigurationProvider("myConfiguration.json");
builder.RegisterJsonFileConfiguration<MyConfiguration>(providerKey)
    .AsImplementedInterfaces()
    .SingleInstance();

var otherKey = builder.RegisterKeyedJsonFileConfigurationProvider("otherConfiguration.json");
builder.RegisterJsonFileConfiguration<OtherConfiguration>(otherKey)
    .AsImplementedInterfaces()
    .SingleInstance();

4. Overrides

A configuration can be assembled from one base configuration and one or more overrides.

In this case we have two config files. One containing the default values of the configuration and the other containing values to override.

Default values come from baseConfiguration.json

{
    "Value":
    {
        "InnerValue_1": 1,
        "InnerValue_2": 2
    }
}

InnerValue_2 will be changed by the overrides.json

{
    "Value":
    {
        "InnerValue_2": 3
    }
}

The configuration 

public interface IMyConfiguration
{
    IInnerConfiguration Value { get; }  
}

public interface IInnerConfiguration
{
    int InnerValue_1 { get; }
    int InnerValue_2 { get; }
}

To specify overrides we need to provide more than one file path when registering the configuration provider. The overrides are applied in the same order they passed to RegisterJsonFileConfigurationProvider.

builder.RegisterJsonFileConfigurationProvider("baseConfiguration.json", "overrides.json");
builder.RegisterJsonFileConfiguration<MyConfiguration>()
    .AsImplementedInterfaces()
    .SingleInstance();

5. Extension of the configuration

Let's add a property to IInnerConfiguration from previous paragraph.

public interface IInnerConfiguration
{
    int InnerValue_1 { get; }
    int InnerValue_2 { get; }
    string NewValue { get; }
}

Add the corresponding property to the JSON file baseConfiguration.json

{
    "Value":
    {
        "InnerValue_1": 1,
        "InnerValue_2": 2,
        "NewValue": "content"
    }
}

That's it.

Working with different frameworks, storages and data models

Using another DI framework

To use a different DI framwork than Autofac use the package Thinktecture.Configuration.JsonFile instead of Thinktecture.Configuration.JsonFile.Autofac and implement the interface IJsonTokenConverter using your favorite DI framework.  The converter has just one method TConfiguration Convert<TConfiguration>(JToken token).

Load JSON from other media

To load JToken from other storages than the file system just implement the interface IConfigurationLoader<JToken,JToken>. For example, if the JSON configuration are in a database then inject the database context or a data access layer and select corresponding data rows.

Use different data models

If you are using other data model than JSON then reference the package Thinktecture.Configuration and implement the interfaces IConfigurationLoader<TRawDataIn,TRawDataOut>, IConfigurationProvider<TRawDataIn,TRawDataOut> and IConfigurationSelector<TRawDataIn,TRawDataOut>. It sounds like much but if you look into the code of the corresponding JSON-based classes you will see that the classes are pretty small and trivial.

Some final words...

Although the configuration is an important part of the software development it is not the most exciting one. Therefore, a software developer may be inclined to take shortcuts and work with meaningful hardcoded values. Thinktecture.Configuration gives you the means to work with .NET types without thinking too much how to load and parse the values. This saves time, improves the reusability of the components and the software architecture.