Quantcast
Channel: Josef Ottosson
Viewing all 244 articles
Browse latest View live

AWS Lambda + Python + PIP Packages = true

$
0
0

Intro

I'm in the process of moving all my stuff from Azure to AWS. One of the things I currently run in Azure is a twitter bot that tweets every hour (it even made the local news a couple of years ago...).
Currently it's beeing triggered by a cron job on a VM. It has worked perfectly fine for 6 years now, but since I'm moving stuff and don't wan't to maintain a VM anymore, I decided to "convert it" to run in a AWS Lambda instead.

Solution

The bot is written in Python and has two pip dependencies, twitter and pytz.

Code changes

Before the code looked liked this:
tweet.py

from twitter import OAuth, Twitter
from datetime import datetime, time
import pytz, random

TOKEN = "SECRET"
TOKEN_KEY = "SECRET"
API_KEY = "SECRET"
API_SECRET = "SECRET"

MY_TZ = pytz.timezone('Europe/Stockholm')
now = datetime.now(MY_TZ).time()
retries = 0
def get_current_hour(timestamp):
    if timestamp.hour == 0 or timestamp.hour == 12:
        return 12
    return timestamp.hour % 12

def compose_tweet(timestamp):
    number_of_rings = get_current_hour(timestamp)
    # Creates a tweet, e.g. "BONG BONG BONG #03:00
    alert_sound = get_bell_sound(retries)
    tweet = " ".join([alert_sound] * number_of_rings)
    hashtag = "#%s:%s" %(str(timestamp.hour).zfill(2), str(timestamp.minute).zfill(2))
    return "%s %s" %(tweet, hashtag)

def send_tweet(tweet):
    global retries
    auth = OAuth(TOKEN, TOKEN_KEY, API_KEY, API_SECRET)
    t = Twitter(auth=auth)
    try:
        t.statuses.update(status=tweet)
        retries = 0
    except:
        retries += 1
        if retries <= 7:
            main()
        else:
            raise

def get_bell_sound(index):
    sounds = ('BONG', 'DONG', 'DING', 'BING-BONG', 'RING', 'PING', 'JINGLE', 'DING-DONG')
    return sounds[index]

def main(timestamp = now):
    send_tweet(compose_tweet(timestamp))

if __name__ == "__main__":
    main()

To make it "Lambda compatible", I only needed to add the following method

def lambda_handler(event, context):
    main()

The lambda_handler function is called by the lambda runtime. You get access to both the event that triggered the function and also a context. I don't need any of it though.

Packaging

Here the documentation lacked a little when it comes to how to deploy a python function with external (pip) dependencies. It wasn't that hard to figure out but it took me like 30 minutes that I could have spent on something else, so that's why I'm writing this post :).

Lambda supports uploading a zip file, so let's create one. It needs to contain the pip packages and the entrypoint, in our case the entrypoint is tweet.py.

The easiest way dealing with PIP imo is to have a requirements.txt file, so let's create one.

requirements.txt

twitter==1.18.0
pytz==2018.5

Here we have specified which packages and what version to use. I use quite old packages, I don't know if newer versions work and I don't have time to try it out either, so bare with me.

After creating a requirements.txt, we can install the packages to the current folder like this:

pip install -r requirements.txt -t .

This will create the following folder structure:
folderstructure

Now the only thing left to do is create the zip file.
I added the following files/folders to the zip file:
zipfile
Then I uploaded it by choosing Upload a .zip file.
upload-aws
The UI will now look like this:
aws-uploaded
Here I've also specified the entrypoint for the function by entering tweet.lambda_handler in the Handler input field (tweet is the name of the file and lambda_handler is the name of the method).

Scheduling/triggering

Now the only thing that's left is to make the function run every hour (08:00, 09:00 and so on).
It's really easy, all we need to do is add a CloudWatch Event Trigger with the following schedule expression: cron(0 * * * ? *).

That's it, we're now tweeting from the (aws) cloud!


C# - Sealed and internal - how to use in unit test?

$
0
0

The problem

I'm using the Firebase Admin dotnet sdk to send push notifications. They have the following method that allows you to send multiple notifications with one request:

public async Task<BatchResponse> SendAllAsync(IEnumerable<Message> messages)
{
    return await this.SendAllAsync(messages, false).ConfigureAwait(false);
}

The SendAllAsync method returns a Batchresponse that will tell you how many notifications that were sent successfully and how many that failed. Usually when a notification fails to deliver it's because of the token used has expired or been revoked.
Whenever I receive a response that tells me that the token is invalid I want to remove that token from the database so I don't end up with a bunch of expired/invalid push tokens.

The logic is quite simple

var response = await _firebaseMessaging.SendAllAsync(messages);
return HandleResponse(response);

I then have the following code that acts on the response:

public async Task<Result> HandleResponse(Batchresponse response)
{
    if (response.SuccessCount > 0 && response.FailureCount == 0)
    {
        return Result.Ok();
    }
    
    if (response.FailureCount > 0)
    {
       var invalidTokens = ExtractInvalidTokens(response);
       await _removeTokensCommand(invalidTokens);
       return Result.Fail();
    }
    ...
}

You get the idea, if we have any failed tokens, remove them.
So, seems quite easy to test this, right?

Well, turns out that BatchResponse looks like this:

public sealed class BatchResponse
{
    internal BatchResponse(IEnumerable<SendResponse> responses)
    {
        // some irrelevant code here
    }
    
    public IReadOnlyList<SendResponse> Responses { get; }
    public int SuccessCount { get; }
    public int FailureCount => this.Responses.Count - this.SuccessCount;
}

It's sealed and the constructor is internal, meaning we can not create a new instance of it like this:

var batchResponse = new BatchResponse(new List<SendResponse>());

What to do?

The solution

Reflection to the rescue.

By using reflection it's possible to do...basically whatever you want, you can set private fields, you can call methods that are not public and so on.

It goes without saying but you should probably not rely on what I'm about to show you in your production code. Classes are (almost) always sealed for a good reason, same goes with internal constructors, the developers clearly don't want you to create instances of their classes (for whatever reason).

One simple solution in my case would be to change the signature of my HandleResponse method to the following...

Task<Result> HandleResponse(int successCount, int failureCount)

...or just map it to some domain object that I control.

But, HOW FUN IS THAT?

This is what I came up with:

public async Task GivenOneSuccessAndZeroFailures_WhenHandleResponse_ThenReturnsOkResult()
{
    var sendResponse = CreateInstance<SendResponse>(
        BindingFlags.NonPublic | BindingFlags.Instance,
        new[] { typeof(string) },
        new object[] {"foo"});
    var batchResponse = CreateInstance<BatchResponse>(
        BindingFlags.NonPublic | BindingFlags.Instance,
        new[] { typeof(IEnumerable<SendResponse>) },
        new object[] { new List<SendResponse>{ sendResponse }});
        
    var result = await sut.HandleResponse(batchResponse);
    
    result.Success.ShouldBeTrue();
}

private static T CreateInstance<T>(BindingFlags bindingFlags, Type[] types, object[] ctorParams)
{
    var type = typeof(T);
    var constructor = type.GetConstructor(
        bindingFlags,
        null,
        types,
        Array.Empty<ParameterModifier>()) ?? throw new Exception("This didn't work...");
        
    return (T)constructor.Invoke(ctorParams);
}
  1. Get the relevant constructor
  2. Invoke the constructor
  3. Cast the result to T
  4. BOOM, we've just created an instance of a sealed class without any public constructors.

Customizing PropertyHandlers in JOS.ContentSerializer

$
0
0

The problem

Imagine the following ContentType:

public class PostPage : PageData
{
    ....
    [Display(Order = 400)]
    [CultureSpecific]
    [AllowedTypes(typeof(AuthorBlock))]
    public virtual ContentReference Author { get; set; }
    ...
}

As you can see, it's only possible to select AuthorBlocks in the Author-property.
The default implementation of ContentReferencePropertyHandler looks like this:

public class ContentReferencePropertyHandler : IPropertyHandler<ContentReference>
{
    private readonly IUrlHelper _urlHelper;
    private readonly IContentSerializerSettings _contentSerializerSettings;

    public ContentReferencePropertyHandler(IUrlHelper urlHelper, IContentSerializerSettings contentSerializerSettings)
    {
        _urlHelper = urlHelper;
        _contentSerializerSettings = contentSerializerSettings ?? throw new ArgumentNullException(nameof(contentSerializerSettings));
    }

    public object Handle(ContentReference contentReference, PropertyInfo propertyInfo, IContentData contentData)
    {
        if (contentReference == null || contentReference == ContentReference.EmptyReference)
        {
            return null;
        }

        var url = new Uri(this._urlHelper.ContentUrl(contentReference, this._contentSerializerSettings.UrlSettings));

        if (this._contentSerializerSettings.UrlSettings.UseAbsoluteUrls && url.IsAbsoluteUri)
        {
            return url.AbsoluteUri;
        }

        return url.PathAndQuery;
    }
}

What will happen when we run .ToJson?
Well, since it's impossible to link directly to a block, the output will look like this:

{
    ...
    "author": "http://localhost:54321/"
    ...
}

That's not nice, it would be better if we could display the actual block instead.

The solution

Simply create a new class implementing the IPropertyHandler<ContentReference> interface and then register it in the DI container.

Improved ContentReferenceHandler

public class CustomContentReferencePropertyHandler : IPropertyHandler<ContentReference>
{
    private readonly IUrlHelper _urlHelper;
    private readonly IContentSerializerSettings _contentSerializerSettings;
    private readonly IContentLoader _contentLoader;
    private readonly IPropertyHandler<BlockData> _blockDataPropertyHandler;

    public CustomContentReferencePropertyHandler(
        IUrlHelper urlHelper,
        IContentSerializerSettings contentSerializerSettings,
        IContentLoader contentLoader,
        IPropertyHandler<BlockData> blockDataPropertyHandler)
    {
        _urlHelper = urlHelper ?? throw new ArgumentNullException(nameof(urlHelper));
        _contentSerializerSettings = contentSerializerSettings ?? throw new ArgumentNullException(nameof(contentSerializerSettings));
        _contentLoader = contentLoader ?? throw new ArgumentNullException(nameof(contentLoader));
        _blockDataPropertyHandler = blockDataPropertyHandler ?? throw new ArgumentNullException(nameof(blockDataPropertyHandler));
    }

    public object Handle(ContentReference contentReference, PropertyInfo property, IContentData contentData)
    {
        if (contentReference == null || contentReference == ContentReference.EmptyReference)
        {
            return null;
        }

        if (IsReferenceToBlock(property))
        {
            if (this._contentLoader.TryGet<BlockData>(contentReference, out var blockData))
            {
                return this._blockDataPropertyHandler.Handle(blockData, property, contentData);
            }
        }

        var url = new Uri(this._urlHelper.ContentUrl(contentReference, this._contentSerializerSettings.UrlSettings));

        if (this._contentSerializerSettings.UrlSettings.UseAbsoluteUrls && url.IsAbsoluteUri)
        {
            return url.AbsoluteUri;
        }

        return url.PathAndQuery;
    }

    private static bool IsReferenceToBlock(MemberInfo property)
    {
        var allowedTypesAttribute = property.GetCustomAttribute<AllowedTypesAttribute>();

        if (allowedTypesAttribute?.AllowedTypes == null || !allowedTypesAttribute.AllowedTypes.Any())
        {
            return false;
        }

        return allowedTypesAttribute.AllowedTypes.All(x => typeof(BlockData).IsAssignableFrom(x));
    }
}

Replace the default implementation

Replace for all properties of type ContentReference

[InitializableModule]
[ModuleDependency(typeof(JOS.ContentSerializer.Internal.ContentSerializerInitalizationModule))]
public class ContentSerializerInitializationModule : IConfigurableModule
{
    public void Initialize(InitializationEngine context) {}

    public void Uninitialize(InitializationEngine context) {}

    public void ConfigureContainer(ServiceConfigurationContext context)
    {
        context.Services.RemoveAll<IPropertyHandler<ContentReference>>();
        context.Services.AddSingleton<IPropertyHandler<ContentReference>, CustomContentReferencePropertyHandler>();
    }
}

Replace for specific property

[Display(Order = 400)]
[CultureSpecific]
[AllowedTypes(typeof(AuthorBlock))]
[ContentSerializerPropertyHandler(typeof(CustomContentReferencePropertyHandler))]
public virtual ContentReference Author { get; set; }

Swagger - Tips and Tricks - Part 2

$
0
0

Exclude certain properties from the Swagger UI

The problem

Sometimes you have some properties on your request model that you don't want to show in the Swagger UI, for whatever reason.

The solution

I'm using Swashbuckle 5.0.
Start by creating the following attribute.
SwaggerIgnorePropertyAttribute.cs

[AttributeUsage(AttributeTargets.Property)]
public class SwaggerIgnorePropertyAttribute : Attribute
{
}

Next, we need to create a new SchemaFilter. It will get all properties for the given type from the SchemaFilterContext, only selecting the ones with the SwaggerIgnorePropertyAttribute.

SwaggerExcludePropertySchemaFilter.cs

public class SwaggerExcludePropertySchemaFilter : ISchemaFilter
{
    public void Apply(OpenApiSchema schema, SchemaFilterContext context)
    {
        if (schema?.Properties == null)
        {
            return;
        }

        var excludedProperties = context.Type.GetProperties().Where(t => t.GetCustomAttribute<SwaggerIgnorePropertyAttribute>() != null);

        foreach (var excludedProperty in excludedProperties)
        {
            var propertyToRemove = schema.Properties.Keys.SingleOrDefault(x => string.Equals(x, excludedProperty.Name, StringComparison.OrdinalIgnoreCase));

            if (propertyToRemove != null)
            {
                schema.Properties.Remove(propertyToRemove);
            }
        }
    }
}

Then we need to register the SchemaFilter like this:

services.AddSwaggerGen(c =>
{
    .....
    c.SchemaFilter<SwaggerExcludePropertySchemaFilter>();
    ....
}

Demo

Given the following OrderInput, the Swagger UI will show two properties, Name and Optional (really good name, right?).

public class OrderInput
{
    public string Name { get; set; }
    public IDictionary<string, object> Optional { get; set; }   
}

orders-before

We wan't to hide the Optional property so we add the SwaggerIgnoreProperty attribute like this...

public class OrderInput
{
    public string Name { get; set; }
    [SwaggerIgnoreProperty]
    public IDictionary<string, object> Optional { get; set; }   
}

orders-after

...and now it does not show up anymore.

This solution is inspired by this answer by Richard on Stack Overflow. I've updated it to work with the latest Swashbuckle release.

How to configure Episerver to use Active Directory

$
0
0

This is a quick guide showing how to configure Episerver to use Active Directory instead of Multiplexing/WindowsProvider. This is NOT showing how to use Azure AD.
Im testing this on a new Alloy site running Episerver 11.3.1.

NOTE, while this is a quick way of adding Active Directory support, I still recommend using ADFS or something similar instead.

Some good resources if you want to read more about this topic:

Active Directory

My AD setup.

  • Domain: local.josef.guru
  • AD Service account: jos-ad-service@local.josef.guru, this is a normal user account responsible for connecting to the AD, used in the Membership/RoleProvider section in Web.config.
  • AD user in correct OU. In my case I will name my user josefweb and the OU will be Web
  • AD Groups named CmsAdmins and CmsEditors(you can name these groups whatever you want).

Here's an image of how my AD is setup, it's pretty standard, for this guide I've added a new OU(Organizational Units) named Web where I will add all users who will be able to access the Edit interface.

Adding new user to AD

The users also needs to be a member of the correct groups to be able to login. I will add my user to the group CmsAdmins which means that this user will be able to do everything in the cms/admin(because we will map this role to the virtual role CmsAdmins further down).

Adding correct A group to the user

Web.Config

Membership/roleprovider

Change the <membership> section to the following

<membership defaultProvider="ActiveDirectoryMembershipProvider" userIsOnlineTimeWindow="10" hashAlgorithmType="HMACSHA512">
    <providers>
      <clear />
      <add name="ActiveDirectoryMembershipProvider"
          type="System.Web.Security.ActiveDirectoryMembershipProvider, System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
          connectionStringName="ActiveDirectoryProviderConnection"
          connectionUsername="jos-ad-service@local.josef.guru"
          connectionPassword="mypassword"
          enableSearchMethods="true"
          attributeMapUsername="sAMAccountName" />
    </providers>
</membership>

Change the <rolemanager> section to the following

<roleManager enabled="true" defaultProvider="ActiveDirectoryRoleProvider" cacheRolesInCookie="true">
    <providers>
        <clear />
        <add name="ActiveDirectoryRoleProvider"
         type="EPiServer.Security.ActiveDirectoryRoleProvider, EPiServer.Cms.AspNet, Version=11.3.1.0, Culture=neutral, PublicKeyToken=8fe83dea738b45b7"
         connectionStringName="ActiveDirectoryProviderConnection"
         connectionUsername="jos-ad-service@local.josef.guru"
         connectionPassword="mypassword"
         attributeMapUsername="sAMAccountName" />
    </providers>
</roleManager>

Connectionstring

Add a new connectionstring, ActiveDirectoryProviderConnection

<connectionStrings>
    <add name="ActiveDirectoryProviderConnection" connectionString="LDAP://local.josef.guru/OU=Web,DC=local,DC=josef,DC=guru" />
    ....
</connectionStrings>

If you are unsure about how your LDAP connectionstring should look, read this.

Virtual roles

Map the virtual roles, we want to map our AD groups CmsAdmins/CmsEditors to the virtual roles CmsAdmins and CmsEditors, this is achieved by populating the roles attribute. I've also removed the default WebAdmins, WebEditors and Administrators roles.

<virtualRoles addClaims="true">
    <providers>
        ...
        <add name="CmsAdmins" type="EPiServer.Security.MappedRole, EPiServer.Framework" roles="CmsAdmins" mode="Any" />
        <add name="CmsEditors" type="EPiServer.Security.MappedRole, EPiServer.Framework" roles="CmsAdmins, CmsEditors" mode="Any" />
        ...
    </providers>
</virtualRoles>

Location sections

You will need to edit the authorization section under the following locations:

  • EPiServer
  • EPiServer/CMS/admin

It should look like this <allow roles="CmsAdmins, CmsEditors" />

Example:

<location path="EPiServer">
    <system.web>
        ...     
        <authorization>
            <allow roles="CmsAdmins, CmsEditors" />
            <deny users="*" />
        </authorization>
        ...

You should now be able to login!

Login with AD credentials

Beware of Enum.TryParse

$
0
0

What do you think Enum.TryParse will return when running this code?

public enum ContactMethod
{
   Unknown = 0,
   Email = 1,
   Phone = 2
}

var result = Enum.TryParse("10", out ContactMethod contactMethod);

"It will return false of course, 10 is not a valid value!!"

Wrong.
It will return true. And even worse, the output variable contactMethod will have the value of...10!

Don't believe me? See it for yourself (dotnetfiddle)

Now, imagine that some developer wrote the following program:

public class Program
{
    public static void Main(string args[])
    {
        var result = Enum.TryParse(args[0], out NukeStatus nukeStatus);
        FireNuke((int)nukeStatus);
    }
    
    public static void FireNuke(int status)
    {
        if(status == 0)
        {
           return;
        }
    
        if(status > 0 && status <= 10)
        {
           Console.WriteLine("TEST FIRING");
           TestFire();
        }
    
        if(status >= 15)
        {
           Console.WriteLine("NUKE EM ALL!");
           NukeEmAll();
        }
    }
}

public enum NukeStatus
{
   Idle = 0,
   TestFireOneMissile = 5,
   TestFireAllMissiles = 10,
   FireOneMissile = 15,
   FireAllMissiles = 20
}

Now imagine that someone with fat fingers should do a test run and slips on the keyboard, so instead of passing in 10, 100 will be passed in instead.

dotnet run NukeProgram 100

BOOM

Now, I know that my example is really stupid and the code is really bad, but still, it could happen!

What to use instead of Enum.TryParse then?

Note, this is only a problem when you try to pass in numeric values to TryParse.
If you want to be sure that your (int)value really exists in the Enum, you could use Enum.IsDefined instead.
Something like this:

var nukeStatusAsString = "100";
var myNukeStatus = int.Parse(nukeStatusAsString); // Yeah yeah, error checking I know.
var isDefined = Enum.IsDefined(typeof(NukeStatus), myNukeStatus);
if (!isDefined)
{
    return NukeStatus.Idle;
}
return (NukeStatus)myNukeStatus;

You can read more about this here (Stackoverflow) and here (Microsoft).

Headless Episerver? Meet JOS.Epi.ContentApi

$
0
0
Headless Episerver? Meet JOS.Epi.ContentApi

I read this blog post by Mathias Kunto where he says that he will leave my beloved library behind and start using the new Episerver Headless API instead.

I felt like the picture above :(

I couldn't stand it. So, I decided to create a new library; meet JOS.Epi.ContentApi..
JOS.Epi.ContentApi uses JOS.ContentSerializer by default(this can be changed by swapping out the IContentApiSerializer) so you don't need to have Episerver Find to use it.

I haven't had the time to try out the new Episerver version so I don't know how well it works, but I noticed this quote from Mathias:

I had a quick look at the API functionality, and it seems like you get pretty JSON requesting URLs like /api/episerver/v1.0/content/3 and so on. However, what I really wanted was friendly URLs delivering JSON for the page in question. For instance, requesting /en/alloy-plan/download-alloy-plan/start-downloading/ would give me JSON for the Start Downloading page.

I don't like that you must know the ContentReference to fetch the JSON data, it would be better if it worked like Mathias wants it to work.

I've built that.

How to use it

  1. Install-Package Jos.Epi.ContentApi(Normal nuget, not episerver feed)
  2. Set your accept header to "application/json" and make a GET request to your desired page.
  3. Profit.

Note, by default(this can be changed) the library will only serialize the response if the Accept header contains ONE value, not two, not empty, one.
If you want to change this behaviour, just swap out the IShouldSerializeResponseStrategy interface.

You can also customize when/if the serialization should take place in the same method.

Demo

This is version 1, I've already started working on filtering and stuff like that, stay tuned!

As always, the code can be found on Github.

JOS.Epi.ContentApi updated to version 3.0.0

$
0
0

Thanks to the discussion in the comments on the blogpost written by Johan Björnfot I could simplify the code a lot and remove the HttpModule.

Version 3.0.0 is now available on the nuget feed.

It's a major version bump since I removed the HttpModule, you will need to remove the ContentApiModule entry in your web.config to make it work.

Remove the following entry

<system.webServer>	
  <modules>
    .....
    <add name="ContentApiModule" type="JOS.Epi.ContentApi.ContentApiModule, JOS.Epi.ContentApi" />
    ....
  </modules>	
</system.webServer>

Get dot-notation of C# property/member

$
0
0

The problem

Validate incoming API calls and return nice errors if some property failed to validate.

Example:

public class AuthorInput
{
    public NameInput Name { get; set; }
}

public class NameInput
{
    public string FirstName { get; set; }
    public string SurName { get;set; }
}

I've removed all validation logic from the above class, but FirstName and SurName are required.

So if someone would post the following body...


{
    "name": {
        "surName": "Ottosson"
    }
}

...the API should return something like this:


{
    "errors": [
        {
            "type": "VALIDATION_ERROR",
            "message": "Property 'FirstName' is required",
            "path": "Name.FirstName"
        }
    ]
}

How do you create the dot-notation path?

"Solution 1"

if(validationResult.IsFailure)
{
    // Go through all properties
    ....
    var firstNamePropertyPath = "Name.FirstName"
    ...
}

This is obviously not a good solution :)

Solution 2

if(validationResult.IsFailure)
{
    // Go through all properties
    ....
    var firstNamePropertyPath = $"{nameof(Author.Name)}.{nameof(Author.Name.FirstName)}";
    ...
}

Now we get help from the compiler, but it can be a bit ugly when we have several levels of nesting.

Solution 3

if(validationResult.IsFailure)
{
    // Go through all properties
    ....
    var sections = new []
    {
        nameof(Author.Name),
        nameof(Author.Name.FirstName)
    };
    var firstNamePropertyPath = string.Join('.', sections);
    ...
}

Maybe a bit more clear? But still not good.

Solution 4

if(validationResult.IsFailure)
{
    // Go through all properties
    ....
    var firstNamePropertyPath = _propertyPathFactory.Create<Author>(x => x.Name.FirstName);
    ...
}

Now we're talking :)
PropertyPathFactory looks like this

public string Create<T>(Expression<Func<T, object>> pathExpression)
{
    var getMemberNameFunc = new Func<Expression, MemberExpression>(expression => expression as MemberExpression);
    var memberExpression = getMemberNameFunc(pathExpression.Body);
    var names = new Stack<string>();

    while (memberExpression != null)
    {
        names.Push(memberExpression.Member.Name);
        memberExpression = getMemberNameFunc(memberExpression.Expression);
    }

    return string.Join('.', names);
}

FluentValidation does something similar here.

This was just a quick writeup, do you guys know any better way to do it? It would be awesome if you could get the FULL path from nameof(Author.Name.FirstName) and not just the last section.

You're (probably still) using HttpClient wrong and it is destabilizing your software

$
0
0

Required reading before reading this blog post:
You're using HttpClient wrong and it is destabilizing your software

TL;DR

  • Don't use .Result.
  • Stream the response instead of storing the whole response in a string.
  • If you really care about performance, create a custom json deserializer.
  • Reuse HttpClient (or use IHttpClientFactory).

Introduction

All code in this blog post can be found here together with instructions on how to run the benchmarks. This post focuses on dotnet core, the benchmarks are run on dotnet core 3 preview3.

HttpClient is really easy to use, and because of that, it's also really easy to use it wrong. Since it's so easy to use, nobody takes the time to really learn how to use it correctly, my code works, why should I change it? My goal with this post is to show how to use HttpClient in the most efficient way.
What do I mean by the most efficient way?

  • Try to run as fast as possible
  • Try to allocate as little memory as possible

Steve Gordon has a bunch of posts regarding HttpClient, I really recommend that you check out his blog if you want to learn more.

The problem

We want to fetch JSON data from an external API and return a subset of it from a controller.
Our architecture will look like this:
Controller -> IGetAllProjectsQuery -> IGitHubClient.
The Query is responsible for mapping the data from DTOs to "domain models".
Note: We will NOT use the real GitHub API, I've created a API that returns dummy data, I just choose to name it GitHub to make the code more...authentic.

The size of the json response will differ between the benchmarks, we will run the benchmarks 4 times with the following sizes:

  • 10 items ~ 11 KB
  • 100 items ~ 112 KB
  • 1 000 items ~ 1 116 KB
  • 10 000 items ~ 11 134 KB

Error handling

I've intentionally left out all error handling here because I wanted to focus on the code that fetches/deserializes data. I have another post coming up that's full with opinions of how to use HttpClient through out your code base with many different third party integrations together with error handling, Polly and so on, so stay tuned!

Without IHttpClientFactory

This is just to show how one could do this before the IHttpClientFactory existed.
If you don't have access to IHttpClientFactory for whatever reason, look at Version 2 and store the HttpClient as a private field so that it could be reused. Also, don't forget to read the Optimization section!

Version 0

Ah, only a few lines of code, what could POSSIBLY be wrong with this code?
This code creates a new HttpClient in a using statement, calls .Result on GetStringAsync and saves the whole response in a string. It was hard writing this code because it goes against everything I stand for :).

Version0Configurator.cs

public static class Version0Configurator
{
    public static void AddVersion0(this IServiceCollection services)
    {
        services.AddSingleton<GetAllProjectsQuery>();
        services.AddSingleton<GitHubClient>();
    }
}

GitHubClient.cs

public class GitHubClient
{
    public IReadOnlyCollection<GitHubRepositoryDto> GetRepositories()
    {
        using (var httpClient = new HttpClient{BaseAddress = new Uri(GitHubConstants.ApiBaseUrl)})
        {
            var result = httpClient.GetStringAsync(GitHubConstants.RepositoriesPath).Result;
            return JsonConvert.DeserializeObject<List<GitHubRepositoryDto>>(result);
        }
    }
}

Pros

NONE

Cons

  • Using .Result on an asynchronous method. It's never a good idea, never.
    But what if I use .GetAwaiter().GetResult()???
    Nope, still bad. You can read more about common async gotchas/pitfalls/recommendations here. It's written by David Fowler (member of the ASP.NET team), he knows what he's talking about.

  • Creating a new HttpClient for every call in a using statement. HttpClient should not be disposed (well, it should, but not by you, more on that further down where I talk about IHttpClientFactory.

  • Fetching the whole response and storing it as a string, this is obviously bad when working with large response objects, they will end up on the Large object heap if they are larger than 85 000 bytes.

Version 1

We have now wrapped the return type in a Task<> and also added the async keyword. This allows us to await the call to .GetStringAsync.

Version1Configurator.cs

public static class Version1Configurator
{
    public static void AddVersion1(this IServiceCollection services)
    {
        services.AddSingleton<GetAllProjectsQuery>();
        services.AddSingleton<GitHubClient>();
    }
}

GitHubClient.cs

public class GitHubClient : IGitHubClient
{
    public async Task<IReadOnlyCollection<GitHubRepositoryDto>> GetRepositories()
    {
        using (var httpClient = new HttpClient { BaseAddress = new Uri(GitHubConstants.ApiBaseUrl) })
        {
            var result = await httpClient.GetStringAsync(GitHubConstants.RepositoriesPath).ConfigureAwait(false);
            return JsonConvert.DeserializeObject<List<GitHubRepositoryDto>>(result);
        }
    }
}

Pros

  • The request to the API is asynchronous.

Cons

  • Creating a new HttpClient for every call in a using statement.
  • Fetching and storing the whole response in a string.

Version 2

We are now creating a HttpClient in the constructor and then storing it as a field so that we can reuse it. Note, all implementations of the GitHubClients so far are intended to be used/registered as singeltons

Version2Configurator.cs

public static class Version2Configurator
{
    public static void AddVersion2(this IServiceCollection services)
    {
        services.AddSingleton<GetAllProjectsQuery>();
        services.AddSingleton<GitHubClient>();
    }
}

GitHubClient.cs

public class GitHubClient : IGitHubClient
{
    private readonly HttpClient _httpClient;

    public GitHubClient()
    {
        _httpClient = new HttpClient { BaseAddress = new Uri(GitHubConstants.ApiBaseUrl) };
    }

    public async Task<IReadOnlyCollection<GitHubRepositoryDto>> GetRepositories()
    {
        var result = await _httpClient.GetStringAsync(GitHubConstants.RepositoriesPath).ConfigureAwait(false);
        return JsonConvert.DeserializeObject<List<GitHubRepositoryDto>>(result);
    }
}

Pros

  • The request to the API is asynchronous.
  • Reusing the HttpClient

Cons

  • Fetching and storing the whole response in a string.
  • Since we are resuing the HttpClient forever, DNS changes won't be respected. You can read more about that here.

With IHttpClientFactory

As you have seen so far, it's really easy to use HttpClient wrong, here's what Microsoft has to say about it.

The original and well-known HttpClient class can be easily used, but in some cases, it isn't being properly used by many developers.
As a first issue, while this class is disposable, using it with the using statement is not the best choice because even when you dispose HttpClient object, the underlying socket is not immediately released and can cause a serious issue named ‘sockets exhaustion’.

Therefore, HttpClient is intended to be instantiated once and reused throughout the life of an application. Instantiating an HttpClient class for every request will exhaust the number of sockets available under heavy loads. That issue will result in SocketException errors. Possible approaches to solve that problem are based on the creation of the HttpClient object as singleton or static.

But there’s a second issue with HttpClient that you can have when you use it as singleton or static object. In this case, a singleton or static HttpClient doesn't respect DNS changes.
To address those mentioned issues and make the management of HttpClient instances easier, .NET Core 2.1 introduced a new HttpClientFactory...

Version 3

Here, we are injecting the IHttpClientFactory and then using it to create a new HttpClient every time the method gets called.
Yes, we are creating a new HttpClient every time, that's not a bad thing anymore since we are using the IHttpClientFactory.
Straight from Microsoft:

Each time you get an HttpClient object from the IHttpClientFactory, a new instance is returned. But each HttpClient uses an HttpMessageHandler that's pooled and reused by the IHttpClientFactory to reduce resource consumption, as long as the HttpMessageHandler's lifetime hasn't expired.
Pooling of handlers is desirable as each handler typically manages its own underlying HTTP connections; creating more handlers than necessary can result in connection delays. Some handlers also keep connections open indefinitely, which can prevent the handler from reacting to DNS changes.

Version3Configurator.cs

public static class Version3Configurator
{
    public static void AddVersion3(this IServiceCollection services)
    {
        services.AddSingleton<GetAllProjectsQuery>();
        services.AddHttpClient("GitHub", x => { x.BaseAddress = new Uri(GitHubConstants.ApiBaseUrl); });
        services.AddSingleton<GitHubClient>();
    }
}

GitHubClient.cs

public class GitHubClient : IGitHubClient
{
    private readonly IHttpClientFactory _httpClientFactory;

    public GitHubClient(IHttpClientFactory httpClientFactory)
    {
        _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
    }

    public async Task<IReadOnlyCollection<GitHubRepositoryDto>> GetRepositories()
    {
        var httpClient = _httpClientFactory.CreateClient("GitHub");
        var result = await httpClient.GetStringAsync(GitHubConstants.RepositoriesPath).ConfigureAwait(false);
        return JsonConvert.DeserializeObject<List<GitHubRepositoryDto>>(result);
    }
}

Pros

  • Using IHttpClientFactory

Cons

Version 4

Here we are using a typed client instead of a named one. We are registering the typed client with the .AddHttpClient<> method. Note that we also changed the registration of GetAllProjectsQuery from singleton to transient since typed clients are registered as transient.

Version4Configurator.cs

public static class Version4Configurator
{
    public static void AddVersion4(this IServiceCollection services)
    {
        services.AddTransient<GetAllProjectsQuery>();
        services.AddHttpClient<GitHubClient>(x => { x.BaseAddress = new Uri(GitHubConstants.ApiBaseUrl); });
    }
}

GitHubClient.cs

public class GitHubClient : IGitHubClient
{
    private readonly HttpClient _httpClient;

    public GitHubClient(HttpClient httpClient)
    {
        _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
    }

    public async Task<IReadOnlyCollection<GitHubRepositoryDto>> GetRepositories()
    {
        var result = await _httpClient.GetStringAsync(GitHubConstants.RepositoriesPath).ConfigureAwait(false);
        return JsonConvert.DeserializeObject<List<GitHubRepositoryDto>>(result);
    }
}

Pros

  • Using IHttpClientFactory
  • Using a typed client

Cons

  • Needed to change the lifetime of GetAllProjectsQuery from singleton to transient.
  • Fetching and storing the whole response in a string.

Version 5

I really want my GetAllProjectsQuery to be a singleton. To be able to solve this I've created a GitHubClientFactory that returns a GitHubClient. The trick here is that it resolves the GitHubClient from the ServiceProvider, thus injecting all dependencies that we need, no need to new it up ourselfes. Who taught me that? Steve of course.

Version5Configurator.cs

public static class Version5Configurator
{
    public static void AddVersion5(this IServiceCollection services)
    {
        services.AddSingleton<GetAllProjectsQuery>();
        services.AddHttpClient<GitHubClient>(x => { x.BaseAddress = new Uri(GitHubConstants.ApiBaseUrl); });
        services.AddSingleton<GitHubClientFactory>();
    }
}

GitHubClientFactory.cs

public class GitHubClientFactory
{
    private readonly IServiceProvider _serviceProvider;

    public GitHubClientFactory(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public GitHubClient Create()
    {
        return _serviceProvider.GetRequiredService<GitHubClient>();
    }
}
public class GetAllProjectsQuery : IGetAllProjectsQuery
{
    private readonly GitHubClientFactory _gitHubClientFactory;

    public GetAllProjectsQuery(GitHubClientFactory gitHubClientFactory)
    {
        _gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory));
    }

    public async Task<IReadOnlyCollection<Project>> Execute()
    {
        var gitHubClient = _gitHubClientFactory.Create();
        var response = await gitHubClient.GetRepositories().ConfigureAwait(false);
        return response.Select(x => new Project(x.Name, x.Url, x.Stars)).ToArray();
    }
}

GitHubClient.cs
Looks the same as Version 4.

Pros

  • Using IHttpClientFactory
  • Using a typed client
  • GetAllProjectsQuery is now a singleton again

Cons

  • Fetching and storing the whole response in a string.

Optimization

So, with version 5 we are using a typed client and GetAllProjectsQuery is registered as a singleton, nice, only thing left is to try to get the code perform as good as possible, I have a few tricks :).

Version 6

We are now using the .SendAsync method instead of GetStringAsync. This is to allow us to stream the response instead of fetching it as a string. I've added the HttpCompletionOption.ResponseContentRead parameter to the code for brevity, it's the default option.
Now we are streaming the response from the HttpClient straight into the Deserialize method. We are also injecting the JsonSerializer that we have registered as a singleton.

Version6Configurator.cs

public static class Version6Configurator
{
    public static void AddVersion6(this IServiceCollection services)
    {
        services.AddSingleton<GetAllProjectsQuery>();
        services.AddHttpClient<GitHubClient>(x => { x.BaseAddress = new Uri(GitHubConstants.ApiBaseUrl); });
        services.AddSingleton<GitHubClientFactory>();
        services.AddSingleton<JsonSerializer>();
    }
}

GitHubClient.cs

public class GitHubClient : IGitHubClient
{
    private readonly HttpClient _httpClient;
    private readonly JsonSerializer _jsonSerializer;

    public GitHubClient(HttpClient httpClient, JsonSerializer jsonSerializer)
    {
        _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
        _jsonSerializer = jsonSerializer ?? throw new ArgumentNullException(nameof(jsonSerializer));
    }

    public async Task<IReadOnlyCollection<GitHubRepositoryDto>> GetRepositories()
    {
        var request = CreateRequest();
        var result = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseContentRead).ConfigureAwait(false);

        using (var responseStream = await result.Content.ReadAsStreamAsync())
        {
            using (var streamReader = new StreamReader(responseStream))
            using (var jsonTextReader = new JsonTextReader(streamReader))
            {
                return _jsonSerializer.Deserialize<List<GitHubRepositoryDto>>(jsonTextReader);
            }
        }
    }

    private static HttpRequestMessage CreateRequest()
    {
        return new HttpRequestMessage(HttpMethod.Get, GitHubConstants.RepositoriesPath);
    }
}

Pros

  • Using IHttpClientFactory
  • Using a typed client
  • Streaming the response

Cons

  • ResponseContentRead

Version 7

The only difference here is that we are using ResponseHeadersRead instead of ResponseContentRead. You can read more about the difference between ResponseContentRead vs ResponseHeadersRead here but it basically boils down to that methods using ResponseContentRead waits until both the headers AND content is read where as methods using ResponseHeadersRead just reads the headers and then returns.

public class GitHubClient : IGitHubClient
{
    private readonly HttpClient _httpClient;
    private readonly JsonSerializer _jsonSerializer;

    public GitHubClient(HttpClient httpClient, JsonSerializer jsonSerializer)
    {
        _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
        _jsonSerializer = jsonSerializer ?? throw new ArgumentNullException(nameof(jsonSerializer));
    }

    public async Task<IReadOnlyCollection<GitHubRepositoryDto>> GetRepositories()
    {
        var request = CreateRequest();
        var result = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);

        using (var responseStream = await result.Content.ReadAsStreamAsync())
        {
            using (var streamReader = new StreamReader(responseStream))
            using (var jsonTextReader = new JsonTextReader(streamReader))
            {
                return _jsonSerializer.Deserialize<List<GitHubRepositoryDto>>(jsonTextReader);
            }
        }
    }

    private static HttpRequestMessage CreateRequest()
    {
        return new HttpRequestMessage(HttpMethod.Get, GitHubConstants.RepositoriesPath);
    }
}

Pros

  • Using IHttpClientFactory
  • Using a typed client
  • Streaming the response
  • Using ResponseHeadersRead

Cons

  • Maybe the json deserialization could be improved?

Version 8

Here we've created a custom Json deserializer for JSON.Net, you can find a bunch of performance tips regarding JSON.Net here.

Version8Configurator.cs

public static class Version8Configurator
{
    public static void AddVersion8(this IServiceCollection services)
    {
        services.AddSingleton<GetAllProjectsQuery>();
        services.AddHttpClient<GitHubClient>(x => { x.BaseAddress = new Uri(GitHubConstants.ApiBaseUrl); });
        services.AddSingleton<GitHubClientFactory>();
        services.AddSingleton<JsonSerializer>();
        services.AddSingleton<ProjectDeserializer>();
    }
}

ProjectDeserializer.cs

public class ProjectDeserializer
{
    public IReadOnlyCollection<GitHubRepositoryDto> Deserialize(JsonTextReader jsonTextReader)
    {
        var repositories = new List<GitHubRepositoryDto>();
        var currentPropertyName = string.Empty;
        GitHubRepositoryDto repository = null;
        while (jsonTextReader.Read())
        {
            switch (jsonTextReader.TokenType)
            {
                case JsonToken.StartObject:
                    repository = new GitHubRepositoryDto();
                    continue;
                case JsonToken.EndObject:
                    repositories.Add(repository);
                    continue;
                case JsonToken.PropertyName:
                    currentPropertyName = jsonTextReader.Value.ToString();
                    continue;
                case JsonToken.String:
                    switch (currentPropertyName)
                    {
                        case "name":
                            repository.Name = jsonTextReader.Value.ToString();
                            continue;
                        case "url":
                            repository.Url = jsonTextReader.Value.ToString();
                            continue;
                    }
                    continue;
                case JsonToken.Integer:
                    switch (currentPropertyName)
                    {
                        case "stars":
                            repository.Stars = int.Parse(jsonTextReader.Value.ToString());
                            continue;
                    }
                    continue;
            }
        }
        return repositories;
    }
}

GitHubClient.cs

public class GitHubClient : IGitHubClient
{
    private readonly HttpClient _httpClient;
    private readonly ProjectDeserializer _projectDeserializer;

    public GitHubClient(HttpClient httpClient, ProjectDeserializer projectDeserializer)
    {
        _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
        _projectDeserializer = projectDeserializer ?? throw new ArgumentNullException(nameof(projectDeserializer));
    }

    public async Task<IReadOnlyCollection<GitHubRepositoryDto>> GetRepositories()
    {
        var request = CreateRequest();
        var result = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);

        using (var streamReader = new StreamReader(await result.Content.ReadAsStreamAsync()))
        using (var jsonTextReader = new JsonTextReader(streamReader))
        {
            return _projectDeserializer.Deserialize(jsonTextReader);
        }
    }

    private static HttpRequestMessage CreateRequest()
    {
        return new HttpRequestMessage(HttpMethod.Get, GitHubConstants.RepositoriesPath);
    }
}

Pros

  • Using IHttpClientFactory
  • Using a typed client
  • Streaming the response
  • Using ResponseHeadersRead
  • Optimized the json deserialization

Cons

  • As pointed out in the comments, we are still blocking when we are deserializing the response.

Version 9

This version uses the new json serializer from Microsoft.

Version9Configurator.cs

public static class Version9Configurator
{
    public static void AddVersion9(this IServiceCollection services)
    {
        services.AddSingleton<GetAllProjectsQuery>();
        services.AddHttpClient<GitHubClient>("GitHubClient.Version9", x => { x.BaseAddress = new Uri(GitHubConstants.ApiBaseUrl); });
        services.AddSingleton<GitHubClientFactory>();
    }
}

GitHubClient.cs

public class GitHubClient : IGitHubClient
{
    private readonly HttpClient _httpClient;

    public GitHubClient(HttpClient httpClient)
    {
        _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
    }

    public async Task<IReadOnlyCollection<GitHubRepositoryDto>> GetRepositories()
    {
        var request = CreateRequest();
        var result = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);

        using (var contentStream = await result.Content.ReadAsStreamAsync())
        {
            return await JsonSerializer.DeserializeAsync<List<GitHubRepositoryDto>>(contentStream, DefaultJsonSerializerOptions.Options);;
        }
    }

    private static HttpRequestMessage CreateRequest()
    {
        return new HttpRequestMessage(HttpMethod.Get, GitHubConstants.RepositoriesPath);
    }
}

Pros

  • Using IHttpClientFactory
  • Using a typed client
  • Streaming the response
  • Using ResponseHeadersRead
  • Using the new System.Text.Json serializer that allows async deserialization.

Cons

  • ?

Version 10

Your version! Do you think you can make an even better version than my best version? Feel free to send a PR on GitHub and I will happily add it to the benchmarks and to this post!

Benchmarks

Time for some data!

I will run the different benchmarks four times, I will change how many items the API returns between the benchmarks.

  • First run: 10 items ~ 11 KB
  • Second run: 100 items ~ 112 KB
  • Third run: 1 000 items ~ 1 116 KB
  • Fourth run: 10 000 items ~ 11 134 KB

Every pass in the benchmark run invokes the method 50 times.

Benchmark 1

This benchmark resolves the GetAllProjectsQuery from the ServiceProvider, fetches the data with the GitHubClient, deserializes it and then maps it to domain objects.

10 projects

BenchmarkDotNet=v0.12.0, OS=Windows 10.0.19025
Intel Core i7-8750H CPU 2.20GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores
.NET Core SDK=3.1.100-preview3-014645
  [Host]     : .NET Core 3.1.0 (CoreCLR 4.700.19.53102, CoreFX 4.700.19.55104), X64 RyuJIT
  Job-VJTEAD : .NET Core 3.1.0 (CoreCLR 4.700.19.53102, CoreFX 4.700.19.55104), X64 RyuJIT

Runtime=.NET Core 3.1  InvocationCount=50  UnrollFactor=1  

|   Method |       Mean |     Error |      StdDev |     Median | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated |
|--------- |-----------:|----------:|------------:|-----------:|------:|--------:|------:|------:|------:|----------:|
| Version0 | 1,000.1 us |  19.87 us |    43.62 us |   983.6 us |  1.00 |    0.00 |     - |     - |     - |  75.51 KB |
| Version1 | 3,127.8 us | 482.61 us | 1,423.00 us | 3,948.8 us |  2.53 |    1.64 |     - |     - |     - |  75.29 KB |
| Version2 |   397.0 us |   9.22 us |    26.75 us |   395.4 us |  0.41 |    0.03 |     - |     - |     - |   58.1 KB |
| Version3 |   389.1 us |   7.76 us |    20.03 us |   387.1 us |  0.39 |    0.03 |     - |     - |     - |  60.23 KB |
| Version4 |   407.0 us |  10.31 us |    29.92 us |   399.3 us |  0.41 |    0.03 |     - |     - |     - |  59.32 KB |
| Version5 |   421.8 us |   9.65 us |    27.99 us |   417.3 us |  0.43 |    0.03 |     - |     - |     - |   59.3 KB |
| Version6 |   412.4 us |  12.27 us |    35.59 us |   406.1 us |  0.43 |    0.04 |     - |     - |     - |  53.27 KB |
| Version7 |   394.1 us |   7.84 us |    21.33 us |   393.5 us |  0.40 |    0.03 |     - |     - |     - |  44.25 KB |
| Version8 |   367.4 us |   7.86 us |    22.43 us |   362.7 us |  0.37 |    0.03 |     - |     - |     - |  44.51 KB |
| Version9 | 1,124.6 us |  23.09 us |    39.83 us | 1,116.9 us |  1.11 |    0.06 |     - |     - |     - |  60.97 KB |

Things to note
Version 7 and 8 allocates the least amount of memory. Version9 that uses the new System.Text.Json serializer allocates more than Version 7 and 8 and performs quite bad in general, will this trend continue?

100 projects

BenchmarkDotNet=v0.12.0, OS=Windows 10.0.19025
Intel Core i7-8750H CPU 2.20GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores
.NET Core SDK=3.1.100-preview3-014645
  [Host]     : .NET Core 3.1.0 (CoreCLR 4.700.19.53102, CoreFX 4.700.19.55104), X64 RyuJIT
  Job-LOIBJZ : .NET Core 3.1.0 (CoreCLR 4.700.19.53102, CoreFX 4.700.19.55104), X64 RyuJIT

Runtime=.NET Core 3.1  InvocationCount=50  UnrollFactor=1  

|   Method |     Mean |     Error |    StdDev | Ratio | RatioSD |   Gen 0 |   Gen 1 |   Gen 2 | Allocated |
|--------- |---------:|----------:|----------:|------:|--------:|--------:|--------:|--------:|----------:|
| Version0 | 9.125 ms | 0.1798 ms | 0.2521 ms |  1.00 |    0.00 | 80.0000 | 40.0000 | 40.0000 | 499.21 KB |
| Version1 | 8.977 ms | 0.1731 ms | 0.1852 ms |  0.98 |    0.03 | 80.0000 | 40.0000 | 40.0000 | 499.58 KB |
| Version2 | 7.911 ms | 0.1979 ms | 0.3082 ms |  0.87 |    0.04 | 80.0000 | 40.0000 | 40.0000 | 482.74 KB |
| Version3 | 7.885 ms | 0.1266 ms | 0.1057 ms |  0.86 |    0.03 | 80.0000 | 40.0000 | 40.0000 | 483.85 KB |
| Version4 | 8.000 ms | 0.1556 ms | 0.1792 ms |  0.87 |    0.03 | 80.0000 | 40.0000 | 40.0000 | 483.96 KB |
| Version5 | 7.812 ms | 0.1544 ms | 0.1585 ms |  0.85 |    0.02 | 80.0000 | 40.0000 | 40.0000 | 483.96 KB |
| Version6 | 7.829 ms | 0.1505 ms | 0.1673 ms |  0.85 |    0.03 | 80.0000 | 20.0000 | 20.0000 | 396.49 KB |
| Version7 | 7.912 ms | 0.1574 ms | 0.1933 ms |  0.86 |    0.03 | 60.0000 |       - |       - | 306.16 KB |
| Version8 | 7.526 ms | 0.0877 ms | 0.0685 ms |  0.82 |    0.03 | 60.0000 |       - |       - | 309.23 KB |
| Version9 | 8.547 ms | 0.1663 ms | 0.1708 ms |  0.93 |    0.03 | 20.0000 |       - |       - | 106.67 KB |

Things to note
As soon as the size of the json response increased, Version 9 really starts to shine. Version 7 and 8 is not even close when it comes to allocation.

1 000 projects

BenchmarkDotNet=v0.12.0, OS=Windows 10.0.19025
Intel Core i7-8750H CPU 2.20GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores
.NET Core SDK=3.1.100-preview3-014645
  [Host]     : .NET Core 3.1.0 (CoreCLR 4.700.19.53102, CoreFX 4.700.19.55104), X64 RyuJIT
  Job-LGGORO : .NET Core 3.1.0 (CoreCLR 4.700.19.53102, CoreFX 4.700.19.55104), X64 RyuJIT

Runtime=.NET Core 3.1  InvocationCount=50  UnrollFactor=1  

|   Method |     Mean |    Error |   StdDev |   Median | Ratio | RatioSD |    Gen 0 |    Gen 1 |    Gen 2 |  Allocated |
|--------- |---------:|---------:|---------:|---------:|------:|--------:|---------:|---------:|---------:|-----------:|
| Version0 | 83.04 ms | 2.763 ms | 8.147 ms | 84.11 ms |  1.00 |    0.00 | 940.0000 | 660.0000 | 400.0000 | 4673.49 KB |
| Version1 | 82.84 ms | 3.010 ms | 8.876 ms | 83.35 ms |  1.00 |    0.13 | 940.0000 | 660.0000 | 400.0000 | 4673.26 KB |
| Version2 | 82.90 ms | 3.000 ms | 8.847 ms | 85.61 ms |  1.01 |    0.16 | 960.0000 | 660.0000 | 420.0000 | 4657.06 KB |
| Version3 | 82.76 ms | 2.990 ms | 8.816 ms | 85.76 ms |  1.01 |    0.15 | 900.0000 | 620.0000 | 380.0000 | 4657.83 KB |
| Version4 | 82.65 ms | 2.870 ms | 8.463 ms | 85.89 ms |  1.01 |    0.16 | 900.0000 | 600.0000 | 380.0000 | 4657.94 KB |
| Version5 | 82.86 ms | 3.107 ms | 9.161 ms | 85.37 ms |  1.01 |    0.16 | 940.0000 | 660.0000 | 400.0000 | 4658.11 KB |
| Version6 | 82.76 ms | 3.146 ms | 9.276 ms | 85.78 ms |  1.01 |    0.16 | 740.0000 | 460.0000 | 220.0000 |  3757.4 KB |
| Version7 | 82.95 ms | 3.090 ms | 9.112 ms | 85.88 ms |  1.01 |    0.15 | 540.0000 | 240.0000 |        - | 2852.27 KB |
| Version8 | 83.06 ms | 2.971 ms | 8.761 ms | 86.83 ms |  1.01 |    0.12 | 560.0000 | 220.0000 |        - | 2883.18 KB |
| Version9 | 82.76 ms | 3.068 ms | 9.047 ms | 86.88 ms |  1.00 |    0.13 | 100.0000 |  40.0000 |        - |  564.68 KB |

Things to note
Version 9 continues to keep the allocations really low compared to the other verisons.
Except for Version 9, it's only version 7 and 8 that does not cause any gen 2 collects. Version 6 that uses ResponseContentRead causes gen 2 collects but version 7 that uses ResponseHeadersRead does not...

10 000 projects

BenchmarkDotNet=v0.12.0, OS=Windows 10.0.19025
Intel Core i7-8750H CPU 2.20GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores
.NET Core SDK=3.1.100-preview3-014645
  [Host]     : .NET Core 3.1.0 (CoreCLR 4.700.19.53102, CoreFX 4.700.19.55104), X64 RyuJIT
  Job-WPDQER : .NET Core 3.1.0 (CoreCLR 4.700.19.53102, CoreFX 4.700.19.55104), X64 RyuJIT

Runtime=.NET Core 3.1  InvocationCount=50  UnrollFactor=1  

|   Method |       Mean |    Error |   StdDev | Ratio | RatioSD |     Gen 0 |     Gen 1 |     Gen 2 | Allocated |
|--------- |-----------:|---------:|---------:|------:|--------:|----------:|----------:|----------:|----------:|
| Version0 |   805.2 ms | 15.08 ms | 14.10 ms |  1.00 |    0.00 | 5740.0000 | 1840.0000 |  980.0000 |  53.98 MB |
| Version1 |   804.1 ms | 16.00 ms | 17.12 ms |  1.00 |    0.02 | 5720.0000 | 1780.0000 |  980.0000 |  53.98 MB |
| Version2 |   811.2 ms | 15.96 ms | 17.08 ms |  1.01 |    0.02 | 5740.0000 | 1880.0000 |  980.0000 |  53.96 MB |
| Version3 |   814.2 ms | 15.79 ms | 14.77 ms |  1.01 |    0.03 | 5680.0000 | 1740.0000 |  980.0000 |  53.96 MB |
| Version4 |   806.5 ms | 13.47 ms | 12.60 ms |  1.00 |    0.03 | 5780.0000 | 1960.0000 |  980.0000 |  53.96 MB |
| Version5 |   803.7 ms | 16.06 ms | 20.88 ms |  1.00 |    0.03 | 5740.0000 | 1880.0000 |  980.0000 |  53.96 MB |
| Version6 | 1,094.9 ms | 28.47 ms | 82.59 ms |  1.20 |    0.19 | 5580.0000 | 1520.0000 | 1000.0000 |  36.36 MB |
| Version7 | 1,108.4 ms | 21.23 ms | 23.60 ms |  1.38 |    0.04 | 4980.0000 | 1300.0000 |  640.0000 |  27.55 MB |
| Version8 | 1,090.0 ms | 21.67 ms | 20.27 ms |  1.35 |    0.04 | 5120.0000 | 1340.0000 |  640.0000 |  27.85 MB |
| Version9 | 1,070.7 ms | 20.35 ms | 19.03 ms |  1.33 |    0.03 | 1000.0000 |  380.0000 |  180.0000 |   5.12 MB |

Things to note
Version 9, do I need to say more?

Benchmark 2

This benchmark resolves the GitHubClient from the ServiceProvider, fetches the data and deserializes it.

10 projects

BenchmarkDotNet=v0.12.0, OS=Windows 10.0.19025
Intel Core i7-8750H CPU 2.20GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores
.NET Core SDK=3.1.100-preview3-014645
  [Host]     : .NET Core 3.1.0 (CoreCLR 4.700.19.53102, CoreFX 4.700.19.55104), X64 RyuJIT
  Job-VJTEAD : .NET Core 3.1.0 (CoreCLR 4.700.19.53102, CoreFX 4.700.19.55104), X64 RyuJIT

Runtime=.NET Core 3.1  InvocationCount=50  UnrollFactor=1  

|   Method |       Mean |    Error |   StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated |
|--------- |-----------:|---------:|---------:|------:|--------:|------:|------:|------:|----------:|
| Version0 | 1,057.4 us | 31.31 us | 91.35 us |  1.00 |    0.00 |     - |     - |     - |  74.01 KB |
| Version1 | 1,014.1 us | 20.26 us | 49.32 us |  0.94 |    0.09 |     - |     - |     - |  75.29 KB |
| Version2 |   392.1 us |  7.82 us | 14.11 us |  0.35 |    0.02 |     - |     - |     - |  58.78 KB |
| Version3 |   396.3 us |  7.92 us | 18.66 us |  0.36 |    0.03 |     - |     - |     - |  59.84 KB |
| Version4 |   416.3 us |  8.62 us | 23.46 us |  0.39 |    0.04 |     - |     - |     - |  59.73 KB |
| Version5 |   402.1 us |  8.02 us | 21.54 us |  0.38 |    0.03 |     - |     - |     - |  59.77 KB |
| Version6 |   406.5 us |  8.30 us | 20.82 us |  0.38 |    0.03 |     - |     - |     - |  53.61 KB |
| Version7 |   402.0 us |  8.00 us | 19.16 us |  0.37 |    0.03 |     - |     - |     - |  44.52 KB |
| Version8 |   361.6 us |  7.43 us | 17.36 us |  0.33 |    0.03 |     - |     - |     - |  44.82 KB |
| Version9 | 1,157.0 us | 21.72 us | 20.31 us |  1.00 |    0.05 |     - |     - |     - |  61.89 KB |

Things to note
Same pattern as in benchmark 1, version 7 and 8 allocates the least amount while version 9 is lagging somewhat behind.

100 projects

BenchmarkDotNet=v0.12.0, OS=Windows 10.0.19025
Intel Core i7-8750H CPU 2.20GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores
.NET Core SDK=3.1.100-preview3-014645
  [Host]     : .NET Core 3.1.0 (CoreCLR 4.700.19.53102, CoreFX 4.700.19.55104), X64 RyuJIT
  Job-LOIBJZ : .NET Core 3.1.0 (CoreCLR 4.700.19.53102, CoreFX 4.700.19.55104), X64 RyuJIT

Runtime=.NET Core 3.1  InvocationCount=50  UnrollFactor=1  

|   Method |     Mean |     Error |    StdDev | Ratio | RatioSD |   Gen 0 |   Gen 1 |   Gen 2 | Allocated |
|--------- |---------:|----------:|----------:|------:|--------:|--------:|--------:|--------:|----------:|
| Version0 | 9.032 ms | 0.1826 ms | 0.1954 ms |  1.00 |    0.00 | 80.0000 | 40.0000 | 40.0000 | 494.61 KB |
| Version1 | 9.012 ms | 0.1387 ms | 0.1297 ms |  1.00 |    0.03 | 80.0000 | 40.0000 | 40.0000 | 494.67 KB |
| Version2 | 7.833 ms | 0.1551 ms | 0.1375 ms |  0.87 |    0.03 | 80.0000 | 40.0000 | 40.0000 | 477.84 KB |
| Version3 | 7.946 ms | 0.1581 ms | 0.1402 ms |  0.88 |    0.03 | 80.0000 | 40.0000 | 40.0000 | 478.95 KB |
| Version4 | 8.035 ms | 0.1491 ms | 0.2041 ms |  0.89 |    0.03 | 80.0000 | 40.0000 | 40.0000 | 479.03 KB |
| Version5 | 7.855 ms | 0.1564 ms | 0.1463 ms |  0.87 |    0.03 | 80.0000 | 40.0000 | 40.0000 | 479.03 KB |
| Version6 | 7.809 ms | 0.1505 ms | 0.1478 ms |  0.87 |    0.03 | 60.0000 | 20.0000 | 20.0000 |  391.6 KB |
| Version7 | 7.854 ms | 0.2158 ms | 0.2120 ms |  0.87 |    0.03 | 60.0000 |       - |       - | 301.25 KB |
| Version8 | 7.649 ms | 0.1515 ms | 0.2022 ms |  0.84 |    0.03 | 60.0000 |       - |       - | 304.33 KB |
| Version9 | 8.562 ms | 0.1706 ms | 0.2335 ms |  0.95 |    0.04 | 20.0000 |       - |       - | 101.75 KB |

Things to note
Same pattern as in benchmark 1, Version 9 starts to shine as soon as the repsonse body size goes up.

1 000 projects

BenchmarkDotNet=v0.12.0, OS=Windows 10.0.19025
Intel Core i7-8750H CPU 2.20GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores
.NET Core SDK=3.1.100-preview3-014645
  [Host]     : .NET Core 3.1.0 (CoreCLR 4.700.19.53102, CoreFX 4.700.19.55104), X64 RyuJIT
  Job-LGGORO : .NET Core 3.1.0 (CoreCLR 4.700.19.53102, CoreFX 4.700.19.55104), X64 RyuJIT

Runtime=.NET Core 3.1  InvocationCount=50  UnrollFactor=1  

|   Method |     Mean |    Error |   StdDev |   Median | Ratio | RatioSD |    Gen 0 |    Gen 1 |    Gen 2 |  Allocated |
|--------- |---------:|---------:|---------:|---------:|------:|--------:|---------:|---------:|---------:|-----------:|
| Version0 | 82.55 ms | 3.107 ms | 9.162 ms | 84.51 ms |  1.00 |    0.00 | 880.0000 | 660.0000 | 360.0000 | 4626.39 KB |
| Version1 | 82.81 ms | 3.103 ms | 9.149 ms | 82.97 ms |  1.02 |    0.17 | 900.0000 | 660.0000 | 380.0000 | 4626.44 KB |
| Version2 | 82.82 ms | 3.092 ms | 9.116 ms | 85.60 ms |  1.02 |    0.16 | 940.0000 | 620.0000 | 400.0000 | 4609.61 KB |
| Version3 | 82.82 ms | 3.095 ms | 9.125 ms | 84.20 ms |  1.02 |    0.17 | 960.0000 | 660.0000 | 420.0000 | 4610.88 KB |
| Version4 | 82.98 ms | 3.050 ms | 8.994 ms | 85.04 ms |  1.02 |    0.17 | 960.0000 | 660.0000 | 420.0000 | 4610.91 KB |
| Version5 | 82.95 ms | 3.048 ms | 8.986 ms | 85.12 ms |  1.02 |    0.17 | 940.0000 | 660.0000 | 420.0000 | 4610.86 KB |
| Version6 | 82.95 ms | 2.961 ms | 8.729 ms | 85.36 ms |  1.02 |    0.16 | 740.0000 | 420.0000 | 220.0000 |  3710.1 KB |
| Version7 | 82.73 ms | 3.238 ms | 9.549 ms | 82.98 ms |  1.02 |    0.20 | 540.0000 | 200.0000 |        - | 2805.17 KB |
| Version8 | 82.83 ms | 3.021 ms | 8.907 ms | 87.16 ms |  1.02 |    0.16 | 540.0000 | 220.0000 |        - | 2836.09 KB |
| Version9 | 82.74 ms | 3.033 ms | 8.944 ms | 87.21 ms |  1.02 |    0.17 |  80.0000 |  20.0000 |        - |   517.5 KB |

10 000 projects

BenchmarkDotNet=v0.12.0, OS=Windows 10.0.19025
Intel Core i7-8750H CPU 2.20GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores
.NET Core SDK=3.1.100-preview3-014645
  [Host]     : .NET Core 3.1.0 (CoreCLR 4.700.19.53102, CoreFX 4.700.19.55104), X64 RyuJIT
  Job-WPDQER : .NET Core 3.1.0 (CoreCLR 4.700.19.53102, CoreFX 4.700.19.55104), X64 RyuJIT

Runtime=.NET Core 3.1  InvocationCount=50  UnrollFactor=1  

|   Method |     Mean |    Error |   StdDev | Ratio | RatioSD |     Gen 0 |     Gen 1 |    Gen 2 | Allocated |
|--------- |---------:|---------:|---------:|------:|--------:|----------:|----------:|---------:|----------:|
| Version0 | 810.4 ms | 12.61 ms | 11.79 ms |  1.00 |    0.00 | 5740.0000 | 2000.0000 | 980.0000 |  53.52 MB |
| Version1 | 814.2 ms | 16.12 ms | 19.80 ms |  1.00 |    0.03 | 5760.0000 | 2000.0000 | 980.0000 |  53.52 MB |
| Version2 | 808.4 ms | 12.23 ms | 11.44 ms |  1.00 |    0.02 | 5820.0000 | 2000.0000 | 980.0000 |   53.5 MB |
| Version3 | 816.5 ms | 15.44 ms | 17.17 ms |  1.01 |    0.02 | 5820.0000 | 2000.0000 | 980.0000 |  53.51 MB |
| Version4 | 808.7 ms | 13.64 ms | 12.76 ms |  1.00 |    0.03 | 5820.0000 | 2000.0000 | 980.0000 |  53.51 MB |
| Version5 | 808.0 ms | 14.11 ms | 13.20 ms |  1.00 |    0.02 | 5800.0000 | 1960.0000 | 960.0000 |  53.51 MB |
| Version6 | 809.7 ms | 11.95 ms | 11.18 ms |  1.00 |    0.02 | 5780.0000 | 2000.0000 | 980.0000 |   35.9 MB |
| Version7 | 808.9 ms | 12.36 ms | 11.56 ms |  1.00 |    0.02 | 5040.0000 | 1520.0000 | 640.0000 |  27.09 MB |
| Version8 | 813.6 ms | 16.17 ms | 16.60 ms |  1.00 |    0.03 | 5120.0000 | 1220.0000 | 540.0000 |  27.39 MB |
| Version9 | 811.0 ms | 15.26 ms | 14.98 ms |  1.00 |    0.03 |  860.0000 |  300.0000 | 140.0000 |   4.66 MB |

Let's talk about mapping objects in c-sharp (C#)

$
0
0

Intro

In this post I will focus on two things:

  1. Why should we map our models (database, external api dtos...) to a different object before exposing the data in a API response?
  2. How do we map our models?

Background

Imagine that we're building an API that returns orders. Our domain object looks like this, one abstract Order class and then multiple concrete implementations of it, we will focus on BusinessOrder (B2B).

Order

public abstract class Order<TBillingAddress, TShippingAddress, TCustomer>
{
    protected Order(
        int id,
        TBillingAddress billing,
        TShippingAddress shipping,
        TCustomer customer,
        OrderDetails orderDetails)
    {
        Id = id;
        Billing = billing;
        Shipping = shipping;
        Customer = customer;
        OrderDetails = orderDetails;
    }

    public int Id { get; }
    public abstract string Type { get; }
    public TBillingAddress Billing { get; }
    public TShippingAddress Shipping { get; }
    public TCustomer Customer { get; }
    public OrderDetails OrderDetails { get; }
}

public class BusinessOrder : Order<BusinessBillingAddress, BusinessShippingAddress, BusinessCustomer>
{
    public BusinessOrder(
        int id,
        BusinessBillingAddress billing,
        BusinessShippingAddress shipping,
        BusinessCustomer customer,
        OrderDetails orderDetails) : base(
        id,
        billing,
        shipping,
        customer,
        orderDetails)
    {

    }

    public override string Type => "BUSINESS";
}

You don't need to know how the BusinessBillingAddress, BusinessShippingAddress or OrderDetails look like, we will focus on BusinessCustomer in this post.

BusinessCustomer

public abstract class Customer
{
    protected Customer(string id)
    {
        Id = id;
    }

    public string Id { get; }
}

public class BusinessCustomer : Customer
{
    public BusinessCustomer(
        string id,
        string companyName,
        string organizationalNumber,
        string description) : base(id)
    {
        CompanyName = companyName;
        OrganizationalNumber = organizationalNumber;
        Description = description;
    }
    
    public string CompanyName { get; }
    public string OrganizationalNumber { get; }
    public string Description {get; }
}

Why should we map our models?

Now, imagine that we have the following customer:

var businessCustomer = new BusinessCustomer(
    "bc-1234",
    "Dunder Mifflin Paper",
    "12345678",
    "Sells paper, very annoying boss"
);

Then they want to use V1 of our API that looks like this:

public async Task<IActionResult> Get(int orderId)
{
    var order = await _getOrderQuery.Execute(orderId);
    return new ObjectResult(order);
}

The response would look something like this:

{
    "orderId": 1234,
    "type": "BUSINESS",
    "billing": {......},
    "shipping": {......},
    "orderDetails": {......},
    "customer": {
        "id": "bc-1234",
        "companyName": "Dunder Mifflin Paper",
        "organizationalNumber": "12345678",
        "description": "Sells paper, very annoying boss"
    }
}

A few hours later you receive a phone call from someone called Michael Scott who yells like crazy and screams something about a hitman named Dwight.

So, what happened here?
We exposed secret internal data about our customer in the API response. Not good.
How can we prevent this from happening again?

How do we map our models?

"just put a JsonIgnoreattribute on the description property and get on with your life"
Non-caring developer

Sure, that would work, but I don't want to clutter my domain models with attributes that are specific for serialization.
And what if I want to rename the companyName to just name in the response? Should I just rename the property in my domain model or what?
Or what if I want to group the Billing and Shipping address together in the response? Should I remodel my domain model to make it work?

Of course not, that's why we're going to map it.

Meet OrderResponseDto.

public abstract class OrderResponseDto
{
    public int OrderId { get; }
    public abstract string Type { get; }
    public OrderDetailsResponseDto OrderDetails { get; }

    protected OrderResponseDto(int orderId, OrderDetailsResponseDto orderDetails)
    {
        OrderId = orderId;
        OrderDetails = orderDetails;
    }
}

public class BusinessOrderResponseDto : OrderResponseDto
{
    public BusinessOrderResponseDto(
        int orderId,
        BusinessAddressResponseDto address,
        BusinessCustomerResponseDto customer,
        OrderDetailsResponseDto orderDetails) : base(
        orderId,
        orderDetails)
    {
        Address = address;
        Customer = customer;
    }
    
    public override string Type => "BUSINESS";
    public BusinessAddressResponseDto Address { get; }
    public BusinessCustomerResponseDto Customer { get; }
}

The BusinessCustomerResponseDto looks like this:

public class BusinessCustomerResponseDto
{
    public BusinessCustomerResponseDto(
        string id,
        string name,
        string organizationalNumber)
    {
        Id = id;
        Name = name;
        OrganizationalNumber = organizationalNumber;
    }

    public string Id { get; }
    public string Name { get; }
    public string OrganizationalNumber { get; }
}

Now, if we were to populate our BusinessOrderResponseDto and return that one instead of the domain object the response would look something like this:

{
    "orderId": 1234,
    "type": "BUSINESS",
    "address": {
        "billing": {.....},
        "shipping": {....}
    },
    "orderDetails": {......},
    "customer": {
        "id": "bc-1234",
        "name": "Dunder Mifflin Paper",
        "organizationalNumber": "12345678"
    }

Look! We have now managed to rename companyName to name and we are no longer leaking the description, NICE!

But...how do we create the response object?

Right now our controller looks like this, we are new:ing it up and all our mapping takes place in the controller.

public async Task<IActionResult> Get(int orderId)
{
    var order = await _getOrderQuery.Execute(orderId);
    var response = new BusinessOrderResponseDto(.....);
    return new ObjectResult(response);
}

That works, but it's not optimal, let's fix it!

Before showing the different approaches (you can stop screaming "just use AutoMapper!!"), think of the following:

  • Do we really need to introduce a new dependency just for this?
  • All models are immutable, both domain and repsonse.

Option 1 - OrderResponseDtoFactory

This has been my prefered way of doing mappings for a long time. I create a factory class that I register as a singleton and then inject it wherever I need it.

OrderResponseDtoFactory

public class OrderResponseDtoFactory
{
    public BusinessOrderResponseDto Create(BusinessOrder order)
    {
        var billingAddress = new BusinessAddressBillingResponseDto(
            order.Billing.Receiver.CompanyName,
            order.Billing.Receiver.Reference,
            order.Billing.CareOf,
            order.Billing.City,
            order.Billing.Street,
            order.Billing.Zip,
            order.Billing.Country);

        var shippingAddress = order.Shipping != null
            ? new BusinessAddressShippingResponseDto(
                order.Shipping.Receiver.CompanyName,
                order.Shipping.Receiver.Reference,
                order.Shipping.CareOf,
                order.Shipping.City,
                order.Shipping.Street,
                order.Shipping.Zip,
                order.Shipping.Country)
            : null;

        var orderDetailsResponseDto = new OrderDetailsResponseDto(
            order.OrderDetails.TotalPrice,
            order.OrderDetails.OrderRows.Select(x => new OrderRowResponseDto(
                x.Name,
                x.ProductId,
                x.Quantity,
                x.Price,
                x.Vat,
                x.VatPercentage
            ))
        );

        return new BusinessOrderResponseDto(
            order.Id,
            new BusinessAddressResponseDto(
                billingAddress,
                shippingAddress
            ),
            new BusinessCustomerResponseDto(
                order.Customer.Id,
                order.Customer.CompanyName,
                order.Customer.OrganizationalNumber), 
            orderDetailsResponseDto);
    }
}

Usage

public async Task<IActionResult> Get(int orderId)
{
    var order = await _getOrderQuery.Execute(orderId);
    var mapped = _orderResponseDtoFactory.Create(order);
    return new ObjectResult(mapped);
}

Pros

  • All mapping logic in one place
  • Testable
  • Easy to inject other dependencies (other factories for example)

Cons

  • You need to take a new dependency on the factory wherever you want to map. In our case this will just be in our controller so it's not that big of a deal, but could be in other cases.

Option 2 - AutoMapper

First, HUGE DISCLAIMER:
AutoMapper is a really really really nice library IF it fits your use case. I have never used AutoMapper like this before (with immutable models) so take all the config/benchmarking with a huge grain of salt.

I recommend everyone to read this article written by Jimmy Bogard (the creator of AutoMapper) regarding AutoMapper's design philosophy.

And this: Usage guidelines

The "Auto" is for "automatic" and if it's not "Auto" then don't use this library, it will make things more, not less complicated.
-- Jimmy Bogard

I chose to add AutoMapper here just because of how widely used it is and it seems to be the go-to for a lot of developers as soon as any mapping needs to be done. I've worked in projects where the mapping config has been several hundred lines of code with a bunch of hacks here and there and it has been quite obvious that AutoMapper should not have been used.

I believe in choosing the right tool for the job and in this case, as you will see, AutoMapper might not be the right tool.

AutoMapper works something like this:

var config = new MapperConfiguration(cfg => cfg.CreateMap<BusinessOrder, BusinessOrderResponseDto>());
var mapper = config.CreateMapper();
var businessOrderResponseDto = mapper.Map<BusinessOrderResponseDto>(order);

So we need to configure AutoMapper before using it, seems easy enough.

Usage

public async Task<IActionResult> Get(int orderId)
{
    var order = await _getOrderQuery.Execute(orderId);
    var mapped = Mapper.Map<BusinessOrderResponseDto>.Map(order);
    return new ObjectResult(mapped);
}

There's only one problem though. The above code does not work. Since my models are immutable, AutoMapper can't automatically map them. So we need to modify our config a bit.

var config = new MapperConfiguration(cfg =>
{
    cfg.CreateMap<BusinessOrder, BusinessOrderResponseDto>()
        .ConstructUsing(x => new BusinessOrderResponseDto(
                x.Id,
                new BusinessAddressResponseDto(
                    new BusinessAddressBillingResponseDto(
                        x.Billing.Receiver.CompanyName,
                        x.Billing.Receiver.Reference,
                        x.Billing.CareOf,
                        x.Billing.City,
                        x.Billing.Street,
                        x.Billing.Zip,
                        x.Billing.Country),
                   new BusinessAddressShippingResponseDto(
                        x.Shipping.Receiver.CompanyName,
                        x.Shipping.Receiver.Reference,
                        x.Shipping.CareOf,
                        x.Shipping.City,
                        x.Shipping.Street,
                        x.Shipping.Zip,
                        x.Shipping.Country)),
                   new BusinessCustomerResponseDto(
                       x.Customer.Id,
                       x.Customer.CompanyName,
                       x.Customer.OrganizationalNumber),
                   new OrderDetailsResponseDto(
                       x.OrderDetails.TotalPrice,
                       x.OrderDetails.OrderRows.Select(r => new OrderRowResponseDto(
                           r.Name,
                           r.ProductId,
                           r.Quantity,
                           r.Price,
                           r.Vat,
                           r.VatPercentage)))));

Im not an expert on AutoMapper so there is most certainly a better/simpler way of creating the mapper but right now, there isn't that much auto in the above mapping.

Now we can use AutoMapper to map from BusinessOrder to a BusinessOrderResponseDto
Usage

public async Task<IActionResult> Get(int orderId)
{
    var order = await _getOrderQuery.Execute(orderId);
    var mapped = _mapper.Map<BusinessOrderResponseDto>.Map(order);
    return new ObjectResult(mapped);
}

Pros

  • All mapping logic in one place
  • Testable
  • ...we are using AutoMapper, like everyone else?

Cons

  • There isn't anything auto going on, we still need to create all the mapping configuration ourselves since we are using immutable objects.
  • Performance (as we will see in the benchmarks section)

Option 3 - Implicit/Explicit mapping

If you don't need any external dependencies (inject other services in your factory for example) to do your mappings, then Implicit/Explicit mappings could be a great option.

OrderResponseDto

public static implicit operator BusinessOrderResponseDto(BusinessOrder order)
{
    var billing = (BusinessAddressBillingResponseDto) order.Billing;
    var shipping = order.Shipping == null
        ? null
        : (BusinessAddressShippingResponseDto) order.Shipping;
    var customer = (BusinessCustomerResponseDto) order.Customer;
    var orderDetails = (OrderDetailsResponseDto) order.OrderDetails;
            

    return new BusinessOrderResponseDto(
        order.Id,
        new BusinessAddressResponseDto(billing, shipping),
        customer, 
        orderDetails);
}

By adding the following operator to my code, I can now do my mappings like this:
(Note, I have implemented the implicit operator in the billing, shipping, customer and orderDetails classes as well but left it out for clarity).

Usage

public async Task<IActionResult> Get(int orderId)
{
    var order = await _getOrderQuery.Execute(orderId);
    BusinessOrderResponseDto mapped = _businessOrder;
    return new ObjectResult(mapped);
}
public async Task<IActionResult> Get(int orderId)
{
    var order = await _getOrderQuery.Execute(orderId);
    var mapped = (BusinessOrderResponseDto)_businessOrder;
    return new ObjectResult(mapped);
}
public async Task<BusinessOrderResponseDto> Get(int orderId)
{
    return await _getOrderQuery.Execute(orderId);
}

Pretty neat right?

Pros

  • All mapping logic in one place
  • Testable

Cons

  • You can't inject dependencies.

Benchmarks

I like to meassure things so I ran a quick benchmark and here's the result.
There is more or less no difference between using a factory or explicit/implicit mapping. Use what works best for your use case, if you need external dependencies, use a factory and inject them.
If not, go for implicit/explicit mapping.
And, as I said before, AutoMapper is not the right tool for the use case in this post, ~6.5 times slower (and allocates more as well).
mapping-benchmarks

All code for this post can be found here, complete with benchmark code and tests.

ASP.NET Core - Protect your API with API Keys

$
0
0

Disclaimer

There's a bunch of different ways to handle authentication/authorization. As pointed out here on Twitter by Greg Bair, API keys has some limitations/drawbacks.

One problem is that usually API keys never expires and that's obviously bad from a security point of view. It's better to have short lived tokens.

For my use case (described below) API keys are more than good enough, but if I were to build an application that was publicly available, I would go for something like OAuth 2/JWT/Identityserver4 instead.
It's important to always think twice about security and not just blindly copy/paste code from the internet.


2019-09-25 This blog post has now been updated to use ASP.NET Core 3.0. If you are looking for information about how to do this using ASP.NET Core 2.0, just checkout this git commit and you should be good to go.

The problem

We've an API that are going to be used internally (called by other internal applications).

The consumers of our API are a lot of different departments in our company like accounting, customer service and so on.

We want to solve the following problems:

  1. Identify who is using our API (which department)
  2. Only authenticated and authorized calls should be allowed access.
  3. Give different access levels to different departments

The solution

We are going to generate API keys, one for each department. They will then need to add the API key in all of their API requests. This will allow us to lock down our endpoints, see who is using our API (and keep a bunch of statistics) and much more!

It's important to know the difference between Authentication and Authorization, I will just copy paste this straight from Microsoft:

Authentication is a process in which a user provides credentials that are then compared to those stored in an operating system, database, app or resource. If they match, users authenticate successfully, and can then perform actions that they're authorized for, during an authorization process. The authorization refers to the process that determines what a user is allowed to do.

Authentication

So, let's start!
First we need to define our ApiKey.
ApiKey.cs

public class ApiKey
{
    public ApiKey(int id, string owner, string key, DateTime created, IReadOnlyCollection<string> roles)
    {
        Id = id;
        Owner = owner ?? throw new ArgumentNullException(nameof(owner));
        Key = key ?? throw new ArgumentNullException(nameof(key));
        Created = created;
        Roles = roles ?? throw new ArgumentNullException(nameof(roles));
    }

    public int Id { get; }
    public string Owner { get; }
    public string Key { get; }
    public DateTime Created { get; }
    public IReadOnlyCollection<string> Roles { get; }
}

We also need a place to store/retrieve our API keys so let's create the following interface and implementation.
IGetAllApiKeysQuery.cs

public interface IGetApiKeyQuery
{
    Task<ApiKey> Execute(string providedApiKey);
}

InMemoryGetApiKeyQuery.cs

public class InMemoryGetApiKeyQuery : IGetApiKeyQuery
{
    private readonly IDictionary<string, ApiKey> _apiKeys;

    public InMemoryGetApiKeyQuery()
    {
        var existingApiKeys = new List<ApiKey>
        {
            new ApiKey(1, "Finance", "C5BFF7F0-B4DF-475E-A331-F737424F013C", new DateTime(2019, 01, 01),
                new List<string>
                {
                    Roles.Employee,
                }),
            new ApiKey(2, "Reception", "5908D47C-85D3-4024-8C2B-6EC9464398AD", new DateTime(2019, 01, 01),
                new List<string>
                {
                    Roles.Employee
                }),
            new ApiKey(3, "Management", "06795D9D-A770-44B9-9B27-03C6ABDB1BAE", new DateTime(2019, 01, 01),
                new List<string>
                {
                    Roles.Employee,
                    Roles.Manager
                }),
            new ApiKey(4, "Some Third Party", "FA872702-6396-45DC-89F0-FC1BE900591B", new DateTime(2019, 06, 01),
                new List<string>
                {
                    Roles.ThirdParty
                })
        };

        _apiKeys = existingApiKeys.ToDictionary(x => x.Key, x => x);
    }

    public Task<ApiKey> Execute(string providedApiKey)
    {
        _apiKeys.TryGetValue(providedApiKey, out var key);
        return Task.FromResult(key);
    }
}

As you can see, for this post we are storing the keys in memory, but in the real world we would use a database.

The endpoints we want to protect looks like this:
UserController.cs

[Route("api/[controller]")]
[ApiController]
public class UserController : ControllerBase
{
    [HttpGet("anyone")]
    public IActionResult Anyone()
    {
        var message = $"Hello from {nameof(Anyone)}";
        return new ObjectResult(message);
    }

    [HttpGet("only-authenticated")]
    public IActionResult OnlyAuthenticated()
    {
        var message = $"Hello from {nameof(OnlyAuthenticated)}";
        return new ObjectResult(message);
    }

    [HttpGet("only-employees")]
    public IActionResult OnlyEmployees()
    {
        var message = $"Hello from {nameof(OnlyEmployees)}";
        return new ObjectResult(message);
    }

    [HttpGet("only-managers")]
    public IActionResult OnlyManagers()
    {
        var message = $"Hello from {nameof(OnlyManagers)}";
        return new ObjectResult(message);
    }

    [HttpGet("only-third-parties")]
    public IActionResult OnlyThirdParties()
    {
        var message = $"Hello from {nameof(OnlyThirdParties)}";
        return new ObjectResult(message);
    }
}

So right now, anyone can call any endpoint. Not good, so let's fix that.
We will start with the OnlyAuthenticated(/api/user/only-authenticated) endpoint.

By adding the [Authorize] attribute, we are saying that (in this example) "as long as you are authenticated, you are allowed access".

UserController.cs

[HttpGet("only-authenticated")]
[Authorize]
public IActionResult OnlyAuthenticated()
{
    var message = $"Hello from {nameof(OnlyAuthenticated)}";
    return new ObjectResult(message);
}

If we try to call our endpoint now with our API key...it will not work, of course. We need to setup the authentication.

First, we add the following to our Startup class
Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = ApiKeyAuthenticationOptions.DefaultScheme;
        options.DefaultChallengeScheme = ApiKeyAuthenticationOptions.DefaultScheme;
    })
    .AddApiKeySupport(options => {});

The important thing to notice here is the AddApiKeySupport extension method.
ApiKeyAuthenticationOptions.cs

public class ApiKeyAuthenticationOptions : AuthenticationSchemeOptions
{
    public const string DefaultScheme = "API Key";
    public string Scheme => DefaultScheme;
    public string AuthenticationType = DefaultScheme;
}

AuthenticationBuilderExtensions.cs

public static class AuthenticationBuilderExtensions
{
    public static AuthenticationBuilder AddApiKeySupport(this AuthenticationBuilder authenticationBuilder, Action<ApiKeyAuthenticationOptions> options)
    {
        return authenticationBuilder.AddScheme<ApiKeyAuthenticationOptions, ApiKeyAuthenticationHandler>(ApiKeyAuthenticationOptions.DefaultScheme, options);
    }
}

In the AddApiKeySupportmethod we are adding a scheme, we are basically saying that ApiKeyAuthenticationHandler should handle the Api Key scheme.

ApiKeyAuthenticationHandler.cs

public class ApiKeyAuthenticationHandler : AuthenticationHandler<ApiKeyAuthenticationOptions>
{
    private const string ProblemDetailsContentType = "application/problem+json";
    private readonly IGetApiKeyQuery _getApiKeyQuery;
    private const string ApiKeyHeaderName = "X-Api-Key";
    public ApiKeyAuthenticationHandler(
        IOptionsMonitor<ApiKeyAuthenticationOptions> options,
        ILoggerFactory logger,
        UrlEncoder encoder,
        ISystemClock clock,
        IGetApiKeyQuery getApiKeyQuery) : base(options, logger, encoder, clock)
    {
        _getApiKeyQuery = getApiKeyQuery ?? throw new ArgumentNullException(nameof(getApiKeyQuery));
    }

    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        if (!Request.Headers.TryGetValue(ApiKeyHeaderName, out var apiKeyHeaderValues))
        {
            return AuthenticateResult.NoResult();
        }

        var providedApiKey = apiKeyHeaderValues.FirstOrDefault();

        if (apiKeyHeaderValues.Count == 0 || string.IsNullOrWhiteSpace(providedApiKey))
        {
            return AuthenticateResult.NoResult();
        }

        var existingApiKey = await _getApiKeyQuery.Execute(providedApiKey);

        if (existingApiKey != null)
        {
            var claims = new List<Claim>
            {
                new Claim(ClaimTypes.Name, existingApiKey.Owner)
            };

            claims.AddRange(existingApiKey.Roles.Select(role => new Claim(ClaimTypes.Role, role)));

            var identity = new ClaimsIdentity(claims, Options.AuthenticationType);
            var identities = new List<ClaimsIdentity> { identity };
            var principal = new ClaimsPrincipal(identities);
            var ticket = new AuthenticationTicket(principal, Options.Scheme);

            return AuthenticateResult.Success(ticket);
        }

        return AuthenticateResult.Fail("Invalid API Key provided.");
    }

    protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
    {
        Response.StatusCode = 401;
        Response.ContentType = ProblemDetailsContentType;
        var problemDetails = new UnauthorizedProblemDetails();

        await Response.WriteAsync(JsonSerializer.Serialize(problemDetails, DefaultJsonSerializerOptions.Options));
    }

    protected override async Task HandleForbiddenAsync(AuthenticationProperties properties)
    {
        Response.StatusCode = 403;
        Response.ContentType = ProblemDetailsContentType;
        var problemDetails = new ForbiddenProblemDetails();

        await Response.WriteAsync(JsonSerializer.Serialize(problemDetails, DefaultJsonSerializerOptions.Options));
    }
}

This is were the fun starts!
This method gets called for every request that requires authentication.
The logic goes something like this:

  • If no X-Api-Keyheader is present -> Return no result, let other handlers (if present) handle the request.
  • If the header is present but null or empty -> Return no result.
  • If the provided key does not exists -> Fail the authentication.
  • If the key is valid, create a new identity, add the name claim and add all the roles to the identity.

Not that hard to follow, right?
Now we only need to add the authentication and authorization middleware to our pipeline and we should be good to go.
Startup.cs

public void Configure(IApplicationBuilder app, IHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    
    app.UseRouting();
    app.UseAuthentication();
    app.UseAuthorization();
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

That is basically all it takes to add Authentication based on API keys to our API.

Authorization

Now, say that we want to have specific endpoints that only the upper management can call, how can we achieve that?

Let's modify our OnlyManagement action method to look like this:
UserController.cs

[HttpGet("only-managers")]
[Authorize(Policy = Policies.OnlyManagers)]
public IActionResult OnlyManagers()
{
    var message = $"Hello from {nameof(OnlyManagers)}";
    return new ObjectResult(message);
}

We've added the [Authorize]attribute and set the policy to OnlyManagers.
Think of a policy as something that needs to evaluate to true for us to get access to that specific resource. So in our case, our OnlyManagers policy will, most likely, check that we in fact are managers. You can read more about policies here.

Policies.cs

public static class Policies
{
    public const string OnlyEmployees = nameof(OnlyEmployees);
    public const string OnlyManagers = nameof(OnlyManagers);
    public const string OnlyThirdParties = nameof(OnlyThirdParties);
}

Let's start by adding the following to our Startup class:
Startup.cs

public void ConfigureServices(IServiceCollection services)
{

 ...............
 services.AddAuthorization(options =>
 {
     options.AddPolicy(Policies.OnlyEmployees, policy => policy.Requirements.Add(new OnlyEmployeesRequirement()));
     options.AddPolicy(Policies.OnlyManagers, policy => policy.Requirements.Add(new OnlyManagersRequirement()));
     options.AddPolicy(Policies.OnlyThirdParties, policy => policy.Requirements.Add(new OnlyThirdPartiesRequirement()));
 });
 
 services.AddSingleton<IAuthorizationHandler, OnlyEmployeesAuthorizationHandler>();
 services.AddSingleton<IAuthorizationHandler, OnlyManagersAuthorizationHandler>();
 services.AddSingleton<IAuthorizationHandler, OnlyThirdPartiesAuthorizationHandler>();
 ......

So first, we are registering three different policies, OnlyEmployees, OnlyManagers and OnlyThirdParties, and then we are registering a IAuthorizationHandler for each of the policies.

We are focusing on the OnlyManagers policy so let's check it out.
OnlyManagersRequirement.cs

public class OnlyManagersRequirement : IAuthorizationRequirement
{
    // This is empty, but you can have a bunch of properties and methods here if you like that you can later access from the AuthorizationHandler.
}

OnlyManagersAuthorizationHandler.cs

public class OnlyManagersAuthorizationHandler : AuthorizationHandler<OnlyManagersRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, OnlyManagersRequirement requirement)
    {
        if (context.User.IsInRole(Roles.Manager))
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

So this is really simple, we are just checking if the user is in the Manager role, if true, we call Succeed on the context. If not, we don't do anything.

So, how do we know that the current user is actually part of the management team?
Well, actually, we don't know that for sure since we have just created generic API keys that are shared by all the management applications, but we trust them right? :)

Anyhow, let's recap how we know that the request is actually allowed access to the OnlyManagers endpoint.

  1. The request hits our API
  2. We validate that the request contains a valid API key - Authentication
  3. If the key is valid, we map the roles from the existing API key.
    ApiKeyAuthenticationHandler.cs
.......
var claims = new List<Claim>
{
    new Claim(ClaimTypes.Name, apiKey.Owner)
};

claims.AddRange(apiKey.Roles.Select(role => new Claim(ClaimTypes.Role, role)));
.......
  1. We then validate that the API key contains the Manager role - Authorization
  2. P R O F I T

Revocation of API Keys

It's not covered in this post but it's rather simple to handle it. We created a IHostedService that runs every minute and looks for changes in the database. If a key has been removed or added, we simply update the cache.

A complete project (with integration tests as well!) can be found here.

Also. If you don't protect your API's...I know someone that will come for them :)
61643839_2061987277246624_7996719859469647872_n-1-

Dotnet Core - Filter out specific test projects when running dotnet test

$
0
0

Quick example showing how to exclude tests in a certain project when running dotnet test.

The problem

I have a solution that contains three different test projects:

  • MyProject.Core.Tests
  • MyProject.Persistence.Tests
  • MyProject.IntegrationTests

When running dotnet test on our build server I don't want to run the tests in MyProject.IntegrationTests since they require a real database and I don't have access to one when running on the build server. I will run them later on in my docker swarm.

The solution

dotnet test --filter to the rescue!
On the following link you can find some examples on how to use the dotnet test --filter command.

They don't have any examples of how to exclude projects but they show you how to Runs tests which has TestClass1 in FullyQualifiedName or Category is CategoryA like this: dotnet test --filter "FullyQualifiedName~TestClass1|Category=CategoryA

So, can we somehow do the reverse? Like run all the tests except Tests that contains IntegrationTests in it's namespace?

Yes we can :)

dotnet test --filter FullyQualifiedName!~IntegrationTests

Great success!

Now when running dotnet test it will scan all of your test projects but exclude any tests that contains IntegrationTests in it's FullyQualifiedName.
It will also print out a warning to the console telling you that it could not find any tests in your IntegrationTest projects like this: No test matches the given testcase filter FullyQualifiedName!~IntegrationTests in... (given that you don't have any other tests in the project with a FQN that doesn't contain IntegrationTests of course)

JOS.ContentSerializer now supports TimeSpan? out of the box

Dotnet Core ConfigurationProvider for Docker Swarm Secrets

$
0
0

Where I work, we are using Docker Swarm to host our containers. Since we don't like to commit sensitive data to our git repo (API Keys, passwords etc) we are storing that kind of data in something called Docker Secrets.

Short intro to Secrets

It basically allow us to retrieve sensitive configuration data at runtime. For each secret that's created in your swarm, Docker will mount a file and you can then access that file and read it's value from your container.
HashiCorp Vault, Azure Key Vault and CyberArk Password Vault are other similar services.

Dotnet core - Configuration

Since we are using dotnet core we handle the application configuration with the help of IConfiguration. In our console application we use the Generic Host to be able to bootstrap our application in a similar way to ASP.NET Core.

A typical example of using IConfiguration/IConfigurationBuilder looks something like this:

var host = new HostBuilder()
    .ConfigureHostConfiguration(configHost =>
    {
        configHost.AddJsonFile("appsettings.json", optional: true);
        configHost.AddEnvironmentVariables();
        configHost.AddCommandLine(args);
    })

Here we first read configuration from a file, appsettings.json, then from EnvironmentVariables and lastly from args that gets passed in when starting the application.

It would be nice to follow this pattern when we're reading the secrets in our swarm, right?

So let's get to work.

DockerSwarmSecretsConfigurationProvider

First, we need to implement the IConfigurationSource interface.

public class DockerSwarmSecretsConfigurationSource : IConfigurationSource
{
    private readonly string _secretsPath;
    private readonly Action<string> _handle;

    public DockerSwarmSecretsConfigurationSource(string secretsPath) : this(secretsPath, null) {}

    public DockerSwarmSecretsConfigurationSource(string secretsPath, Action<string> handle)
    {
        _secretsPath = secretsPath ?? throw new ArgumentNullException(nameof(secretsPath));
        _handle = handle;
    }

    public IConfigurationProvider Build(IConfigurationBuilder builder)
    {
        return new DockerSwarmSecretsConfigurationProvider(_secretsPath, _handle);
    }
}

Then we need to create the configuration provider that is responsible for reading the secrets and adding them to the configuration.
Here we are inheriting from ConfigurationProvider so that we don't need to do a bunch of bootstrapping, the Load method is what's important here.

public class DockerSwarmSecretsConfigurationProvider : ConfigurationProvider
{
    public const string DefaultSecretsPath = "/run/secrets";
    private readonly string _secretsPath;
    private readonly Action<string> _handle;

    public DockerSwarmSecretsConfigurationProvider(string secretsPath) : this(secretsPath, null) { }

    public DockerSwarmSecretsConfigurationProvider(string secretsPath, Action<string> handle)
{
    _handle = handle ?? (filePath =>
    {
        var fileName = Path.GetFileName(filePath);
        if (!string.IsNullOrWhiteSpace(fileName))
        {
            var key = fileName.Replace("_", ":");
            var value = File.ReadAllText(filePath);

            Data.Add(key, value);
        }
    });

    _secretsPath = secretsPath ?? throw new ArgumentNullException(nameof(secretsPath));
}

    public override void Load()
    {
        if (Directory.Exists(_secretsPath))
        {
            foreach (var file in Directory.EnumerateFiles(_secretsPath))
            {
                _handle(file);
            }
        }
    }
}

Nothing fancy here, we check that the directory exists, if it exists, read all files in it and add to the Data dictionary that's provided from ConfigurationProvider.
Our secrets uses underscores in the filenames as a convention so we are replacing the underscores with colons instead since that's the format IConfiguration expects.

If you don't follow that convention, you can override the behaviour by passing in your own Action<string>.

The only thing left for us to do now is to create a couple of extension methods for the IConfigurationBuilder and we should be good to go.

public static class DockerSwarmSecretsConfigurator
{
    public static IConfigurationBuilder AddDockerSwarmSecrets(this IConfigurationBuilder configurationBuilder)
    {
        return AddDockerSwarmSecrets(configurationBuilder, DockerSwarmSecretsConfigurationProvider.DefaultSecretsPath);
    }

    public static IConfigurationBuilder AddDockerSwarmSecrets(this IConfigurationBuilder configurationBuilder, string secretsPath, Action<string> handle = null)
    {
        configurationBuilder.Add(new DockerSwarmSecretsConfigurationSource(secretsPath, handle));
        return configurationBuilder;
    }
}

And that's it. Now we can read secrets from our Swarm by just adding the AddDockerSwarmSecrets call like this:

var host = new HostBuilder()
    .ConfigureHostConfiguration(configHost =>
    {
        configHost.AddJsonFile("appsettings.json", optional: true);
        configHost.AddEnvironmentVariables();
        configHost.AddDockerSwarmSecrets();
        configHost.AddCommandLine(args);
    })

All code in this post can be found here.

Cover photo by Georg Wolf / Unsplash


Swagger - Tips and Tricks - Part 1

$
0
0

Appending API Key header from UI automatically

Hi Josef,
I am trying to get my swagger to send the x-api-key in its header, but i cant get it to work.
Do you maybe know how to connect the swagger authentication to this API authentication method?

That qoute is from the following issue that was created on my JOS.ApiKeyAuthentication repository and since I actually know the answer (since I've done it before), I decided to blog about it.

I've actually done this in a few different projects, and it's pretty straightforward. I decided to implement this in JOS.ApiKeyAuthentication so if you just want to see the code, go to this commit.

As you can see in the GitHub issue, Gijs was on the right track, he just forgot to add the SecurityRequirement.

services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new OpenApiInfo { Title = "JOS.ApiKeyAuthentication", Version = "v1" });

    c.AddSecurityDefinition(ApiKeyConstants.HeaderName, new OpenApiSecurityScheme
    {
        Description = "Api key needed to access the endpoints. X-Api-Key: My_API_Key",
        In = ParameterLocation.Header,
        Name = ApiKeyConstants.HeaderName,
        Type = SecuritySchemeType.ApiKey
    });

    c.AddSecurityRequirement(new OpenApiSecurityRequirement
    {
        { 
            new OpenApiSecurityScheme 
            {
                Name = ApiKeyConstants.HeaderName,
                Type = SecuritySchemeType.ApiKey,
                In = ParameterLocation.Header,
                Reference = new OpenApiReference
                { 
                    Type = ReferenceType.SecurityScheme,
                    Id = ApiKeyConstants.HeaderName
                },
             },
             new string[] {}
         }
    });
});

That's it!
Now when browsing to /swagger the UI will look like this:
swagger-ui
Clicking on Authorize will bring up the following popup:
swagger-authentication-popup
Enter your API Key and you should be good to go!

One thing to note.
The first endpoint, /api/user/anyone, by looking at the UI, it seems like you need to be authenticated/authorized to call it because of the lock? That is simply not true since the action method does not have any Authorize attribute specified. It seems to be a hot potato in the Swagger community and I have currently not found a solution to this problem...maybe in the next blog post? :)

AWS Lambda + Python + PIP Packages = true

$
0
0

Intro

I'm in the process of moving all my stuff from Azure to AWS. One of the things I currently run in Azure is a twitter bot that tweets every hour (it even made the local news a couple of years ago...).
Currently it's beeing triggered by a cron job on a VM. It has worked perfectly fine for 6 years now, but since I'm moving stuff and don't wan't to maintain a VM anymore, I decided to "convert it" to run in a AWS Lambda instead.

Solution

The bot is written in Python and has two pip dependencies, twitter and pytz.

Code changes

Before the code looked liked this:
tweet.py

from twitter import OAuth, Twitter
from datetime import datetime, time
import pytz, random

TOKEN = "SECRET"
TOKEN_KEY = "SECRET"
API_KEY = "SECRET"
API_SECRET = "SECRET"

MY_TZ = pytz.timezone('Europe/Stockholm')
now = datetime.now(MY_TZ).time()
retries = 0
def get_current_hour(timestamp):
    if timestamp.hour == 0 or timestamp.hour == 12:
        return 12
    return timestamp.hour % 12

def compose_tweet(timestamp):
    number_of_rings = get_current_hour(timestamp)
    # Creates a tweet, e.g. "BONG BONG BONG #03:00
    alert_sound = get_bell_sound(retries)
    tweet = " ".join([alert_sound] * number_of_rings)
    hashtag = "#%s:%s" %(str(timestamp.hour).zfill(2), str(timestamp.minute).zfill(2))
    return "%s %s" %(tweet, hashtag)

def send_tweet(tweet):
    global retries
    auth = OAuth(TOKEN, TOKEN_KEY, API_KEY, API_SECRET)
    t = Twitter(auth=auth)
    try:
        t.statuses.update(status=tweet)
        retries = 0
    except:
        retries += 1
        if retries <= 7:
            main()
        else:
            raise

def get_bell_sound(index):
    sounds = ('BONG', 'DONG', 'DING', 'BING-BONG', 'RING', 'PING', 'JINGLE', 'DING-DONG')
    return sounds[index]

def main(timestamp = now):
    send_tweet(compose_tweet(timestamp))

if __name__ == "__main__":
    main()

To make it "Lambda compatible", I only needed to add the following method

def lambda_handler(event, context):
    main()

The lambda_handler function is called by the lambda runtime. You get access to both the event that triggered the function and also a context. I don't need any of it though.

Packaging

Here the documentation lacked a little when it comes to how to deploy a python function with external (pip) dependencies. It wasn't that hard to figure out but it took me like 30 minutes that I could have spent on something else, so that's why I'm writing this post :).

Lambda supports uploading a zip file, so let's create one. It needs to contain the pip packages and the entrypoint, in our case the entrypoint is tweet.py.

The easiest way dealing with PIP imo is to have a requirements.txt file, so let's create one.

requirements.txt

twitter==1.18.0
pytz==2018.5

Here we have specified which packages and what version to use. I use quite old packages, I don't know if newer versions work and I don't have time to try it out either, so bare with me.

After creating a requirements.txt, we can install the packages to the current folder like this:

pip install -r requirements.txt -t .

This will create the following folder structure:
folderstructure

Now the only thing left to do is create the zip file.
I added the following files/folders to the zip file:
zipfile
Then I uploaded it by choosing Upload a .zip file.
upload-aws
The UI will now look like this:
aws-uploaded
Here I've also specified the entrypoint for the function by entering tweet.lambda_handler in the Handler input field (tweet is the name of the file and lambda_handler is the name of the method).

Scheduling/triggering

Now the only thing that's left is to make the function run every hour (08:00, 09:00 and so on).
It's really easy, all we need to do is add a CloudWatch Event Trigger with the following schedule expression: cron(0 * * * ? *).

That's it, we're now tweeting from the (aws) cloud!

C# - Sealed and internal - how to use in unit test?

$
0
0

The problem

I'm using the Firebase Admin dotnet sdk to send push notifications. They have the following method that allows you to send multiple notifications with one request:

public async Task<BatchResponse> SendAllAsync(IEnumerable<Message> messages)
{
    return await this.SendAllAsync(messages, false).ConfigureAwait(false);
}

The SendAllAsync method returns a Batchresponse that will tell you how many notifications that were sent successfully and how many that failed. Usually when a notification fails to deliver it's because of the token used has expired or been revoked.
Whenever I receive a response that tells me that the token is invalid I want to remove that token from the database so I don't end up with a bunch of expired/invalid push tokens.

The logic is quite simple

var response = await _firebaseMessaging.SendAllAsync(messages);
return HandleResponse(response);

I then have the following code that acts on the response:

// This is MY code that takes a BatchResponse (from the Google library).
public async Task<Result> HandleResponse(Batchresponse response)
{
    if (response.SuccessCount > 0 && response.FailureCount == 0)
    {
        return Result.Ok();
    }
    
    if (response.FailureCount > 0)
    {
       var invalidTokens = ExtractInvalidTokens(response);
       await _removeTokensCommand(invalidTokens);
       return Result.Fail();
    }
    ...
}

You get the idea, if we have any failed tokens, remove them.
So, seems quite easy to test this, right?

Well, turns out that BatchResponse looks like this:

public sealed class BatchResponse
{
    internal BatchResponse(IEnumerable<SendResponse> responses)
    {
        // some irrelevant code here
    }
    
    public IReadOnlyList<SendResponse> Responses { get; }
    public int SuccessCount { get; }
    public int FailureCount => this.Responses.Count - this.SuccessCount;
}

It's sealed and the constructor is internal, meaning we can not create a new instance of it like this:

var batchResponse = new BatchResponse(new List<SendResponse>());

What to do?

The solution

Reflection to the rescue.

By using reflection it's possible to do...basically whatever you want, you can set private fields, you can call methods that are not public and so on.

It goes without saying but you should probably not rely on what I'm about to show you in your production code. Classes are (almost) always sealed for a good reason, same goes with internal constructors, the developers clearly don't want you to create instances of their classes (for whatever reason).

One simple solution in my case would be to change the signature of my HandleResponse method to the following...

Task<Result> HandleResponse(int successCount, int failureCount)

...or just map it to some domain object that I control.

But, HOW FUN IS THAT?

This is what I came up with:

public async Task GivenOneSuccessAndZeroFailures_WhenHandleResponse_ThenReturnsOkResult()
{
    // Create the instances
    var sendResponse = CreateInstance<SendResponse>(
        BindingFlags.NonPublic | BindingFlags.Instance,
        new[] { typeof(string) },
        new object[] {"foo"});
    var batchResponse = CreateInstance<BatchResponse>(
        BindingFlags.NonPublic | BindingFlags.Instance,
        new[] { typeof(IEnumerable<SendResponse>) },
        new object[] { new List<SendResponse>{ sendResponse }});
        
    // Pass the mocks to our code
    var result = await sut.HandleResponse(batchResponse);
    
    result.Success.ShouldBeTrue();
}

private static T CreateInstance<T>(BindingFlags bindingFlags, Type[] types, object[] ctorParams)
{
    var type = typeof(T);
    var constructor = type.GetConstructor(
        bindingFlags,
        null,
        types,
        Array.Empty<ParameterModifier>()) ?? throw new Exception("This didn't work...");
        
    return (T)constructor.Invoke(ctorParams);
}
  1. Get the relevant constructor
  2. Invoke the constructor
  3. Cast the result to T
  4. BOOM, we've just created an instance of a sealed class without any public constructors.

Swagger - Tips and Tricks - Part 2

$
0
0

Exclude certain properties from the Swagger UI

The problem

Sometimes you have some properties on your request model that you don't want to show in the Swagger UI, for whatever reason.

The solution

I'm using Swashbuckle 5.0.
Start by creating the following attribute.
SwaggerIgnorePropertyAttribute.cs

[AttributeUsage(AttributeTargets.Property)]
public class SwaggerIgnorePropertyAttribute : Attribute
{
}

Next, we need to create a new SchemaFilter. It will get all properties for the given type from the SchemaFilterContext, only selecting the ones with the SwaggerIgnorePropertyAttribute.

SwaggerExcludePropertySchemaFilter.cs

public class SwaggerExcludePropertySchemaFilter : ISchemaFilter
{
    public void Apply(OpenApiSchema schema, SchemaFilterContext context)
    {
        if (schema?.Properties == null)
        {
            return;
        }

        var excludedProperties = context.Type.GetProperties().Where(t => t.GetCustomAttribute<SwaggerIgnorePropertyAttribute>() != null);

        foreach (var excludedProperty in excludedProperties)
        {
            var propertyToRemove = schema.Properties.Keys.SingleOrDefault(x => string.Equals(x, excludedProperty.Name, StringComparison.OrdinalIgnoreCase));

            if (propertyToRemove != null)
            {
                schema.Properties.Remove(propertyToRemove);
            }
        }
    }
}

Then we need to register the SchemaFilter like this:

services.AddSwaggerGen(c =>
{
    .....
    c.SchemaFilter<SwaggerExcludePropertySchemaFilter>();
    ....
}

Demo

Given the following OrderInput, the Swagger UI will show two properties, Name and Optional (really good name, right?).

public class OrderInput
{
    public string Name { get; set; }
    public IDictionary<string, object> Optional { get; set; }   
}

orders-before

We wan't to hide the Optional property so we add the SwaggerIgnoreProperty attribute like this...

public class OrderInput
{
    public string Name { get; set; }
    [SwaggerIgnoreProperty]
    public IDictionary<string, object> Optional { get; set; }   
}

orders-after

...and now it does not show up anymore.

This solution is inspired by this answer by Richard on Stack Overflow. I've updated it to work with the latest Swashbuckle release.

HttpClient - Error handling, a test driven approach

$
0
0

Intro

When I wrote my last blog post about HttpClient (You're (probably still) using HttpClient wrong and it is destabilizing your software) I promised to do a post about error handling since I intentionally left that out.

This post will be somewhat of a journey, we will start with some happy path code where error handling is non existent and (hopefully) end up in a much better place.

One note before we start, the code in this post will use a class called Result. Think about it as something like a monad, Vladimir Khorikov has a great post on this topic.
I've used a modified version of this Result class for years with great success in a lot of different projects. It has a few benefits in my opinion:

  1. You don't need to rely on exceptions.
  2. You are forced to think about (and handle) failures.
  3. It makes the code a lot easier to read.

Simple example:

var result = await _something.GetData();

if (result.Success)
{
    return result.Data;
}
else if(result.Retryable)
{
   // Handle retry
}
else
{
   // Handle fail
   _logger.LogError(result.Message, result.Errors);
}

A note about Polly

Polly is a great tool that will help you dealing with timeouts, exceptions, retries and so on when using HttpClient. I've choosen NOT to use Polly in this post, simply because I believe that it's important to understand what happens behind the scenes of such a library before using it. I will do a follow up on this post where I use Polly, don't worry. :)

The problem

We want to fetch some information about a GitHub repository from some (imaginary) API. The API is really unstable and we need to handle all of the different errors.
This is the code we have to work with, let's call it...

...The Happy Path.

public class GithubHttpClient
{
    private readonly HttpClient _httpClient;

    public GithubHttpClient(HttpClient httpClient)
    {
        _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
    }

    public async Task<Result<GitHubRepositoryDto>> GetRepository(int id)
    {
        var request = new HttpRequestMessage(HttpMethod.Get, $"api/v1/repositories/{id}");
        using (var response = await _httpClient.SendAsync(request))
        {
            var responseJson = await response.Content.ReadAsStringAsync();
            var repository = JsonConvert.DeserializeObject<GitHubRepositoryDto>(responseJson);
            return Result<GitHubRepositoryDto>.Ok(repository);
        }
    }
}

Nice, we've some code. It will send a GET request to the API, read the JSON from the response, deserialize it and return the result, GREAT.
Let's write a test and verify that everything works as it should.

Test

[Fact]
public async Task GivenSuccessfulResponseFromGitHub_WhenGetRepository_ThenReturnsOkResult()
{
    var responseBody = new GitHubRepositoryDto
    {
        Id = 1,
        Name = "jos.httpclient",
        Stars = 999999,
        Url = "https://github.com/joseftw/jos.httpclient"
    };
    var apiResponse = new HttpResponseMessage(HttpStatusCode.OK)
    {
        Content = new StringContent(JsonConvert.SerializeObject(responseBody))
    };
    var httpClient = new HttpClient(new MockedHttpMessageHandler(apiResponse))
    {
        BaseAddress = new Uri("http://localhost")
    };
    var sut = new GithubHttpClient(httpClient);

    var result = await sut.GetRepository(1);

    result.Success.ShouldBeTrue();
}
Outcome

Test SUCCEEDED.

So, what are we doing here?
We are creating a HttpResponseMessage with status code 200 OK and a body that contains a successful response from the API. We then create a new HttpClient with the overload that takes a HttpMessageHandler. The MockedHttpMessageHandler looks like this:

MockedHttpMessageHandler

public class MockedHttpMessageHandler : HttpMessageHandler
{
    private readonly Func<Task<HttpResponseMessage>> _responseFactory;
    private readonly HttpResponseMessage _httpResponseMessage;

    public MockedHttpMessageHandler(HttpResponseMessage httpResponseMessage)
    {
        _httpResponseMessage = httpResponseMessage;
    }

    public MockedHttpMessageHandler(Func<Task<HttpResponseMessage>> responseFactory)
    {
        _responseFactory = responseFactory ?? throw new ArgumentNullException(nameof(responseFactory));
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
        if (_responseFactory != null)
        {
            return await _responseFactory.Invoke();
        }

        return _httpResponseMessage;
    }
}

This allows us to control what the HttpClient methods returns.

Let's add another test that verifies that the the deserialization of the data works as well.

[Fact]
public async Task GivenSuccessfulResponseFromGitHub_WhenGetRepository_ThenReturnsSaidRepository()
{
    var responseBody = new GitHubRepositoryDto
    {
        Id = 1,
        Name = "jos.httpclient",
        Stars = 999999,
        Url = "https://github.com/joseftw/jos.httpclient"
    };
    var apiResponse = new HttpResponseMessage(HttpStatusCode.OK)
    {
        Content = new StringContent(JsonConvert.SerializeObject(responseBody))
    };
    var httpClient = new HttpClient(new MockedHttpMessageHandler(apiResponse))
    {
        BaseAddress = new Uri("http://localhost")
    };
    var sut = new GithubHttpClient(httpClient);

    var result = await sut.GetRepository(1);

    result.Data.Id.ShouldBe(1);
    result.Data.Name.ShouldBe("jos.httpclient");
    result.Data.Url.ShouldBe("https://github.com/joseftw/jos.httpclient");
    result.Data.Stars.ShouldBe(999999);
}

Let's focus on errors.

Null response content

What happens if the response content is null?

Test
[Fact]
public async Task GivenNullResponseContent_WhenGetRepository_ThenReturnsFailResult()
{
    var response = new HttpResponseMessage(HttpStatusCode.OK);
    var httpClient = new HttpClient(new MockedHttpMessageHandler(response))
    {
        BaseAddress = new Uri("http://localhost")
    };
    var sut = new GithubHttpClient(httpClient);

    var result = await sut.GetRepository(1);

    result.Success.ShouldBeFalse();
    result.Message.ShouldBe("Response from SendAsync was null");
}
Outcome

Test FAILED.
The test failed because of a NullReferenceException.
We are calling ReadAsStringAsync on the response content which is null, not good.

Fix

GitHubClient

public async Task<Result<GitHubRepositoryDto>> GetRepository(int id)
{
    var request = new HttpRequestMessage(HttpMethod.Get, $"api/v1/repositories/{id}");
    using (var response = await _httpClient.SendAsync(request))
    {
        if (response.Content == null)
        {
            return Result<GitHubRepositoryDto>.Fail("Response content was null");
        }

        var responseJson = await response.Content.ReadAsStringAsync();
        var repository = JsonConvert.DeserializeObject<GitHubRepositoryDto>(responseJson);
        return Result<GitHubRepositoryDto>.Ok(repository);
    }
}

We've added a check, if response.Content is null, we return a Fail result.

Empty response body

What happens if the response content is empty?

Test
[Fact]
public async Task GivenEmptyResponseContent_WhenGetRepository_ThenReturnsFailResult()
{
    var response = new HttpResponseMessage(HttpStatusCode.OK)
    {
        Content = new StringContent(string.Empty)
    };
    var httpClient = new HttpClient(new MockedHttpMessageHandler(response))
    {
        BaseAddress = new Uri("http://localhost")
    };
    var sut = new GithubHttpClient(httpClient);

    var result = await sut.GetRepository(1);

    result.Success.ShouldBeFalse();
    result.Message.ShouldBe("Failed to deserialize response");
}
Outcome

Test FAILED.
The test fails because...success is TRUE. (??)
Here's why:

var repository = JsonConvert.DeserializeObject<GitHubRepositoryDto>(responseJson);

We are passing an empty string to the DeserializeObject method, it will return null. We will come back to how to handle deserialization errors later, for now, let's just fix our code.

Fix
public async Task<Result<GitHubRepositoryDto>> GetRepository(int id)
{
    var request = new HttpRequestMessage(HttpMethod.Get, $"api/v1/repositories/{id}");
    using (var response = await _httpClient.SendAsync(request))
    {
        if (response.Content == null)
        {
            return Result<GitHubRepositoryDto>.Fail("Response content was null");
        }

        var responseJson = await response.Content.ReadAsStringAsync();
        var repository = JsonConvert.DeserializeObject<GitHubRepositoryDto>(responseJson);
        if (repository == null)
        {
            return Result<GitHubRepositoryDto>.Fail("Failed to deserialize response");
        }
        return Result<GitHubRepositoryDto>.Ok(repository);
    }
}

We've now added a null check, if repository is null, we return a Fail result.

Exceptions

What happens if we encounter some exceptions when calling the API?

Test
[Fact]
public async Task GivenExceptionWhenCallingTheApi_WhenGetRepository_ThenReturnsRetryResult()
{
    var httpClient = new HttpClient(new MockedHttpMessageHandler(() => throw new HttpRequestException("Boom from test")))
    {
        BaseAddress = new Uri("http://localhost")
    };
    var sut = new GithubHttpClient(httpClient);

    var result = await sut.GetRepository(1);

    result.Success.ShouldBeFalse();
    result.Retryable.ShouldBeTrue();
    result.Message.ShouldBe("Exception when calling the API");
}
Outcome

Test FAILED.
Turns out we are not handling exceptions at all, what a shocker.

Fix
public async Task<Result<GitHubRepositoryDto>> GetRepository(int id)
{
    var request = new HttpRequestMessage(HttpMethod.Get, $"api/v1/repositories/{id}");
    try
    {
        using (var response = await _httpClient.SendAsync(request))
        {
            if (response.Content == null)
            {
                return Result<GitHubRepositoryDto>.Fail("Response content was null");
            }

            var responseJson = await response.Content.ReadAsStringAsync();
            var repository = JsonConvert.DeserializeObject<GitHubRepositoryDto>(responseJson);
            if (repository == null)
            {
                return Result<GitHubRepositoryDto>.Fail("Failed to deserialize response");
            }
            return Result<GitHubRepositoryDto>.Ok(repository);
        }
    }
    catch (HttpRequestException exception)
    {
        _logger.LogError(exception, "HttpRequestException when calling the API");
        return Result<GitHubRepositoryDto>.Retry("HttpRequestException when calling the API");
    }
    catch (Exception exception)
    {
        _logger.LogError(exception, "Unhandled exception when calling the API");
        // Here it's up to you if you want to throw or return Retry/Fail, im choosing to FAIL.
        return Result<GitHubRepositoryDto>.Fail("Unhandled exception when calling the API");
     }
}

Here I've choosen to wrap everything in a try/catch. If an HttpRequestException happens, we choose to return a Retry Result.

If it's not an HttpRequestException, we choose to return a Fail Result. Here it's really up to you as a developer to make a choice, either you choose to return some kind of result (fail/retry) or you just log and throw it, it's up to you.

I've also added a test for exceptions != HttpRequestException:

[Fact]
public async Task GivenExceptionWhenCallingTheApi_WhenGetRepository_ThenReturnsRetryResult()
{
    var httpClient = new HttpClient(new MockedHttpMessageHandler(() => throw new Exception("Boom from test")))
    {
        BaseAddress = new Uri("http://localhost")
    };
    var sut = new GithubHttpClient(httpClient, _fakeLogger);

    var result = await sut.GetRepository(1);

    result.Success.ShouldBeFalse();
    result.Retryable.ShouldBeFalse();
    result.Message.ShouldBe("Unhandled exception when calling the API");
}

Great, we are now handling all kinds of exceptions, surely we must be done now?.
Nope, let's move on to...

...Status codes

If a project doesn't exists, the API will return a 404 with a different JSON body.
The body will look like this:

{
    "error": "The repository was not found"
}

Do we handle that already?

Test

[Fact]
public async Task GivenNotFoundResponseFromTheApi_WhenGetRepository_ThenReturnsFailResult()
{
    var response = new HttpResponseMessage(HttpStatusCode.NotFound)
    {
        Content = new StringContent("{\"error\": \"The repository was not found\"}")
    };
    var httpClient = new HttpClient(new MockedHttpMessageHandler(response))
    {
        BaseAddress = new Uri("http://localhost")
    };
    var sut = new GithubHttpClient(httpClient, _fakeLogger);

    var result = await sut.GetRepository(1);

    result.Success.ShouldBeFalse();
    result.Retryable.ShouldBeFalse();
    result.Message.ShouldBe("The repository was not found");
}
Outcome

Test FAILED because Success is...true?!

Turns out, our extremely naive deserialization strikes again. The deserialized repository is not null now, it's infact an instance of GitHubRepositoryDto where all properties have their default value.
That's bad.
We are still going to cover deserialization later, so let's just fix the code for now.

Fix

We only want to deserialize to a GitHubRepositoryDto if the response status code is 200 OK. If not, we will return a fail result.

public async Task<Result<GitHubRepositoryDto>> GetRepository(int id)
{
    var request = new HttpRequestMessage(HttpMethod.Get, $"api/v1/repositories/{id}");
    try
    {
        using (var response = await _httpClient.SendAsync(request))
        {
            if (response.StatusCode == HttpStatusCode.OK)
            {
                if (response.Content == null)
                {
                    return Result<GitHubRepositoryDto>.Fail("Response content was null");
                }

                var responseJson = await response.Content.ReadAsStringAsync();
                var repository = JsonConvert.DeserializeObject<GitHubRepositoryDto>(responseJson);
                if (repository == null)
                {
                    return Result<GitHubRepositoryDto>.Fail("Failed to deserialize response");
                }
                return Result<GitHubRepositoryDto>.Ok(repository);
            }

        return Result<GitHubRepositoryDto>.Fail(response.StatusCode == HttpStatusCode.NotFound
                ? "The repository was not found"
                : "Did not receive 200 OK status code");
        }
    }
    catch (HttpRequestException exception)
    {
        _logger.LogError(exception, "HttpRequestException when calling the API");
        return Result<GitHubRepositoryDto>.Retry("HttpRequestException when calling the API");
    }
    catch (Exception exception)
    {
        _logger.LogError(exception, "Unhandled exception when calling the API");
        // Here it's up to you if you want to throw or return Retry/Fail, im choosing to FAIL.
         return Result<GitHubRepositoryDto>.Fail("Unhandled exception when calling the API");
    }
}

I've added a check, if status code == 200 OK, then we deserialize. If not, we return a fail result. This is really naive and will be improved later in this post.

Retryable status codes

Right now, if we receive a status code that is not 200 OK, we return a Result.Fail result. But what if we receive a 500? 503? 408 and so on? If we recieve a status code that is worth retrying later on, I want to handle that by returning a Result.Retry.

Test
[Theory]
[InlineData(HttpStatusCode.InternalServerError)]
[InlineData(HttpStatusCode.GatewayTimeout)]
[InlineData(HttpStatusCode.RequestTimeout)]
[InlineData(HttpStatusCode.BadGateway)]
[InlineData(HttpStatusCode.TooManyRequests)]
public async Task GivenRetryableStatusCode_WhenGetRepository_ThenReturnsRetryResult(HttpStatusCode statusCode)
{
    var response = new HttpResponseMessage(statusCode);
    var httpClient = new HttpClient(new MockedHttpMessageHandler(response))
    {
        BaseAddress = new Uri("http://localhost")
    };
    var sut = new GithubHttpClient(httpClient, _fakeLogger);

    var result = await sut.GetRepository(1);

    result.Success.ShouldBeFalse();
    result.Retryable.ShouldBeTrue();
    result.Message.ShouldBe($"Received retryable status code '{statusCode}'");
}
Outcome

Test FAILED because Retryable == false.

Fix
public async Task<Result<GitHubRepositoryDto>> GetRepository(int id)
{
    var request = new HttpRequestMessage(HttpMethod.Get, $"api/v1/repositories/{id}");
    try
    {
        using (var response = await _httpClient.SendAsync(request))
        {
            if (response.StatusCode == HttpStatusCode.OK)
            {
                if (response.Content == null)
                {
                    return Result<GitHubRepositoryDto>.Fail("Response content was null");
                }

                var responseJson = await response.Content.ReadAsStringAsync();
                var repository = JsonConvert.DeserializeObject<GitHubRepositoryDto>(responseJson);
                if (repository == null)
                {
                    return Result<GitHubRepositoryDto>.Fail("Failed to deserialize response");
                }
                return Result<GitHubRepositoryDto>.Ok(repository);
            }

            if (RetryableStatusCodes.Contains(response.StatusCode))
            {
                return Result<GitHubRepositoryDto>.Retry($"Received retryable status code '{response.StatusCode}'");
            }

            return Result<GitHubRepositoryDto>.Fail(response.StatusCode == HttpStatusCode.NotFound
                ? "The repository was not found"
                : "Did not receive 200 OK status code");
            }
        }
        catch (HttpRequestException exception)
        {
            _logger.LogError(exception, "HttpRequestException when calling the API");
            return Result<GitHubRepositoryDto>.Retry("HttpRequestException when calling the API");
        }
        catch (Exception exception)
        {
            _logger.LogError(exception, "Unhandled exception when calling the API");
            // Here it's up to you if you want to throw or return Retry/Fail, im choosing to FAIL.
            return Result<GitHubRepositoryDto>.Fail("Unhandled exception when calling the API");
        }
}

Here I've introduced a HashSet<HttpStatusCode> and added a few status codes. If the set contains the response status code, I return Result.Retry.

Timeouts

What about timeouts? What happens if we can't get a response in reasonable time?
The default timeout for HttpClient is 100 seconds. In our case, that is NOT reasonable (quite frankly, I don't think it's ever reasonable...).
A more reasonable timeout would be something like 2-3 seconds in our case.

When a timeout occours, HttpClient throws a TimeoutException, so we will mimic the behaviour by throwing the exception from our MockedHttpMessageHandler.

Test
[Fact]
public async Task GivenTimeoutFromHttpClient_WhenGetRepository_ThenReturnsRetryResult()
{
    var httpClient = new HttpClient(new MockedHttpMessageHandler(() => throw new TimeoutException("Timeout from test")))
    {
        BaseAddress = new Uri("http://localhost"),
        Timeout = TimeSpan.FromMilliseconds(100) // This doesn't do anything in the test, just to show how to set the timeout.
    };
    var sut = new GithubHttpClient(httpClient, _fakeLogger);

    var result = await sut.GetRepository(1);

    result.Success.ShouldBeFalse();
    result.Retryable.ShouldBeTrue();
    result.Message.ShouldBe("TimeoutException during call to API");
}
Outcome

Test FAILED because Retryable == false.

Fix
public async Task<Result<GitHubRepositoryDto>> GetRepository(int id)
{
    var request = new HttpRequestMessage(HttpMethod.Get, $"api/v1/repositories/{id}");
    try
    {
        using (var response = await _httpClient.SendAsync(request))
        {
            if (response.StatusCode == HttpStatusCode.OK)
            {
                if (response.Content == null)
                {
                    return Result<GitHubRepositoryDto>.Fail("Response content was null");
                }

                var responseJson = await response.Content.ReadAsStringAsync();
                var repository = JsonConvert.DeserializeObject<GitHubRepositoryDto>(responseJson);
                if (repository == null)
                {
                    return Result<GitHubRepositoryDto>.Fail("Failed to deserialize response");
                }

                    return Result<GitHubRepositoryDto>.Ok(repository);
            }

            if (RetryableStatusCodes.Contains(response.StatusCode))
            {
                return Result<GitHubRepositoryDto>.Retry($"Received retryable status code '{response.StatusCode}'");
            }

            return Result<GitHubRepositoryDto>.Fail(response.StatusCode == HttpStatusCode.NotFound
                    ? "The repository was not found"
                    : "Did not receive 200 OK status code");
        }
    }
    catch (HttpRequestException exception)
    {
        _logger.LogError(exception, "HttpRequestException when calling the API");
        return Result<GitHubRepositoryDto>.Retry("HttpRequestException when calling the API");
    }
    catch (TimeoutException exception)
    {
        _logger.LogError(exception, "TimeoutException during call to API");
        return Result<GitHubRepositoryDto>.Retry("TimeoutException during call to API");
    }
    catch (Exception exception)
    {
        _logger.LogError(exception, "Unhandled exception when calling the API");
        // Here it's up to you if you want to throw or return Retry/Fail, im choosing to FAIL.
        return Result<GitHubRepositoryDto>.Fail("Unhandled exception when calling the API");
    }
}

Now we are catching TimeoutException and return a Retry result.

CancellationToken "timeouts"

Since we are working with async code we SHOULD be using CancellationTokens so let's add that.
Our GetRepositorymethod will now look like this:

public async Task<Result<GitHubRepositoryDto>> GetRepository(int id, CancellationToken cancellationToken)

We are also passing the CancellationToken to the HttpClient SendAsync method.

Test
[Fact]
public async Task GivenCanceledCancellationToken_WhenGetRepository_ThenReturnsRetryResult()
{
    var response = new HttpResponseMessage(HttpStatusCode.OK);
    var httpClient = new HttpClient(new MockedHttpMessageHandler(response))
    {
        BaseAddress = new Uri("http://localhost")
    };
    var cancelledCancellationToken = new CancellationToken(true);
    var sut = new GithubHttpClient(httpClient, _fakeLogger);

    var result = await sut.GetRepository(1, cancelledCancellationToken);

    result.Success.ShouldBeFalse();
    result.Retryable.ShouldBeTrue();
    result.Message.ShouldBe("Task was canceled during call to API");
}
[Fact]
public async Task GivenTaskCanceledException_WhenGetRepository_ThenReturnsRetryResult()
{
    var httpClient = new HttpClient(new MockedHttpMessageHandler(() => throw new TaskCanceledException("canceled from test")))
    {
        BaseAddress = new Uri("http://localhost")
    };
    var sut = new GithubHttpClient(httpClient, _fakeLogger);

    var result = await sut.GetRepository(1, CancellationToken.None);

    result.Success.ShouldBeFalse();
    result.Retryable.ShouldBeTrue();
    result.Message.ShouldBe("Task was canceled during call to API");
}
Outcome

Tests FAILED.

The tests failed because Retryable == false.
Note that we added 2 tests here, one that passes in a canceled CancellationToken and one that throws a TaskCanceledException. The reason for doing this is that our MockedHttpMessageHandler (and the standard one used by HttpClient afaik) uses this code:

cancellationToken.ThrowIfCancellationRequested()

That code throws a OperationCanceledException. But there is also a posibility that some code throws a TaskCanceledException instead, so we want to handle both cases.

Fix
public async Task<Result<GitHubRepositoryDto>> GetRepository(int id, CancellationToken cancellationToken)
{
    var request = new HttpRequestMessage(HttpMethod.Get, $"api/v1/repositories/{id}");
    try
    {
        using (var response = await _httpClient.SendAsync(request, cancellationToken))
        {
            if (response.StatusCode == HttpStatusCode.OK)
            {
                if (response.Content == null)
                {
                    return Result<GitHubRepositoryDto>.Fail("Response content was null");
                }

                var responseJson = await response.Content.ReadAsStringAsync();
                var repository = JsonConvert.DeserializeObject<GitHubRepositoryDto>(responseJson);
                if (repository == null)
                {
                    return Result<GitHubRepositoryDto>.Fail("Failed to deserialize response");
                }

                return Result<GitHubRepositoryDto>.Ok(repository);
            }

            if (RetryableStatusCodes.Contains(response.StatusCode))
            {
                return Result<GitHubRepositoryDto>.Retry($"Received retryable status code '{response.StatusCode}'");
            }

            return Result<GitHubRepositoryDto>.Fail(response.StatusCode == HttpStatusCode.NotFound
                    ? "The repository was not found"
                    : "Did not receive 200 OK status code");
        }
    }
    catch (HttpRequestException exception)
    {
        _logger.LogError(exception, "HttpRequestException when calling the API");
        return Result<GitHubRepositoryDto>.Retry("HttpRequestException when calling the API");
    }
    catch (TimeoutException exception)
    {
        _logger.LogError(exception, "TimeoutException during call to API");
        return Result<GitHubRepositoryDto>.Retry("TimeoutException during call to API");
    }
    catch (OperationCanceledException exception)
    {
        _logger.LogError(exception, "Task was canceled during call to API");
        return Result<GitHubRepositoryDto>.Retry("Task was canceled during call to API");
    }
    catch (Exception exception)
    {
        _logger.LogError(exception, "Unhandled exception when calling the API");
        // Here it's up to you if you want to throw or return Retry/Fail, im choosing to FAIL.
        return Result<GitHubRepositoryDto>.Fail("Unhandled exception when calling the API");
    }
}

Now we are catching OperationCanceledException and return a Retry result. TaskCanceledException inherits from OperationCanceledException.

Deserialization

Some might have noticed that we are deserializing the http response to a string. That is not best practices and goes against my previous post You're (probably still) using HttpClient wrong and it is destabilizing your software. I choosed to do that because I wanted to keep the examples simple. But now the time has come to tackle deserialization in a better way. I will create a deserializer (based on Newtonsoft) and inject it instead of using the static api. Here's what it will look like:

IJsonSerializer

public interface IJsonSerializer
{
    Task<Result<T>> TryDeserializeAsync<T>(Stream streamContent, CancellationToken cancellationToken);
    Task<T> DeserializeAsync<T>(Stream stream, CancellationToken cancellationToken);
}

MyJsonSerializer

public class MyJsonSerializer : IJsonSerializer
{
    private readonly JsonSerializer _jsonSerializer;
    public MyJsonSerializer()
    {
        _jsonSerializer = new JsonSerializer { ContractResolver = JsonSerializerSettings.ContractResolver };
    }
    public static readonly JsonSerializerSettings JsonSerializerSettings = new JsonSerializerSettings
    {
        ContractResolver = new CamelCasePropertyNamesContractResolver()
    };

    public async Task<Result<T>> TryDeserializeAsync<T>(Stream streamContent, CancellationToken cancellationToken) where T : class
    {
        try
        {
            var result = await DeserializeAsync<T>(streamContent, cancellationToken);
            return Result<T>.Ok(result);
        }
        catch (Exception e)
        {
            return Result<T>.Fail(e.Message);
        }
    }

    public async Task<T> DeserializeAsync<T>(Stream stream, CancellationToken cancellationToken)
    {
        using (var streamReader = new StreamReader(stream))
        using (var jsonReader = new JsonTextReader(streamReader))
        {
            var loaded = await JToken.LoadAsync(jsonReader, cancellationToken);
            return loaded.ToObject<T>(_jsonSerializer);
        }
    }
}
Test

[Fact]
public async Task GivenDeserializationError_WhenGetRepository_ThenReturnsFailResult()
{
    var response = new HttpResponseMessage(HttpStatusCode.OK)
    {
        Content = new StringContent("invalid json")
    };
    var httpClient = new HttpClient(new MockedHttpMessageHandler(response))
    {
        BaseAddress = new Uri("http://localhost")
    };
    var sut = new GitHubHttpClient(httpClient, faultyDeserializer, _fakeLogger);

    var result = await sut.GetRepository(1, CancellationToken.None);

    result.Success.ShouldBeFalse();
    result.Retryable.ShouldBeFalse();
    result.Message.ShouldBe("Failed to deserialize response");
}
Outcome

Test FAILED.
This test obviously failed because we are not using the MyJsonSerializer yet.

Fix

public async Task<Result<GitHubRepositoryDto>> GetRepository(int id, CancellationToken cancellationToken)
{
    var request = new HttpRequestMessage(HttpMethod.Get, $"api/v1/repositories/{id}");
    try
    {
        using (var response = await _httpClient.SendAsync(request, cancellationToken))
        {
            if (response.StatusCode == HttpStatusCode.OK)
            {
                if (response.Content == null)
                {
                    return Result<GitHubRepositoryDto>.Fail("Response content was null");
                }

                var responseStream = await response.Content.ReadAsStreamAsync();
                var deserializationResult = await _jsonDeserializer.TryDeserializeAsync<GitHubRepositoryDto>(responseStream, cancellationToken);

                return deserializationResult.Success
                        ? deserializationResult
                        : Result<GitHubRepositoryDto>.Fail($"Failed to deserialize stream to {nameof(GitHubRepositoryDto)}");
            }

            if (RetryableStatusCodes.Contains(response.StatusCode))
            {
                return Result<GitHubRepositoryDto>.Retry($"Received retryable status code '{response.StatusCode}'");
            }

            return Result<GitHubRepositoryDto>.Fail(response.StatusCode == HttpStatusCode.NotFound
                    ? "The repository was not found"
                    : "Did not receive 200 OK status code");
        }
    }
    catch (HttpRequestException exception)
    {
        _logger.LogError(exception, "HttpRequestException when calling the API");
        return Result<GitHubRepositoryDto>.Retry("HttpRequestException when calling the API");
    }
    catch (TimeoutException exception)
    {
        _logger.LogError(exception, "TimeoutException during call to API");
        return Result<GitHubRepositoryDto>.Retry("TimeoutException during call to API");
    }
    catch (OperationCanceledException exception)
    {
        _logger.LogError(exception, "Task was canceled during call to API");
        return Result<GitHubRepositoryDto>.Retry("Task was canceled during call to API");
    }
    catch (Exception exception)
    {
        _logger.LogError(exception, "Unhandled exception when calling the API");
        // Here it's up to you if you want to throw or return Retry/Fail, im choosing to FAIL.
        return Result<GitHubRepositoryDto>.Fail("Unhandled exception when calling the API");
    }
}

We've now replaced the static deserialization call with my new deserializer. Note that we're able to directly return the deserialization result since the TryDeserializeAsync has the same signature as the GetRepository method.

Great, we're now asynchronously deserializing the response...

...but how do we handle the following responses?

{}

or just an empty response like in our example earlier?

[Theory]
[InlineData("")]
[InlineData("{}")]
public async Task GivenInvalidJsonResponse_WhenGetRepository_ThenReturnsFailResult(string responseJson)
{
    var response = new HttpResponseMessage(HttpStatusCode.OK)
    {
        Content = new StringContent(responseJson)
    };
    var httpClient = new HttpClient(new MockedHttpMessageHandler(response))
    {
        BaseAddress = new Uri("http://localhost")
    };
    var sut = new GitHubHttpClient(httpClient, new MyJsonDeserializer(), _fakeLogger);

    var result = await sut.GetRepository(1, CancellationToken.None);

    result.Success.ShouldBeFalse();
    result.Retryable.ShouldBeFalse();
    result.Message.ShouldBe("Failed to deserialize response");
}
Outcome

The test that passes an empty string succeeds, but the test passing {} fails.

The deserialization still does not think that the empty object is a problem and will happily
create a new GitHubRepositoryDtoinstance.

Fix
public class GitHubRepositoryDto
{
    public GitHubRepositoryDto(int? id, string name, string url, int? stars)
    {
        Id = id ?? throw new ArgumentNullException(nameof(id));
        Name = name ?? throw new ArgumentNullException(nameof(name));
        Url = url ?? throw new ArgumentNullException(nameof(url));
        Stars = stars ?? throw new ArgumentNullException(nameof(stars));
    }

    public int Id { get; }
    public string Name { get; }
    public string Url { get; }
    public int Stars { get; }
}

By removing the setters and adding a constructor to the GitHubRepositoryDto we can add some simple "validation" to the dto. This is also why I'm still using Newtonsoft instead of System.Text.Json, as far as I know, System.Text.Json does not currently support this.

If we now run the test again, both passes.

Final thoughts

Some of you might think "DAMN that's a lot of code for just handling some simple http calls". Well, as you've seen, there's a lot of different things that can (and will) go wrong when dealing with http.

Also if you look at the code in GitHubHttpClient, it's really easy to make it generic and move it to some kind of BaseHttpClient and reuse it (just replace GitHubRepositoryDto with T). Usually I create a BaseHttpClient with different virtual methods so I can choose to override specific behaviour for different integrations, since no integration ever looks the same... :)

I'm NOT using HttpCompletionOption.ResponseHeadersRead in this post simply because I came across the following bug report. It boils down to that the ReadAsStringAsync, ReadAsStreamAsync and ReadAsByteArrayAsync does not provide CancellationToken support and can deadlock in combination with ResponseHeadersRead. It will be fixed in .NET 5.

Thank you for reading!

Viewing all 244 articles
Browse latest View live