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

How C# Records will change my life

$
0
0

Introduction

Whenever I code I prefer to have immutable domain models.
Example:

public class Game
{
    public Game(Team homeTeam, Team awayTeam, GameResult result)
    {
        HomeTeam = homeTeam ?? throw new ArgumentNullException(nameof(homeTeam));
        AwayTeam = awayTeam ?? throw new ArgumentNullException(nameof(awayTeam));
        Result = result ?? throw new ArgumentNullException(nameof(result));
    }

    public Team HomeTeam { get; }
    public Team AwayTeam { get; }
    public GameResult Result { get; }
}

Here you can see that it's only possible to set the properties when creating the object (get only properties).

By doing this you can make sure that you don't accidentally mutate the state of the object. This would not compile for example:

...
var game = new Game(homeTeam, awayTeam, result);
game.AwayTeam = new Team(Guid.NewGuid(), "Arsenal"); // Compilation error

Immutability comes with a lot of benefits, but sometimes it can be a bit cumbersome to deal with when you only want to update some properties. Since the object is immutable, you need to create a copy with all the existing values and the new updated one.

I will show you how Records in C# 9 will greatly simplify this

Let's set the scene

Let's say that we are building an application related to betting. A user can place bets on different games.

Our current domain model (before records) looks something like this (greatly simplified):

public class Bet
{
    public Bet(string id, string userId, IReadOnlyCollection<BetRow> rows)
    {
        Id = id ?? throw new ArgumentNullException(nameof(id));
        UserId = userId ?? throw new ArgumentNullException(nameof(userId));
        Rows = rows ?? throw new ArgumentNullException(nameof(rows));
    }

    public string Id { get; }
    public string UserId {get; }
    public IReadOnlyCollection<BetRow> Rows { get; }
}

public class BetRow
{
    public BetRow(string gameId, int homeScore, int awayScore) : this(gameId, homeScore, awayScore, BetRowOutcome.Unsettled)
    {
    }

    public BetRow(string gameId, int homeScore, int awayScore, BetRowOutcome outcome)
    {
        GameId = gameId ?? throw new ArgumentNullException(nameof(gameId));
        HomeScore = homeScore;
        AwayScore = awayScore;
        Outcome = outcome;
    }

    public string GameId { get; }
    public int HomeScore { get; }
    public int AwayScore { get; }
    public BetRowOutcome Outcome { get; }
}

A Bet contains a collection of BetRows and also some other data like Id and UserId. A BetRow also holds a BetRowOutcome property. This property will be updated when a game is finished.

public enum BetRowOutcome
{
    Unsettled,
    Win,
    Loss
}

Now we want to check if the user has won or not. We need to check and update the Outcome property for each BetRow.

Without records

I will now show you one of the "pain points" when it comes to immutability and c#. If we weren't using immutability, the following code would work. We just loop through the bet rows and update (mutate) the Outcome property.
BetService

public Bet SettleBet(Dictionary<string, Game> finishedGames, Bet bet)
{
    foreach (var betRow in bet.Rows)
    {
        var result = finishedGames[betRow.GameId].Result;

        if (betRow.HomeScore == result.HomeScore && betRow.AwayScore == result.AwayScore)
        {
            betRow.Outcome = BetRowOutcome.Win;
        }
        else
        {
            betRow.Outcome = BetRowOutcome.Loss;
        }
    }

    return bet;
}

Since the above code doesn't compile, we need to add a bit more code to make it work.

public class BetService
{
    public Bet SettleBet(Dictionary<string, Game> finishedGames, Bet bet)
    {
        var updatedBetRows = new List<BetRow>();
        foreach (var betRow in bet.Rows)
        {
            var result = finishedGames[betRow.GameId].Result;

            BetRowOutcome betRowOutcome;
            if (betRow.HomeScore == result.HomeScore && betRow.AwayScore == result.AwayScore)
            {
                betRowOutcome = BetRowOutcome.Win;
            }
            else
            {
                betRowOutcome = BetRowOutcome.Loss;
            }

            var updatedBetRow = new BetRow(betRow.GameId, betRow.HomeScore, betRow.AwayScore, betRowOutcome);
            updatedBetRows.Add(updatedBetRow);
        }

        return new Bet(bet.Id, bet.UserId, updatedBetRows);
    }
}
  1. We added a list that we use to keep track of the updated BetRows.
  2. Foreach row we create a new BetRow object and add it to the list.
  3. We return a new Bet with the updated rows.

This works great but imagine that your object has a bunch of different properties, you could easily end up with something like this:

return new Bet(
  bet.Id,
  bet.UserId,
  updatedBetRows,
  more,
  properties,
  here,
  updateThisAsWell,
  easy,
  to,
  forget,
  and,
  also,
  make,
  errors)

Hopefully you get my point. :)
Basically we want to avoid passing all the existing parameters to the constructor.

With records

I have now converted the following classes to records.

public record Bet
{
    public Bet(string id, string userId, IReadOnlyCollection<Record.BetRow> rows)
    {
        Id = id ?? throw new ArgumentNullException(nameof(id));
        UserId = userId ?? throw new ArgumentNullException(nameof(userId));
        Rows = rows ?? throw new ArgumentNullException(nameof(rows));
    }

    public string Id { get; }
    public string UserId { get; }
    public IReadOnlyCollection<Record.BetRow> Rows { get; init; }
}
public record BetRow
{
    public BetRow(string gameId, int homeScore, int awayScore) : this(gameId, homeScore, awayScore, BetRowOutcome.Unsettled)
    {
    }

    public BetRow(string gameId, int homeScore, int awayScore, BetRowOutcome outcome)
    {
        GameId = gameId ?? throw new ArgumentNullException(nameof(gameId));
        HomeScore = homeScore;
        AwayScore = awayScore;
        Outcome = outcome;
    }

    public string GameId { get; }
    public int HomeScore { get; }
    public int AwayScore { get; }
    public BetRowOutcome Outcome { get; init; }
}

The key thing here is that I changed class to record and added the init keyword to the Rows and Outcome properties. This allows me to do the following:
BetService

public Bet SettleBet(Dictionary<string, Game> finishedGames, Bet bet)
{
    var updatedBetRows = new List<Record.BetRow>();
    foreach (var betRow in bet.Rows)
    {
        var result = finishedGames[betRow.GameId].Result;

       BetRowOutcome betRowOutcome;
       if (betRow.HomeScore == result.HomeScore && betRow.AwayScore == result.AwayScore)
       {
           betRowOutcome = BetRowOutcome.Win;
       }
       else
       {
           betRowOutcome = BetRowOutcome.Loss;
       }

       var updatedBetRow = betRow with { Outcome = betRowOutcome }; // No more new
       updatedBetRows.Add(updatedBetRow);
    }
    return bet with { Rows = updatedBetRows }; // No more new
}

Now I can use the with keyword and only specify the properties that I would like to update. I will not mutate the existing Bet object, I will get a completely new object with updated Rows, all other properties will keep their old values.

This was just a quick example of how to use records, if you want to read more about records have a look here


Heartbeat logger in Dotnet Core

$
0
0
Heartbeat logger in Dotnet Core

Photo by Jair Lázaro / Unsplash

Introduction

It's important to monitor your applications so that you can act fast when problems occurs. When it comes to web applications it's common to have a /health endpoint that responds back with the status of your application. Usually a response indicates that everything is OK. If no response (timeout) or a 500 Internal Server Error response usually means that you have a problem. When using Kubernetes, Docker or basically any cloud provider you can also choose to spin up new instances of your application if any of your running instances responds with a non OK status.

In ASP.NET Core, Microsoft provides a health check middleware that can help you with creating your own health checks.

There's a bunch of third party tools like Pingdom, Uptime Robot and Site 24x7 that can help you keep track of your applications health. What they all have in common is that they send a HTTP request to your health endpoint and then you can configure alarms/notifications based on the response status code.

You can say that it's somewhat of an "outside in approach".

  1. Call to your service from the "outside".
  2. Act on the response (or lack of).

But what if you want to monitor your non web applications (console applications etc)? What if you don't have a web server?

The "inside out approach"

This approach relies on that you are indexing your logs in some kind of log aggregator like ELK, Splunk or Application Insights/Azure Monitor.

The idea is that your application logs a heartbeat every x seconds and then you configure some alarm that will trigger if 1 or more heartbeats has been missed. No heartbeats == application is down/having some problems.

It's really simple to set this up when using dotnet core, I will create a HostedService, or in this case, use the Background Service class.

public class HeartbeatLoggerHostedService : BackgroundService
{
    private readonly string _applicationName;
    private readonly TimeSpan _interval;
    private readonly ILogger<HeartbeatLoggerHostedService> _logger;

    public HeartbeatLoggerHostedService(
        ILogger<HeartbeatLoggerHostedService> logger,
        IConfiguration configuration)
    {
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        _applicationName = configuration.GetValue<string>("Application:Name") ?? throw new Exception("Missing 'Application:Name' in Configuration");
        _interval = configuration.GetValue<TimeSpan?>("Logging:Heartbeat:Interval") ?? throw new Exception("Missing 'Logging:Heartbeat:Interval' in Configuration");
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            _logger.LogInformation("Heartbeat from {ApplicationName}", _applicationName);
            await Task.Delay(_interval, stoppingToken);
        }
    }
}

The logic here is really simple, I create a while loop that will log a heartbeat and then wait x seconds before logging the next heartbeat.

Then I just register the service in the DI container like this:

.ConfigureServices((context, services) =>
{
    ...
    services.AddHostedService<HeartbeatLoggerHostedService>();
    ...
})

That's all there is to it, now when I run my application a heartbeat will be logged every 30 second.
Heartbeat logger in Dotnet Core

What's great is that this approach will work for both web and console applications. I tend to use a combination of heartbeat logging and health endpoints to monitor my web applications.

Dotnet pack - include referenced projects

$
0
0
Dotnet pack - include referenced projects

Photo taken by Vance Osterhout

Scenario

  1. You provide a nuget package to x amount of different customers.
  2. You want to share common code between the different nuget packages.
  3. You don't want your shared code to be a nuget package on it's own.
  4. You've the following project structure:

Dotnet pack - include referenced projects

On your build server you have separate pipelines for the different clients where you run the following code:

dotnet pack JOS.MyNugetPackage.Client1 -c Release

Everything builds just fine and you ship the package to your client and call it a day, great.

There's a catch though...the client can't install the nuget package.
When they try to install the package they get the following error:

Restoring packages for C:\utv\projects\JOS.DotnetPackIncludeBaseLibrary\src\Client1App\Client1App.csproj...
  GET https://api.nuget.org/v3-flatcontainer/jos.mynugetpackage.core/index.json
  NotFound https://api.nuget.org/v3-flatcontainer/jos.mynugetpackage.core/index.json 658ms
Installing JOS.MyNugetPackage.Client1 1.0.0.
NU1101: Unable to find package JOS.MyNugetPackage.Core. No packages exist with this id in source(s): Dotnet Roslyn, Local feed, Microsoft Visual Studio Offline Packages, nuget.org
Package restore failed. Rolling back package changes for 'Client1App'.
Time Elapsed: 00:00:01.5727356

That's weird? It can't find the JOS.MyNugetPackage.Core nuget package?
And that's the problem right there. When running dotnet pack and having references to other projects, the following nuspec file will be produced:

<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
  <metadata>
    <id>JOS.MyNugetPackage.Client1</id>
    <version>1.0.0</version>
    <authors>JOS.MyNugetPackage.Client1</authors>
    <requireLicenseAcceptance>false</requireLicenseAcceptance>
    <description>Package Description</description>
    <dependencies>
      <group targetFramework=".NETStandard2.0">
        <dependency id="JOS.MyNugetPackage.Core" version="1.0.0" exclude="Build,Analyzers" />
      </group>
    </dependencies>
  </metadata>
</package>

We have a dependency on JOS.MyNugetPackage.Core, as expected right? Well, the problem here is that a dependency in a nuspec file == a nuget package.

The dependencies element within metadata contains any number of dependency elements that identify other packages upon which the top-level package depends.

And since a JOS.MyNugetPackage.Core nuget package doesn't exists the restore fails.

This issue is currently discussed in a couple of different GitHub issues and it seems like many people want this feature.

I encourage you to read the comments in both issues, they contain many great tips and insights.

The first ticket has been open for almost 4 years (!!!) so I don't expect a solution to this problem anytime soon.

Workarounds

It's important to realize that this is workarounds, both solutions have their own pros and cons.

There's a couple of workarounds linked in the GitHub issues, let's try them out.

"The csproj approach"

This workaround was posted in the comments by "teroenko". So let's update the JOS.MyNugetPackage.Client1 csproj.

Note: I'll be honest here, without the comments provided I would've been somewhat lost. Custom build targets and stuff like that in MSBuild has always been (more or less) a black box for me.

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\JOS.MyNugetPackage.Core\JOS.MyNugetPackage.Core.csproj" PrivateAssets="All" />
  </ItemGroup>

  <PropertyGroup>
    <TargetsForTfmSpecificBuildOutput>$(TargetsForTfmSpecificBuildOutput);CopyProjectReferencesToPackage</TargetsForTfmSpecificBuildOutput>
  </PropertyGroup>

  <Target Name="CopyProjectReferencesToPackage" DependsOnTargets="BuildOnlySettings;ResolveReferences">
    <ItemGroup>
      <!-- Filter out unnecessary files -->
      <_ReferenceCopyLocalPaths Include="@(ReferenceCopyLocalPaths->WithMetadataValue('ReferenceSourceTarget', 'ProjectReference')->WithMetadataValue('PrivateAssets', 'All'))"/>
    </ItemGroup>

    <!-- Print batches for debug purposes -->
    <Message Text="Batch for .nupkg: ReferenceCopyLocalPaths = @(_ReferenceCopyLocalPaths), ReferenceCopyLocalPaths.DestinationSubDirectory = %(_ReferenceCopyLocalPaths.DestinationSubDirectory) Filename = %(_ReferenceCopyLocalPaths.Filename) Extension = %(_ReferenceCopyLocalPaths.Extension)" Importance="High" Condition="'@(_ReferenceCopyLocalPaths)' != ''" />

    <ItemGroup>
      <!-- Add file to package with consideration of sub folder. If empty, the root folder is chosen. -->
      <BuildOutputInPackage Include="@(_ReferenceCopyLocalPaths)" TargetPath="%(_ReferenceCopyLocalPaths.DestinationSubDirectory)"/>
    </ItemGroup>
  </Target>
</Project>

It's important that you add the PrivateAssets="All" to your project reference since that's what we filter on in the CopyProjectReferencesToPackage step above.

<ProjectReference Include="..\JOS.MyNugetPackage.Core\JOS.MyNugetPackage.Core.csproj" PrivateAssets="All" /> 

If we now try to publish our nuget package again the produced nuspec will look like this:

<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2012/06/nuspec.xsd">
  <metadata>
    <id>JOS.MyNugetPackage.Client1</id>
    <version>1.3.0</version>
    <authors>JOS.MyNugetPackage.Client1</authors>
    <requireLicenseAcceptance>false</requireLicenseAcceptance>
    <description>Package Description</description>
    <dependencies>
      <group targetFramework=".NETStandard2.0" />
    </dependencies>
  </metadata>
</package>  

Now we don't have any dependency to JOS.MyNugetPackage.Core and if we check the lib folder in the npkg file, we can see that the JOS.MyNugetPackage.Core.dll file is included.
Dotnet pack - include referenced projects

Client1 can now install the package and use it.
Example

namespace Client1App
{    
    class Program
    {
        static void Main(string[] args)
        {
            var companyQuery = new GetCompanyNameQuery();
            var result = companyQuery.Execute();
            Console.WriteLine(result);
        }
    }
}

Outputs...

This is Client 1, brought to you by My Awesome Company

Pros

It works...

Cons

You need to edit your csproj and add PrivateAssets=All to your references. This can mess up references in your test projects for example.

"The nuspec approach"

Let's get Client 2 up and running. They don't want to update their csproj file. By creating the following nuspec file we are basically saying "copy all the dll files starting with JOS.MyNugetPackage. in the bin\Release\netstandard2.0\ folder to the lib\netstandard2.0 folder in the nuget package.

<?xml version="1.0"?>
<package >
  <metadata>
    <id>JOS.MyNugetPackage.Client2</id>
    <version>1.4.0</version>
    <title>$title$</title>
    <authors>Josef Ottosson</authors>
    <owners>Josef Ottosson</owners>
    <description>Really awesome package</description>
    <dependencies>
      <group targetFramework="netstandard2.0" />
    </dependencies>
  </metadata>
   <files>
     <file src="bin\Release\netstandard2.0\JOS.MyNugetPackage.*.dll" target="lib\netstandard2.0" />
  </files>
</package>

If we then use dotnet pack like this...

dotnet pack -c Release -p:NuspecFile=JOS.MyNugetPackage.Client2.nuspec

...the following nuspec will be produced:

<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2012/06/nuspec.xsd">
  <metadata>
    <id>JOS.MyNugetPackage.Client2</id>
    <version>1.4.0</version>
    <title></title>
    <authors>Josef Ottosson</authors>
    <owners>Josef Ottosson</owners>
    <requireLicenseAcceptance>false</requireLicenseAcceptance>
    <description>Really awesome package</description>
    <dependencies>
      <group targetFramework=".NETStandard2.0" />
    </dependencies>
  </metadata>
</package> 

Again, no references to the JOS.MyNugetPackage.Core package anywhere.
And if we look in the lib folder, it contains both the JOS.MyNugetPackage.Core.dll and JOS.MyNugetPackage.Client2.dll.
Dotnet pack - include referenced projects

Pros

You don't need to add PrivateAssets=All to your csproj.

Cons

If you start targeting more frameworks/rename stuff, you need to update the paths etc.

Final thoughts

To me, the nuspec solution feels cleaner. It's important to note though that you need to keep it up to date regarding target framework, paths etc.
IMO this is something that should work out of the box but it seems like Microsoft wants everything to be a (public) nuget package.

What if you actually created the JOS.MyNugetPackage.Core package and published it to your internal company nuget feed?
Your client would still not be able to restore the package since they can't access your internal feed...
Should you in that case ship both packages? And setup a new pipeline only for your Core project? Nah, doesn't make much sense to me :)

I've created a working example that contains the two different approaches that you can find here

Custom Dictionary JsonConverter for System.Text.Json

$
0
0
Custom Dictionary<string, object> JsonConverter for System.Text.Json

Image taken by Romain Vignes

Introduction

A couple of months ago we (more or less unknowingly) started to use System.Text.Json instead of Newtonsoft in one of our ASP.NET Core applications.

The application is responsible for sending emails, so it takes a payload, modifies it a bit and then passes it on to SendGrid. One property in the payload, templateParameters is a Dictionary<string, object> that we use for... template parameters. Basically, the caller can send in whatever they want and then use those parameters when creating the email template. By using a Dictionary<string, object, we don't need to do any code changes when the email template changes. Obviously this is how Hello {{firstName}} errors happens but that's another story.

The bug

This had worked flawlessly until we received the following bug report from one of our customers

I got a really weird email, it looked like this:
Hello {"name": {"ValueKind":3},"data":{"ValueKind":1}}

Now, I recognized the ValueKind stuff straight away since I'd played around with the new System.Text.Json serializer before. And sure enough, it turns out that we are not using Newtonsoft anymore, we are now using System.Text.Json instead. Again, all the details of how this happened can be found in this GitHub issue.

Basically what happend for us was this:

  1. System.Text.Json was used instead of Newtonsoft when modelbinding occurred
  2. We serialized the Dictionary<string, object> (created by System.Text.Json) with Newtonsoft
  3. The following json output was produced:
{
  "name": {
    "ValueKind":3
  },
  "data":{
    "ValueKind":1
  }
}

Oooops.

It turns out that System.Text.Json and Newtonsoft differs in how they treat object by default. System.Text.Json tries to not make any assumptions at all of what type it should deserialize to when it comes to object.

Current functionality treats any object parameter as JsonElement when deserializing. The reason is that we don't know what CLR type to create, and decided as part of the design that the deserializer shouldn't "guess".

IMO that is fair enough but that caused us some problems (especially since we didn't knowingly enabled System.Text.Json :D ).

Solution

Prerequisite: Because of reasons, the serializer used for serializing the request towards SendGrid must use Newtonsoft for now. In the near future we will update that serializer to also use System.Text.Json, but for now, it can't be changed.

Newtonsoft solution

Now, the easiest solution would be to just switch over to Newtonsoft by adding a reference to Microsoft.AspNetCore.Mvc.NewtonsoftJson and then do this:
services.AddMvc().AddNewtonsoftJson();

So if you are not interested in using System.Text.Json, you can stop reading now.

System.Text.Json solution

So, we need to be able to replicate the behaviour of Newtonsoft when deserializing/model binding.

We have the following action method:

[HttpPost]
public IActionResult CustomProperty(MyInput data)
{
    // Do some work here...
    return new OkResult();
}

MyInput looks like this:

public class MyInput
{
    public string Name { get; set; }
    public Dictionary<string, object> Data { get; set; }
}

Now, if we send the following request to our API...

{
  "name": "whatevs",
  "data": {
    "firstName": "Josef"
  }
}

...and inspect it in the debugger, it will look like this:
As you can see, the items in the Dictionary<string, object> are wrapped in a JsonElement.
Custom Dictionary<string, object> JsonConverter for System.Text.Json

We need to customize the deserialization of Dictionary<string, object>.
We will do this by creating a custom JsonConverter. Fortunately, the documentation of how to do this provided by Microsoft is really good, so let's get into it.

public class DictionaryStringObjectJsonConverter : JsonConverter<Dictionary<string, object>>
{
    public override Dictionary<string, object> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (reader.TokenType != JsonTokenType.StartObject)
        {
            throw new JsonException($"JsonTokenType was of type {reader.TokenType}, only objects are supported");
        }

        var dictionary = new Dictionary<string, object>();
        while (reader.Read())
        {
            if (reader.TokenType == JsonTokenType.EndObject)
            {
                return dictionary;
            }

            if (reader.TokenType != JsonTokenType.PropertyName)
            {
                throw new JsonException("JsonTokenType was not PropertyName");
            }

            var propertyName = reader.GetString();

            if (string.IsNullOrWhiteSpace(propertyName))
            {
                throw new JsonException("Failed to get property name");
            }

            reader.Read();

            dictionary.Add(propertyName, ExtractValue(ref reader, options));
        }

        return dictionary;
    }

    public override void Write(Utf8JsonWriter writer, Dictionary<string, object> value, JsonSerializerOptions options)
    {
        JsonSerializer.Serialize(writer, value, options);
    }

    private object ExtractValue(ref Utf8JsonReader reader, JsonSerializerOptions options)
    {
        switch (reader.TokenType)
        {
            case JsonTokenType.String:
                if (reader.TryGetDateTime(out var date))
                {
                    return date;
                }
                return reader.GetString();
            case JsonTokenType.False:
                return false;
            case JsonTokenType.True:
                return true;
            case JsonTokenType.Null:
                return null;
            case JsonTokenType.Number:
                if (reader.TryGetInt64(out var result))
                {
                    return result;
                }
                return reader.GetDecimal();
            case JsonTokenType.StartObject:
                return Read(ref reader, null, options);
            case JsonTokenType.StartArray:
                var list = new List<object>();
                while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)
                {
                    list.Add(ExtractValue(ref reader, options));
                }
                return list;
            default:
                throw new JsonException($"'{reader.TokenType}' is not supported");
        }
    }
}

We start by implementing the Read and Write method. The Read method will be used for deserialization and the Write method is used when serializing.

Some notes:

  • If it's an object, we recursively call ExtractValue.
  • If it's an array, we create a list and read until the end of the array. For each item in the array we call ExtractValue .
  • If the string can be parsed to a valid DateTime we return a DateTime, otherwise we return the string as is.
  • If a number can be parsed to a valid long we return that, otherwise we return a decimal.

You get the point, we can choose exactly how we want to handle the different types of properties.

Time to test our custom converter.
There's a couple of different ways to register our custom converter. I will use the JsonConverterAttribute.

MyInput will now look like this:

public class MyInput
{
    public string Name { get; set; }
        
    [JsonConverter(typeof(DictionaryStringObjectJsonConverter))]
    public Dictionary<string, object> Data { get; set; }
}

If we now send the same payload and inspect the input, it will look like this:
Custom Dictionary<string, object> JsonConverter for System.Text.Json.

Great success :)

Let's try it with a more complex payload:

{
  "name": "whatevs",
  "data": {
    "string": "string",
    "int": 1,
    "bool": true,
    "date": "2020-01-23T00:01:02Z",
    "decimal": 12.345,
    "null": null,
    "array": [1, 2, 3],
    "objectArray": [{
        "string": "string",
	    "int": 1,
        "bool": true
      },
      {
        "string": "string2",
        "int": 2,
        "bool": true
      }
    ],
    "object": {
      "string": "string",
      "int": 1,
      "bool": true
    }
  }
}

Custom Dictionary<string, object> JsonConverter for System.Text.Json

Full DictionaryStringObjectJsonConverter

In my case I didn't need to care about the Write method since we are using Newtonsoft for serializing the request towards SendGrid. But we still need to do something in that method so we just call the Serialize method on the JsonSerializer.

public class DictionaryStringObjectJsonConverter : JsonConverter<Dictionary<string, object>>
{
    .......
    
    public override void Write(Utf8JsonWriter writer, Dictionary<string, object> value, JsonSerializerOptions options)
    {
       JsonSerializer.Serialize(writer, value, options);
    }
}

When doing that you rely on the default behaviour of System.Text.Json. If you want/need more control, you can do something like this:

public override void Write(Utf8JsonWriter writer, Dictionary<string, object> value, JsonSerializerOptions options)
{
    writer.WriteStartObject();

    foreach (var key in value.Keys)
    {
        HandleValue(writer, key, value[key]);
    }

    writer.WriteEndObject();
}

private static void HandleValue(Utf8JsonWriter writer, string key, object objectValue)
{
    if (key != null)
    {
        writer.WritePropertyName(key);
    }

    switch (objectValue)
    {
        case string stringValue:
            writer.WriteStringValue(stringValue);
            break;
        case DateTime dateTime:
            writer.WriteStringValue(dateTime);
            break;
        case long longValue:
            writer.WriteNumberValue(longValue);
            break;
        case int intValue:
            writer.WriteNumberValue(intValue);
            break;
        case float floatValue:
            writer.WriteNumberValue(floatValue);
            break;
        case double doubleValue:
            writer.WriteNumberValue(doubleValue);
            break;
        case decimal decimalValue:
            writer.WriteNumberValue(decimalValue);
            break;
        case bool boolValue:
            writer.WriteBooleanValue(boolValue);
            break;
        case Dictionary<string, object> dict:
             writer.WriteStartObject();
             foreach (var item in dict)
             {
                 HandleValue(writer, item.Key, item.Value);
             }
             writer.WriteEndObject();
             break;
        case object[] array:
            writer.WriteStartArray();
            foreach (var item in array)
            {
                HandleValue(writer, item);
            }
            writer.WriteEndArray();
            break;
        default:
            writer.WriteNullValue();
            break;
    }
}

private static void HandleValue(Utf8JsonWriter writer, object value)
{
    HandleValue(writer, null, value);
}

We do more or less the same thing as in the Read method, we choose how to handle the different types of properties.

Bonus - ModelBinder

If you want to use a Dictionary<string, object> without wrapping it in a model you can use a custom model binder like this:

[HttpPost]
public IActionResult Default([ModelBinder(typeof(DictionaryObjectJsonModelBinder))] Dictionary<string, object> data)
{
    return new OkObjectResult(data);
}
public class DictionaryObjectJsonModelBinder : IModelBinder
{
    private readonly ILogger<DictionaryObjectJsonModelBinder> _logger;

    private static readonly JsonSerializerOptions DefaultJsonSerializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.General)
    {
        Converters = { new DictionaryStringObjectJsonConverter()}
    };

    public DictionaryObjectJsonModelBinder(ILogger<DictionaryObjectJsonModelBinder> logger)
    {
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    public async Task BindModelAsync(ModelBindingContext bindingContext)
    {
        if (bindingContext == null)
        {
            throw new ArgumentNullException(nameof(bindingContext));
        }

        if (bindingContext.ModelType != typeof(Dictionary<string, object>))
        {
            throw new NotSupportedException($"The '{nameof(DictionaryObjectJsonModelBinder)}' model binder should only be used on Dictionary<string, object>, it will not work on '{bindingContext.ModelType.Name}'");
        }

        try
        {
            var data = await JsonSerializer.DeserializeAsync<Dictionary<string, object>>(bindingContext.HttpContext.Request.Body, DefaultJsonSerializerOptions);
            bindingContext.Result = ModelBindingResult.Success(data);
        }
        catch (Exception e)
        {
            _logger.LogError(e, "Error when trying to model bind Dictionary<string, object>");
            bindingContext.Result = ModelBindingResult.Failed();
        }
    }
}

Bonus - Benchmarks

Deserialization

BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19041.450 (2004/?/20H1)
Intel Core i7-8750H CPU 2.20GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores
.NET Core SDK=5.0.100-preview.7.20366.6
  [Host]        : .NET Core 5.0.0 (CoreCLR 5.0.20.36411, CoreFX 5.0.20.36411), X64 RyuJIT
  .NET Core 5.0 : .NET Core 5.0.0 (CoreCLR 5.0.20.36411, CoreFX 5.0.20.36411), X64 RyuJIT

Job=.NET Core 5.0  Runtime=.NET Core 5.0  InvocationCount=1  
UnrollFactor=1  

|              Method |     Mean |    Error |   StdDev |   Median | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated |
|-------------------- |---------:|---------:|---------:|---------:|------:|--------:|------:|------:|------:|----------:|
|             Default | 29.14 μs | 0.859 μs | 2.394 μs | 28.00 μs |  1.00 |    0.00 |     - |     - |     - |   2.15 KB |
| CustomJsonConverter | 21.59 μs | 0.758 μs | 2.163 μs | 20.60 μs |  0.75 |    0.09 |     - |     - |     - |   3.62 KB |

Our custom JsonConverter is faster than using the default behaviour, we allocate a bit more though.

Serialization

BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19041.450 (2004/?/20H1)
Intel Core i7-8750H CPU 2.20GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores
.NET Core SDK=5.0.100-preview.7.20366.6
  [Host]        : .NET Core 5.0.0 (CoreCLR 5.0.20.36411, CoreFX 5.0.20.36411), X64 RyuJIT
  .NET Core 5.0 : .NET Core 5.0.0 (CoreCLR 5.0.20.36411, CoreFX 5.0.20.36411), X64 RyuJIT

Job=.NET Core 5.0  Runtime=.NET Core 5.0  

|                                         Method |     Mean |     Error |    StdDev | Ratio |  Gen 0 | Gen 1 | Gen 2 | Allocated |
|----------------------------------------------- |---------:|----------:|----------:|------:|-------:|------:|------:|----------:|
|                                        Default | 3.107 μs | 0.0108 μs | 0.0101 μs |  1.00 | 0.2022 |     - |     - |     960 B |
|            DictionaryStringObjectJsonConverter | 1.049 μs | 0.0083 μs | 0.0078 μs |  0.34 | 0.0935 |     - |     - |     440 B |
| DictionaryStringObjectJsonConverterCustomWrite | 1.052 μs | 0.0119 μs | 0.0111 μs |  0.34 | 0.0935 |     - |     - |     440 B |

Using the custom JsonConverter when serializing are both faster and allocates less than the default behaviour. There's no difference between the DictionaryStringObjectJsonConverter and DictionaryStringObjectJsonConverterCustomWrite converters.

All code (together with benchmarks and tests) can be found on GitHub.

CSVHelper - Read column value into a List property

$
0
0
CSVHelper - Read column value into a List property

Photo by Mika Baumeister

When working with C# and CSV files, I always use CSVHelper. It's a small library for reading and writing CSV files. Extremely fast, flexible, and easy to use. I couldn't agree more, it's simply awesome :).

Now, I ran into a rather delicate problem a couple of days ago.
Say that you a CSV file looking like this:

id,categories
1234,"food,pasta,meat"
4321,"food,beer,alcohol"

You then have a class that you want to map the data to:

public class GoodFoodCsvRow
{
    public string Id { get; set; }
    public List<string> Categories { get; set; }
}

And your CSVHelper Map looks like this:

public class GoodFoodCsvRowMap : ClassMap<GoodFoodCsvRow>
{
    public GoodFoodCsvRowMap()
    {
        Map(x => x.Id).Index(0);
        Map(x => x.Categories).Index(1);
    }
}

If you now were to read said CSV file like this...

using (var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead))
{
    response.EnsureSuccessStatusCode();
    using (var responseStream = await response.Content.ReadAsStreamAsync())
    {
        using (var reader = new StreamReader(responseStream))
        {
            using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture))
            {
                csv.Configuration.HasHeaderRecord = true;
                csv.Configuration.RegisterClassMap<GoodFoodCsvRowMap>();
                var rows = csv.GetRecords<GoodFoodCsvRow>();
                return rows.ToDictionary(x => x.Id, x => x.Categories);
            }
        }
    }
}

...you would notice that the Categories list only contains one string looking like this:
CSVHelper - Read column value into a List property

That's not what we want, we want each category to be a separate item in the Categories list.

Luckily, it's really easy to achieve using CSVHelper, we just need to update our GoodFoodCsvRowMap so it looks like this:

public class GoodFoodCsvRowMap : ClassMap<GoodFoodCsvRow>
{
    public GoodFoodCsvRowMap()
    {
        Map(x => x.Id).Index(0);
        Map(x => x.Categories).Index(1).ConvertUsing(row =>
        {
            var columnValue = row.GetField<string>("categories");
            return columnValue?.Split(',')?.ToList() ?? new List<string>();
        });
    }
}

Now when we run it again the problem will be solved:
CSVHelper - Read column value into a List property.

C# - Throttle outgoing http requests - Concurrent

$
0
0
C# - Throttle outgoing http requests - Concurrent

Photo by Denys Nevozhai

Introduction

This is the first part in a series I'm planning to write about how to consume http apis with rate limiting in mind. This post will focus on how to handle rate limit when the limit is set using concurrent requests. The next post will focus on "request per seconds" kind of rate limiting.

The problem

We need to fetch weather data for 10 000 different locations. We need to fetch the data as fast as possible. The API only allows 10 requests at the same time. If we send more than that they will respond with the 429 Too Many Requests status code.

Prerequisite

This is the code that fetches the data from the external API, it will not change during our different approaches. As you can see, we have no error handling except the EnsureSuccessStatusCode call, if you are interested in how to handle common errors when using HttpClient, check out my post about that here

public class DummyApiHttpClient
{
    private static readonly JsonSerializerOptions DefaultJsonSerializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.General)
    {
        PropertyNameCaseInsensitive = true
    };

    private readonly HttpClient _httpClient;

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

    public async Task<IReadOnlyCollection<WeatherForecastResponse>> GetWeatherForecast(string location)
    {
        using var request = new HttpRequestMessage(HttpMethod.Get, $"/weatherforecast?location={location}");
        using var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);

        response.EnsureSuccessStatusCode();

        await using var responseContent = await response.Content.ReadAsStreamAsync();

        return await JsonSerializer.DeserializeAsync<IReadOnlyCollection<WeatherForecastResponse>>(responseContent, DefaultJsonSerializerOptions);
    }
}

WeatherForecaseResponse

public class WeatherForecastResponse
{
    public DateTime Date { get; set; }
    public int TemperatureC { get; set; }
    public string Summary { get; set; }
}

Fake API

I've created a fake API with the following controller

[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    private static readonly List<WeatherForecast> Forecasts = new List<WeatherForecast>
    {
        new WeatherForecast
        {
            Date = DateTime.UtcNow.Date,
            TemperatureC = 30,
            Summary = "Freezing"
        },
        new WeatherForecast
        {
            Date = DateTime.UtcNow.AddDays(1).Date,
            TemperatureC = 30,
            Summary = "Freezing"
        },
        new WeatherForecast
        {
            Date = DateTime.UtcNow.AddDays(2).Date,
            TemperatureC = 30,
            Summary = "Freezing"
        },
        new WeatherForecast
        {
            Date = DateTime.UtcNow.AddDays(3).Date,
            TemperatureC = 30,
            Summary = "Freezing"
        },
        new WeatherForecast
        {
            Date = DateTime.UtcNow.AddDays(4).Date,
            TemperatureC = 30,
            Summary = "Freezing"
        },
    };
        
    [HttpGet]
    public async Task<IEnumerable<WeatherForecast>> Get()
    {
        await Task.Delay(25);
        return Forecasts;
    }
}

Approaches

Sequential

We start of with the most basic approach. Given a list of locations, we just loop through them one by one and call the API sequentially.

public class GetWeatherForecastQuerySequentially : IGetWeatherForecastQuery
{
    private readonly DummyApiHttpClient _dummyApiHttpClient;

    public GetWeatherForecastQuerySequentially(DummyApiHttpClient dummyApiHttpClient)
    {
        _dummyApiHttpClient = dummyApiHttpClient ?? throw new ArgumentNullException(nameof(dummyApiHttpClient));
    }

    public async Task<IReadOnlyCollection<WeatherForecastResponse>> Execute(IReadOnlyList<string> locations)
    {
        var responses = new List<IReadOnlyCollection<WeatherForecastResponse>>(locations.Count);

        for (var i = 0; i < locations.Count; i++)
        {
            var result = await _dummyApiHttpClient.GetWeatherForecast(locations[i]);
            responses.Add(result);
        }

        return responses.SelectMany(x => x).ToArray();
    }
}

Straight forward solution:

  1. Call the API
  2. Add the response to the responses list
  3. Flatten the responses and return a new array.

When dealing with the amount of data that we are dealing with (20 000 locations), this is a really slow approach. Imagine that each call takes 1 second...it quickly ends up being horribly slow.

No limits

Don't use this code in production.
This is our first, naive, approach to speed things up.

public class GetWeatherForecastQueryNoLimits : IGetWeatherForecastQuery
{
    private readonly DummyApiHttpClient _dummyApiHttpClient;

    public GetWeatherForecastQueryNoLimits(DummyApiHttpClient dummyApiHttpClient)
    {
        _dummyApiHttpClient = dummyApiHttpClient ?? throw new ArgumentNullException(nameof(dummyApiHttpClient));
    }

    public async Task<IReadOnlyCollection<WeatherForecastResponse>> Execute(IReadOnlyList<string> locations)
    {
        var responses = new ConcurrentBag<IReadOnlyCollection<WeatherForecastResponse>>();
        var tasks = locations.Select(async location => {
            var response = await _dummyApiHttpClient.GetWeatherForecast(location);
                responses.Add(response);
        });
        await Task.WhenAll(tasks);

        return responses.SelectMany(x => x).ToArray();
    }
}
  1. We create a ConcurrentBag where we will store all responses.
  2. We create a task for each location -> fetch the response.
  3. We add the response to the ConcurrentBag.
  4. We wait for all the calls to complete with Task.WhenAll
  5. We flatten the responses and return a new array.

This code is not throttled in any way and you can run into all kind of problems by using this code (running out of resources, getting rate limited...). Don't use it.

SemaphoreSlim

This approach is inspired by this answer found on Stack Overflow.

SemaphoreSlim represents a lightweight alternative to Semaphore that limits the number of threads that can access a resource or pool of resources concurrently.

We can't use a regular lock here since it doesn't work with await.

public async Task<IReadOnlyCollection<WeatherForecastResponse>> Execute(IReadOnlyList<string> locations)
{
    var semaphoreSlim = new SemaphoreSlim(
          initialCount: 10,
          maxCount: 10);
    var responses = new ConcurrentBag<IReadOnlyCollection<WeatherForecastResponse>>();

    var tasks = locations.Select(async location =>
    {
        await semaphoreSlim.WaitAsync();

        try
        {
            var response = await _dummyApiHttpClient.GetWeatherForecast(location);
            responses.Add(response);
        }
        finally
        {
            semaphoreSlim.Release();
        }
    });

    await Task.WhenAll(tasks);
    return responses.SelectMany(x => x).ToArray();
}
  1. We create a new SempahoreSlim where we set initialCount and maxCount to 10. This is how we throttle our requests.
  2. We create a ConcurrentBag that will hold the forecast responses.
  3. For each location we create a task which represents the fetching of the forecast.
  4. We wait until the SemaphoreSlim allows us to "enter".
  5. We add the response to the ConcurrentBag.
  6. We then call Release on the SemaphoreSlim which means that a new request is now allowed to be sent.
  7. We wait for all tasks to finish
  8. We flatten the list and return a new array.

TransformBlock

Another solution to the problem is to use a TransformBlock.

public class GetWeatherForecastQueryTransformBlock : IGetWeatherForecastQuery
{
    private readonly DummyApiHttpClient _dummyApiHttpClient;

    public GetWeatherForecastQueryTransformBlock(DummyApiHttpClient dummyApiHttpClient)
    {
        _dummyApiHttpClient = dummyApiHttpClient ?? throw new ArgumentNullException(nameof(dummyApiHttpClient));
    }

    public async Task<IReadOnlyCollection<WeatherForecastResponse>> Execute(IReadOnlyList<string> locations)
    {
        var transformBlock = new TransformBlock<string, IReadOnlyCollection<WeatherForecastResponse>>(
            _dummyApiHttpClient.GetWeatherForecast,
            new ExecutionDataflowBlockOptions
            {
                MaxDegreeOfParallelism = 10
            }
        );

        var buffer = new BufferBlock<IReadOnlyCollection<WeatherForecastResponse>>();
        transformBlock.LinkTo(buffer);
        for (var i = 0; i < locations.Count; i++)
        {
            await transformBlock.SendAsync(locations[i]);
        }

        transformBlock.Complete();
        await transformBlock.Completion;

        return buffer.TryReceiveAll(out var forecasts)
            ? forecasts.SelectMany(x => x).ToArray()
            : throw new Exception("Error when trying to receive items from Buffer");
    }
}

You can think of the TransformBlock as a message queue.

  1. We create a new TransformBlock. We specify 10 as MaxDegreeOfParallelism, this is how we throttle our requests.
  2. We create a BufferBlock and link it to the TransformBlock. The BufferBlock will hold our responses.
  3. We loop through all locations and call SendAsync on the TransformBlock. This publishes a "message" to the TransformBlock, basically queueing up the work that should be done. Note that we are passing the location as a parameter.
  4. We then call Complete on the transformBlock, meaning that it should not accept any more "messages".
  5. We then wait for all the "messages" to be processed.
  6. We then try to get all "messages" from the buffer and flatten it to a new array.

Benchmarks

We will now run a couple of different benchmarks to see how each version performs.

10 Locations

|         Method |      Mean |     Error |   StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated |
|--------------- |----------:|----------:|---------:|------:|--------:|------:|------:|------:|----------:|
|   Sequentially | 309.23 ms |  41.27 ms | 2.262 ms |  1.00 |    0.00 |     - |     - |     - |   48.8 KB |
| TransformBlock |  32.24 ms | 114.33 ms | 6.267 ms |  0.10 |    0.02 |     - |     - |     - |  58.97 KB |
|  SemaphoreSlim |  28.92 ms |  33.52 ms | 1.837 ms |  0.09 |    0.01 |     - |     - |     - |  53.59 KB |
|       NoLimits |  43.75 ms |  44.22 ms | 2.424 ms |  0.14 |    0.01 |     - |     - |     - |   53.8 KB |

As one would expect, the sequential approach is really slow.

100 Locations

|         Method |       Mean |    Error |   StdDev | Ratio | Gen 0 | Gen 1 | Gen 2 | Allocated |
|--------------- |-----------:|---------:|---------:|------:|------:|------:|------:|----------:|
|   Sequentially | 3,199.8 ms | 647.2 ms | 35.48 ms |  1.00 |     - |     - |     - | 487.42 KB |
| TransformBlock |   318.8 ms | 167.0 ms |  9.15 ms |  0.10 |     - |     - |     - | 509.91 KB |
|  SemaphoreSlim |   328.1 ms | 471.2 ms | 25.83 ms |  0.10 |     - |     - |     - | 495.21 KB |
|       NoLimits |   216.0 ms | 465.4 ms | 25.51 ms |  0.07 |     - |     - |     - | 487.07 KB |

The "NoLimits" approach is still faster than the TransformBlock and SemaphoreSlim since it's not throttled at all.

1000 Locations

|         Method |     Mean |    Error |   StdDev | Ratio |     Gen 0 | Gen 1 | Gen 2 | Allocated |
|--------------- |---------:|---------:|---------:|------:|----------:|------:|------:|----------:|
|   Sequentially | 31.492 s | 3.6081 s | 0.1978 s |  1.00 | 1000.0000 |     - |     - |   4.52 MB |
| TransformBlock |  3.197 s | 0.2569 s | 0.0141 s |  0.10 | 1000.0000 |     - |     - |   4.86 MB |
|  SemaphoreSlim |  3.224 s | 0.7269 s | 0.0398 s |  0.10 | 1000.0000 |     - |     - |   4.84 MB |
|       NoLimits |  2.301 s | 0.2653 s | 0.0145 s |  0.07 | 1000.0000 |     - |     - |   4.75 MB |

Same story here, as expected.

10 000 Locations

|         Method |     Mean |   Error |  StdDev | Ratio | RatioSD |      Gen 0 |     Gen 1 | Gen 2 |  Allocated |
|--------------- |---------:|--------:|--------:|------:|--------:|-----------:|----------:|------:|-----------:|
|   Sequentially | 318.11 s | 9.195 s | 0.504 s |  1.00 |    0.00 | 10000.0000 | 3000.0000 |     - | 47664384 B |
| TransformBlock |  32.05 s | 2.752 s | 0.151 s |  0.10 |    0.00 | 10000.0000 | 1000.0000 |     - | 50970464 B |
|  SemaphoreSlim |  32.03 s | 2.216 s | 0.121 s |  0.10 |    0.00 | 10000.0000 | 1000.0000 |     - | 50812704 B |
|       NoLimits |       NA |      NA |      NA |     ? |       ? |          - |         - |     - |          - |

What happend here? Well it turns out that my computer really didn't like spawning (and running) 10 000 tasks at the same time. I've tried it multiple times but I can't get any results for the NoLimits method. Either my benchmark crashes or my DummyAPI. This is why you should not use this method in production. By not limiting your code you are at risk of either crashing your own application or the API that you are consuming.

Conclusions

  • When dealing with multiple I/O operations that can be processed concurrently, you should really do so. The Sequential approach is really really slow (as expected).
  • Don't use the NoLimits approach.
  • There is not that much of a difference between the TransformBlock and the SemaphoreSlim approach. Personally I think that the SemaphoreSlim approach is a bit easier to read.

All code can be found on GitHub.

Tidy up your HttpClient usage

$
0
0
Tidy up your HttpClient usage

Photo by CDC

Introduction

This is just a quick tip for anyone using HttpClient and wants to avoid some of the boilerplate (error handling, logging etc).

Usually my implementations looks something like this:

public class DummyApiHttpClient
{
    private static readonly JsonSerializerOptions DefaultJsonSerializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.General)
    {
        PropertyNameCaseInsensitive = true
    };

    private readonly HttpClient _httpClient;
    private readonly ILogger<DummyApiHttpClient> _logger;

    public DummyApiHttpClient(HttpClient httpClient, ILogger<DummyApiHttpClient> logger)
    {
        _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    public async Task<IReadOnlyCollection<WeatherForecastResponse>> GetWeatherForecast(string location)
    {
        using var request = new HttpRequestMessage(HttpMethod.Get, $"/weatherforecast?location={location}");
        var stopwatch = Stopwatch.StartNew();
        try
        {    
            using var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
            stopwatch.Stop();
            response.EnsureSuccessStatusCode();
            await using var responseContent = await response.Content.ReadAsStreamAsync();

            return await JsonSerializer.DeserializeAsync<IReadOnlyCollection<WeatherForecastResponse>>(responseContent, DefaultJsonSerializerOptions);
        }
        catch (Exception exception) when (exception is TaskCanceledException || (exception is TimeoutException))
        {
            stopwatch.Stop();
            _logger.LogError("Timeout during {RequestMethod} to {RequestUri} after {ElapsedMilliseconds}ms", request.Method, request.RequestUri?.ToString(), stopwatch.ElapsedMilliseconds);
            throw;
        }
        catch (Exception e)
        {
            stopwatch.Stop();
            _logger.LogError(e, "Error during {HttpMethod} to {RequestUri} after {ElapsedMilliseconds}ms", request.Method, request.RequestUri?.ToString());
            throw;
        }
    }
}
  • We create a HttpRequestMessage.
  • Start a Stopwatch.
  • Ensuring that the status code is successful.
  • Getting the response content and deserialize it.
  • Catch exceptions -> log -> throw

This is completely fine but I find it a bit tedious to write the same code over and over again.

One solution is to create a base class and inherit from it but in my experience that is almost never the best solution. You might want to use a different HttpCompletionOption in one specific call, or you might want to read the response body as a string instead of a stream in another and so on. It can quickly be a mess with a bunch of optional properties and so on.

So let's try another way.

DelegatingHandler

Previously I've written about using a custom HttpMessageHandler during unit testing, now the time has come to use a DelegatingHandler.

There's a bunch of different posts about this topic already so I will not cover what they are or how they work. I will just show you how to use them. If you want to get a more in depth understanding of DelegatingHandler I recommend this post by Steve Gordon.

Let's get into it. We will create a custom DelegatingHandler that will handle:

  • Logging.
  • Timing (stopwatch).
  • Ensuring a successful http status code.
public class DefaultHttpDelegatingHandler : DelegatingHandler
{
    private readonly ILogger<DefaultHttpDelegatingHandler> _logger;

    public DefaultHttpDelegatingHandler(ILogger<DefaultHttpDelegatingHandler> logger)
    {
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        _logger.LogInformation("Sending {RequestMethod} request towards {Request}", request.Method, request?.RequestUri?.ToString());
        var stopwatch = Stopwatch.StartNew();
        HttpResponseMessage response = null;
        try
        {
            response = await base.SendAsync(request, cancellationToken);
            stopwatch.Stop();
            _logger.LogInformation("Response took {ElapsedMilliseconds}ms {StatusCode}", response.StatusCode, stopwatch.ElapsedMilliseconds, response.StatusCode);
            response.EnsureSuccessStatusCode();
            return response;
        }
        catch (Exception exception) when (exception is TaskCanceledException || (exception is TimeoutException))
        {
            stopwatch.Stop();
            _logger.LogError(exception,
                "Timeout during {RequestMethod} to {RequestUri} after {ElapsedMilliseconds}ms {StatusCode}",
                request.Method,
                request.RequestUri?.ToString(),
                stopwatch.ElapsedMilliseconds,
                response?.StatusCode);
            throw;
        }
        catch (Exception exception)
        {
            stopwatch.Stop();
            _logger.LogError(exception,
                "Exception during {RequestMethod} to {RequestUri} after {ElapsedMilliseconds}ms {StatusCode}",
                request.Method,
                request.RequestUri?.ToString(),
                stopwatch.ElapsedMilliseconds,
                response?.StatusCode);
            throw;
        }
    }
}
  1. We log some information about the request we are sending
  2. We start a new stopwatch
  3. We call base.SendAsync (next DelegatingHandler in the pipeline)
  4. We stop the stopwatch and log the time taken
  5. Log errors -> throw

To use our DefaultHttpDelegatingHandler we need to change the registration of our DummyApiHttpClient.

...

services.AddTransient<DefaultHttpDelegatingHandler>();
services.AddHttpClient<DummyApiHttpClient>((client) =>
{
    client.BaseAddress = new Uri("http://localhost:5000");
}).AddHttpMessageHandler<DefaultHttpDelegatingHandler>();
...

The important part here is the call to AddHttpMessageHandler.

We can now remove some code from our DummyApiHttpClient and make it a bit cleaner:

public class DummyApiHttpClient
{
    private static readonly JsonSerializerOptions DefaultJsonSerializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.General)
    {
        PropertyNameCaseInsensitive = true
    };

    private readonly HttpClient _httpClient;

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

    public async Task<IReadOnlyCollection<WeatherForecastResponse>> GetWeatherForecast(string location)
    {
        using var request = new HttpRequestMessage(HttpMethod.Get, $"/weatherforecast?location={location}");
        using var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
        await using var responseContent = await response.Content.ReadAsStreamAsync();
        return await JsonSerializer.DeserializeAsync<IReadOnlyCollection<WeatherForecastResponse>>(responseContent, DefaultJsonSerializerOptions);
    }
}

We can then choose if we want to handle the exceptions here or not, I will just let it propagate further up the chain and deal with it there, for example in a custom exception handler.

The code is now a bit cleaner, the only "drawback" is that it is now not immediately clear that we are logging and timing all requests since that logic is now hidden away in a custom DelegatingHandler, but I think thats a fair trade-off.

This was just a quick tip demonstrating how to use a custom DelegatingHandler. Remember that you can do all kind of things with this, you can chain a bunch of custom DelegatingHandlers for example, one could be responsible for timing, another one for logging...you get the idea :)

All code used in this post can be found here.

MongoDB - Missing discriminator array when doing upserts with UpdateOneModel

$
0
0

The problem

Given the following two classes:

[BsonDiscriminator(RootClass = true)]
[BsonKnownTypes(typeof(MongoDbSomeModel))]
[BsonIgnoreExtraElements]
public class MongoDbBaseModel
{
    [BsonId]
    public string Id { get; set; }
    public string Name { get; set; }
}

public class MongoDbSomeModel : MongoDbBaseModel
{
    public string SomeProperty { get; set; }
}

When inserting a MongoDbSomeModel to the database, it should contain a _t field in the database with the following value:

_t: ["MongoDbBaseModel", "MongoDbSomeModel"]

Let's test this by running the following code:

var settings = MongoClientSettings.FromConnectionString($"mongodb://username:MyPassword@localhost");
settings.ConnectTimeout = TimeSpan.FromSeconds(5);
var client = new MongoClient(settings);
await client.DropDatabaseAsync("test");
var database = client.GetDatabase("test");
var collection = database.GetCollection<MongoDbSomeModel>("test");
var bulkOperations = new List<WriteModel<MongoDbSomeModel>>();

var items = new List<MongoDbSomeModel>
{
    new MongoDbSomeModel{Id = "4", Name = "Name 4 INSERT", SomeProperty = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss")},
    new MongoDbSomeModel{Id = "5", Name = "Name 5 INSERT", SomeProperty = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss")},
    new MongoDbSomeModel{Id = "6", Name = "Name 6 INSERT", SomeProperty = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss")}
};

foreach (var record in items)
{
    var insertOperation = new InsertOneModel<MongoDbSomeModel>(record);
    bulkOperations.Add(insertOperation);
}

await collection.BulkWriteAsync(bulkOperations);

We create some items and save them to the database in a BulkWrite. If we look in the database, we can see that the _t field is present and populated with the correct values.

with-types

Now, imagine that I would like to do an upsert instead. If the document already exist, I only want to update the existing properties. The reason for using UpdateOneModel instead of ReplaceOneModel is that I write to the same document from multiple applications. If I use a ReplaceOneModel, all the data coming from the other applications would be removed. One could argue that this is not optimal (writing to the same document from multiple applications) but I have my reasons, period. :)

So, the code using UpdateOneModel looks like this:

var settings = MongoClientSettings.FromConnectionString($"mongodb://username:MyPassword@localhost");
settings.ConnectTimeout = TimeSpan.FromSeconds(5);
var client = new MongoClient(settings);
await client.DropDatabaseAsync("test");
var database = client.GetDatabase("test");
var collection = database.GetCollection<MongoDbSomeModel>("test");
var bulkOperations = new List<WriteModel<MongoDbSomeModel>>();

var items = new List<MongoDbSomeModel>
{
    new MongoDbSomeModel{Id = "1", Name = "Name 1 UPSERT", SomeProperty = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss")},
    new MongoDbSomeModel{Id = "2", Name = "Name 2 UPSERT", SomeProperty = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss")},
    new MongoDbSomeModel{Id = "3", Name = "Name 3 UPSERT", SomeProperty = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss")}
};

foreach (var record in items)
{
    var updateDefinition = new UpdateDefinitionBuilder<MongoDbSomeModel>()
        .Set(x => x.Name, record.Name)
        .Set(x => x.SomeProperty, record.SomeProperty);
    var updateOperation = new UpdateOneModel<MongoDbSomeModel>(
        Builders<MongoDbSomeModel>.Filter.Eq(x => x.Id, record.Id),
        updateDefinition)
    {
        IsUpsert = true
    };

    bulkOperations.Add(updateOperation);
}

await collection.BulkWriteAsync(bulkOperations);

In the above code I basically say: If the document already exists, update the Name and SomeProperty, if it doesn't exists, create the document (upsert = true).

So, let's have a look in the database:
no-types

As you can see, the _t field is missing. I will show you why this is a problem (and also how I noticed that I had this problem).

Let's run the following code:

static async Task Main(string[] args)
{
    var settings = MongoClientSettings.FromConnectionString($"mongodb://username:MyPassword@localhost");
    settings.ConnectTimeout = TimeSpan.FromSeconds(5);
    var client = new MongoClient(settings);

    await client.DropDatabaseAsync("test");
    var database = client.GetDatabase("test");
    var collection = database.GetCollection<MongoDbSomeModel>("test");

    var bulkOperations = new List<WriteModel<MongoDbSomeModel>>();
    AddUpdateOneModels(bulkOperations);
    AddInsertOneModels(bulkOperations);

    var bulkResult = await collection.BulkWriteAsync(bulkOperations);
}

private static void AddUpdateOneModels(List<WriteModel<MongoDbSomeModel>> bulkOperations)
{
    var items = new List<MongoDbSomeModel>
    {
        new MongoDbSomeModel{Id = "1", Name = "Name 1 UPSERT", SomeProperty = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss")},
        new MongoDbSomeModel{Id = "2", Name = "Name 2 UPSERT", SomeProperty = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss")},
        new MongoDbSomeModel{Id = "3", Name = "Name 3 UPSERT", SomeProperty = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss")}
     };
    foreach (var record in items)
    {
        var updateDefinition = new UpdateDefinitionBuilder<MongoDbSomeModel>()
            .Set(x => x.Name, record.Name)
            .Set(x => x.SomeProperty, record.SomeProperty);
        var updateOperation = new UpdateOneModel<MongoDbSomeModel>(
            Builders<MongoDbSomeModel>.Filter.Eq(x => x.Id, record.Id),
            updateDefinition)
        {
            IsUpsert = true
        };

        bulkOperations.Add(updateOperation);
    }
}

private static void AddInsertOneModels(List<WriteModel<MongoDbSomeModel>> bulkOperations)
{
    var items = new List<MongoDbSomeModel>
    {
        new MongoDbSomeModel{Id = "4", Name = "Name 4 INSERT", SomeProperty = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss")},
        new MongoDbSomeModel{Id = "5", Name = "Name 5 INSERT", SomeProperty = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss")},
        new MongoDbSomeModel{Id = "6", Name = "Name 6 INSERT", SomeProperty = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss")}
    };

    foreach (var record in items)
    {
        var insertOperation = new InsertOneModel<MongoDbSomeModel>(record);
        bulkOperations.Add(insertOperation);
    }
}

In the above code I insert 3 items using UpdateOneModel upsert and 3 items using InsertOneModel.
It produces the following result:
both-runs
The first three items is missing the _t field.

Why is this a problem?

Well imagine that you have the following code:

var baseCollection = database.GetCollection<MongoDbBaseModel>("test");
var items = await baseCollection.Find(x => true).ToListAsync();

foreach (var item in items)
{
    switch (item)
    {
        case MongoDbSomeModel someModel:
            DoSomethingWithSomeModel(someModel)
            break;
        default:
            throw new Exception($"We don't support '{item.GetType().Name}'");
    }
}

I'm using pattern matching here to do something when item == MongoDbSomeModel. Note that I expect all the items to be of the MongoDbSomeModel type since that's the type I've inserted to the database.

But this code will throw an exception since the _t field is missing for the first 3 items.
exception

Solution (workarounds)

Now, I'm not sure if this behaviour is a bug or if it's working as intended, either way, I need to use UpdateOneModel and have the _t field written to the document.

ReplaceOneModel

If you don't write to the same document from multiple places/applications, you could use ReplaceOneModel instead of UpdateOneModel.

var items = new List<MongoDbSomeModel>
{
    new MongoDbSomeModel{Id = "1", Name = "Name 1 UPSERT", SomeProperty = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss")},
    new MongoDbSomeModel{Id = "2", Name = "Name 2 UPSERT", SomeProperty = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss")},
    new MongoDbSomeModel{Id = "3", Name = "Name 3 UPSERT", SomeProperty = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss")}
};

foreach (var record in items)
{
    var updateOperation = new ReplaceOneModel<MongoDbSomeModel>(
        Builders<MongoDbSomeModel>.Filter.Eq(x => x.Id, record.Id),
    record)
    {
        IsUpsert = true
    };

    bulkOperations.Add(updateOperation);
}

replaceonemodel
As you can see, when using ReplaceOneModel, the _t field is added to the document.

Append it yourself!

Since I couldn't use ReplaceOneModel, I went for this approach. I generate the array myself and append it in the UpdateDefinition.

var items = new List<MongoDbSomeModel>
{
    new MongoDbSomeModel{Id = "1", Name = "Name 1 UPSERT", SomeProperty = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss")},
    new MongoDbSomeModel{Id = "2", Name = "Name 2 UPSERT", SomeProperty = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss")},
    new MongoDbSomeModel{Id = "3", Name = "Name 3 UPSERT", SomeProperty = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss")}
};

foreach (var record in items)
{
    var updateDefinition = new UpdateDefinitionBuilder<MongoDbSomeModel>()
        .Set("_t", StandardDiscriminatorConvention.Hierarchical.GetDiscriminator(typeof(MongoDbBaseModel), typeof(MongoDbSomeModel)))
        .Set(x => x.Name, record.Name)
        .Set(x => x.SomeProperty, record.SomeProperty);
        var updateOperation = new UpdateOneModel<MongoDbSomeModel>(
            Builders<MongoDbSomeModel>.Filter.Eq(x => x.Id, record.Id),
            updateDefinition)
        {
            IsUpsert = true
        };

    bulkOperations.Add(updateOperation);
}

I use the StandardDiscriminatorConvention to generate the array, I need to send in the base type and the actual type.

After running the code again, we can see that the _t field is now present in all documents.
discriminator-present-in-all-documents

Now, as I said before, I'm not sure if this is a bug or if it's working as intended, but to me it feels like a bug, especially since ReplaceOneModel correctly appends the _t field.

A small repro can be found here.


Efficient file uploads with dotnet

$
0
0

Introduction

Efficient file uploads with dotnet

Recently I needed to solve the following problem at work

We need to upload a lot of large files to a third party storage provider, it needs to be as efficient as possible.

This problem is more or less the same as when it comes to dealing with large http responses. A rule of thumb when it comes to I/O in dotnet is to always think STREAM STREAM STREAM, let's see if it holds true here as well.

The third party provides an API endpoint that accepts binary data.

POST /files/{filename}

Implementations

Read file content into a string

Note: This is just here for...educational purposes, it's not a good idea to use ReadAllText since it only works correctly on text files. It will not "just work" on binary files.

Here we are using the ReadAllText method on the File object to read the full content of the file into a string. We are then using StringContent (which is basically just the same as ByteArrayContent) to build up the request body.

A StringContent is essentially a ByteArrayContent. We serialize the string into a byte-array in the constructor using encoding information provided by the caller...

So first we allocate a string which contains all the contents of the file and then we also allocate a byte array.

UploadFileLocalCommand_String.cs

public class UploadFileLocalCommand_String : IUploadFileCommand
{
    private readonly HttpClient _httpClient;

    public UploadFileLocalCommand_String(IHttpClientFactory httpClientFactory)
    {
        _httpClient = httpClientFactory.CreateClient(nameof(UploadFileLocalCommand_String));
        _httpClient.BaseAddress = new Uri("http://localhost:5000");
    }

    public async Task<HttpStatusCode> UploadFile(string filename)
    {
        // DON'T DO THIS
        var file = File.ReadAllText(Path.Combine(Config.ExampleFilesAbsolutePath, filename));
        var content = new StringContent(file, Encoding.UTF8);
        var request = new HttpRequestMessage(HttpMethod.Post, $"/files/{filename}.string")
        {
            Content = content
        };

        using (var response = await _httpClient.SendAsync(request))
        {
            return response.StatusCode;
        }
    }
}

I've seen this pattern a bunch of times:

  1. Get all the content (using File.ReadAllText, or response.Content.ReadAsStringAsync() when dealing with http).
  2. Store it in a string.
  3. Do something with the string (manipulation, deserialization...).

Don't do this. It allocates like crazy and performs really bad. There are much nicer APIs to use as you will see.

Read file content into a byte array

Now we are using File.ReadAllBytes instead of File.ReadAllText.
So instead of storing the file content in a string, we are now storing the bytes in a byte array.
We are then using ByteArrayContent instead of StringContent, meaning that we will "only" allocate the byte array instead of first allocating both the string and then the byte array as we did in the first example.

UploadFileLocalCommand_Bytes.cs

public class UploadFileLocalCommand_Bytes : IUploadFileCommand
{
    private readonly HttpClient _httpClient;

    public UploadFileLocalCommand_Bytes(IHttpClientFactory httpClientFactory)
    {
        _httpClient = httpClientFactory.CreateClient(nameof(UploadFileLocalCommand_Bytes));
        _httpClient.BaseAddress = new Uri("http://localhost:5000");
    }

    public async Task<HttpStatusCode> UploadFile(string filename)
    {
        // DON'T DO THIS
        var file = File.ReadAllBytes(Path.Combine(Config.ExampleFilesAbsolutePath, filename));
        var content = new ByteArrayContent(file);
        var request = new HttpRequestMessage(HttpMethod.Post, $"/files/{filename}.bytes")
        {
            Content = content
        };

        using (var response = await _httpClient.SendAsync(request))
        {
            return response.StatusCode;
        }
    }
}

This is an improvement from our first example but it still has one huge drawback, it allocates the whole file and stores it in a byte array. We can do better.

Read file content with a stream

We are now using File.OpenRead instead of File.ReadAllBytes. This gives us a FileStream that we can use to stream the content of the file together with StreamContent.

UploadFileLocalCommand_Stream.cs

public class UploadFileLocalCommand_Stream : IUploadFileCommand
{
    private readonly HttpClient _httpClient;

    public UploadFileLocalCommand_Stream(IHttpClientFactory httpClientFactory)
    {
        _httpClient = httpClientFactory.CreateClient(nameof(UploadFileLocalCommand_Bytes));
        _httpClient.BaseAddress = new Uri("http://localhost:5000");
    }

    public async Task<HttpStatusCode> UploadFile(string filename)
    {
        using (var file = File.OpenRead(Path.Combine(Config.ExampleFilesAbsolutePath, filename)))
        {
            var content = new StreamContent(file);
            var request = new HttpRequestMessage(HttpMethod.Post, $"/files/{filename}.stream")
            {
                Content = content
            };
            using (var response = await _httpClient.SendAsync(request))
            {
                return response.StatusCode;
            }
        }
    }
}

Using a stream allows us to operate on small portions of the file in chunks instead of allocating the whole file.

When using a stream, the flow works (kind of) like this:

  1. Get a small chunk from the file (buffer).
  2. Send this chunk to the receiver (in our case our FileUpload API).
  3. Repeat 1-2 until the whole file has been sent.

Benchmarks

|            Method |    filename |         Mean |     StdDev |       Gen 0 |       Gen 1 |     Gen 2 |     Allocated |
|------------------ |------------ |-------------:|-----------:|------------:|------------:|----------:|--------------:|
| UploadFile_String |    1MB.test |     3.877 ms |  0.0527 ms |    500.0000 |    500.0000 |  500.0000 |    5156.46 KB |
|  UploadFile_Bytes |    1MB.test |     2.402 ms |  0.1362 ms |           - |           - |         - |    1028.28 KB |
| UploadFile_Stream |    1MB.test |     2.317 ms |  0.1189 ms |           - |           - |         - |       5.95 KB |
| UploadFile_String |   10MB.test |    50.732 ms |  7.7102 ms |   3500.0000 |   2000.0000 | 1000.0000 |   51313.21 KB |
|  UploadFile_Bytes |   10MB.test |    43.655 ms |  3.3186 ms |           - |           - |         - |   10244.29 KB |
| UploadFile_Stream |   10MB.test |    31.291 ms |  2.5565 ms |           - |           - |         - |      14.11 KB |
| UploadFile_String |  100MB.test |   573.321 ms | 46.4071 ms |  27500.0000 |  15000.0000 | 2500.0000 |  512969.54 KB |
|  UploadFile_Bytes |  100MB.test |   256.709 ms | 28.3533 ms |           - |           - |         - |  102405.95 KB |
| UploadFile_Stream |  100MB.test |   257.108 ms | 70.1054 ms |           - |           - |         - |     171.29 KB |
| UploadFile_String | 1000MB.test | 5,184.615 ms | 74.6092 ms | 256000.0000 | 172000.0000 | 5000.0000 | 5129272.25 KB |
|  UploadFile_Bytes | 1000MB.test | 2,490.773 ms | 60.2330 ms |           - |           - |         - | 1024006.73 KB |
| UploadFile_Stream | 1000MB.test | 1,910.646 ms | 60.3543 ms |           - |           - |         - |        998 KB |

Polymorphic deserialization with System.Text.Json

$
0
0
Polymorphic deserialization with System.Text.Json

Photo by Florian Krumm

The problem

We need to deserialize the following JSON:

[
  {
    "type": "car",
    "name": "Josefs Car",
    "wheels": 4,
    "model": {
      "brand": "Ferrri",
      "name": "458 Spider",
      "color": "red",
      "horsepower": 562
    },
    "properties": {
      "passengers": 2,
      "rims": {
        "name": "Nutek"
      }
    }
  },
  {
    "type": "truck",
    "name": "Josefs Truck",
    "wheels": 8,
    "model": {
      "brand": "Volvo",
      "name": "FH",
      "color": "black",
      "horsepower": 540
    },
    "trailer": {
      "name": "my trailer name",
      "wheels": 8
    },
    "properties": {
      "passengers": 2,
      "towingCapacity": {
        "maxKg": 18000,
        "maxPounds": 39683
      }
    }
  }
]

As you can see, the JSON differs a bit depending on what type of vehicle it is.

  • The properties of a car contains a rims property.
  • The properties of a truck contains a towingCapacity property.
  • A truck also has a trailer property.

Approaches

We will use the following custom JsonSerializerOptions for all the different approaches.

public static class DefaultJsonSerializerOptions
{
    static DefaultJsonSerializerOptions()
    {
        Instance = new JsonSerializerOptions
        {
           // Don't care about the casing
           PropertyNameCaseInsensitive = true
        };
    }

    public static JsonSerializerOptions Instance { get; }
}

All of our classes will be immutable as well.

Non polymorphic

We start by creating the classes required for our deserialization.

public class Vehicle
{
    public Vehicle(string type, string name, int wheels, Trailer trailer, VehicleModel model, VehicleProperties properties)
    {
        Type = type;
        Name = name;
        Wheels = wheels;
        Trailer = trailer;
        Model = model;
        Properties = properties;
    }

    public string Type { get; }
    public string Name { get; }
    public int Wheels { get; }
    public VehicleModel Model { get; }
    public VehicleProperties Properties { get; }
    public Trailer Trailer { get; }
}
public class VehicleModel
{
    public VehicleModel(string brand, string name, string color, int horsepower)
    {
        Brand = brand;
        Name = name;
        Color = color;
        Horsepower = horsepower;
    }

    public string Brand { get; }
    public string Name { get; }
    public string Color { get; }
    public int Horsepower { get; }
}
public class VehicleProperties
{
    public VehicleProperties(int wheels, int passengers, Rims rims, TruckTowingCapacity towingCapacity)
    {
        Wheels = wheels;
        Passengers = passengers;
        Rims = rims;
        TowingCapacity = towingCapacity;
    }

    public int Wheels { get; }
    public int Passengers { get; }
    public TruckTowingCapacity TowingCapacity { get; }
    public Rims Rims { get; }
}
public class TruckTowingCapacity
{
    public TruckTowingCapacity(int maxKg, int maxPounds)
    {
        MaxKg = maxKg;
        MaxPounds = maxPounds;
    }

    public int MaxKg { get; }
    public int MaxPounds { get; }
}
public class Rims
{
    public Rims(string name)
    {
       Name = name;
    }

    public string Name { get; }
}

Test
We then write the following test:

public class NonPolymorphicTests
{
    [Fact]
    public async Task ShouldDeserializeVehiclesCorrectly()
    {
        using (var jsonFile = File.OpenRead("example.json"))
        {
            var result = await JsonSerializer.DeserializeAsync<List<NonPolymorphic.Vehicle>>(jsonFile, DefaultJsonSerializerOptions.Instance);
                
            result.Count.ShouldBe(2);
            var car = result.Single(x => x.Type == "car");
            var truck = result.Single(x => x.Type == "truck");
            car.Type.ShouldBe("car");
            car.Name.ShouldBe("Josefs Car");
            car.Model.Brand.ShouldBe("Ferrari");
            car.Model.Name.ShouldBe("458 Spider");
            car.Model.Color.ShouldBe("red");
            car.Model.Horsepower.ShouldBe(562);
            car.Properties.Wheels.ShouldBe(4);
            car.Properties.Passengers.ShouldBe(2);
            car.Properties.Rims.Name.ShouldBe("Nutek");
            truck.Type.ShouldBe("truck");
            truck.Name.ShouldBe("Josefs Truck");
            truck.Model.Name.ShouldBe("FH");
            truck.Model.Brand.ShouldBe("Volvo");
            truck.Model.Color.ShouldBe("black");
            truck.Model.Horsepower.ShouldBe(540);
            truck.Properties.Wheels.ShouldBe(8);
            truck.Properties.Passengers.ShouldBe(2);
            truck.Properties.TowingCapacity.MaxKg.ShouldBe(18000);
            truck.Properties.TowingCapacity.MaxPounds.ShouldBe(39683);
            truck.Trailer.Name.ShouldBe("my trailer");
            truck.Trailer.Wheels.ShouldBe(8);
        }
    }
}

If we now run this test, it passes, great.
However...there's a few problems with this approach:

  • All objects in our list will be a Vehicle.
  • A "car" will have a trailer property.
  • A "car" will have a TowingCapacity property in it's properties.
  • A "truck" will have a Rims property.
  • We can't use pattern matching since everything is of the same type (Vehicle).
  • We can't do our null checking in the constructor because some properties NEEDS to be null now (towingCapacity on a "car" for example).

Depending on your use case this might be good enough for you, but for me, this doesn't cut it.

Polymorphic

Let's start with creating some new classes:

public abstract class Vehicle<T> : Vehicle where T : VehicleProperties
{
    protected Vehicle(string type, string name, VehicleModel model, T properties) : base(type, name, model)
    {
        Properties = properties ?? throw new ArgumentNullException(nameof(properties));
    }
        
    public T Properties { get; }
}

public abstract class Vehicle
{
    protected Vehicle(string type, string name, VehicleModel model)
    {
        Type = type ?? throw new ArgumentNullException(nameof(type));
        Name = name ?? throw new ArgumentNullException(nameof(name));
        Model = model ?? throw new ArgumentNullException(nameof(model));
    }
        
    public string Type { get; }
    public string Name { get; }
    public VehicleModel Model { get; }
}
public class Car : Vehicle<CarProperties>
{
    public Car(string type, string name, VehicleModel model, CarProperties properties) : base(type, name, model, properties)
    {
    }
}
public class Truck : Vehicle<TruckProperties>
{
    public Truck(
       string type,
        string name,
        VehicleModel model,
        TruckProperties properties,
        TruckTrailer trailer) : base(type,
        name,
        model,
        properties)
    {
        Trailer = trailer;
    }
        
    public TruckTrailer Trailer { get; }
}
public class VehicleModel
{
    public VehicleModel(string brand, string name, string color, int horsepower)
    {
        Brand = brand ?? throw new ArgumentNullException(nameof(brand));
        Name = name ?? throw new ArgumentNullException(nameof(name));
        Color = color ?? throw new ArgumentNullException(nameof(color));
        Horsepower = horsepower;
    }

    public string Brand { get; }
    public string Name { get; }
    public string Color { get; }
    public int Horsepower { get; }
}
public abstract class VehicleProperties
{
    protected VehicleProperties(int wheels, int passengers)
    {
        Wheels = wheels;
        Passengers = passengers;
    }

    public int Wheels { get; }
    public int Passengers { get; }
}
public class CarProperties : VehicleProperties
{
    public CarProperties(int wheels, int passengers, CarRims rims) : base(wheels, passengers)
    {
        Rims = rims ?? throw new ArgumentNullException(nameof(rims));
    }

    public CarRims Rims { get; }
}
public class CarRims
{
    public CarRims(string name)
    {
        Name = name ?? throw new ArgumentNullException(nameof(name));
    }

    public string Name { get; }
}
public class TruckProperties : VehicleProperties
{
    public TruckProperties(int wheels, int passengers, TruckTowingCapacity towingCapacity) : base(wheels, passengers)
    {
        TowingCapacity = towingCapacity ?? throw new ArgumentNullException(nameof(towingCapacity));
    }
    public TruckTowingCapacity TowingCapacity { get; }
}
public class TruckTowingCapacity
{
    public TruckTowingCapacity(int maxKg, int maxPounds)
    {
        MaxKg = maxKg;
        MaxPounds = maxPounds;
    }

    public int MaxKg { get; }
    public int MaxPounds { get; }
}
public class TruckTrailer
{
    public TruckTrailer(string name, int wheels)
    {
        Name = name ?? throw new ArgumentNullException(nameof(name));
        Wheels = wheels;
    }

    public string Name { get; }
    public int Wheels { get; }
}

We've created an abstract class, Vehicle, together with two implementations, Car and Truck. Now the car specific properties are only accessible on a Car and vice versa, great.

Test

public class PolymorphicTests
{
    [Fact]
    public async Task ShouldDeserializeVehiclesCorrectly()
    {
        using (var jsonFile = File.OpenRead("example.json"))
        {
            var result = await JsonSerializer.DeserializeAsync<List<Polymorphic.Vehicle>>(jsonFile, DefaultJsonSerializerOptions.Instance);
                
            result.Count.ShouldBe(2);
            result.ShouldContain(x => x.GetType() == typeof(Car));
            result.ShouldContain(x => x.GetType() == typeof(Truck));
            var car = result.Single(x => x.Type == "car") as Car;
            var truck = result.Single(x => x.Type == "truck") as Truck;
            car.Type.ShouldBe("car");
            car.Name.ShouldBe("Josefs Car");
            car.Model.Brand.ShouldBe("Ferrari");
            car.Model.Name.ShouldBe("458 Spider");
            car.Model.Color.ShouldBe("red");
            car.Model.Horsepower.ShouldBe(562);
            car.Properties.Wheels.ShouldBe(4);
            car.Properties.Passengers.ShouldBe(2);
            car.Properties.Rims.Name.ShouldBe("Nutek");
            truck.Type.ShouldBe("truck");
            truck.Name.ShouldBe("Josefs Truck");
            truck.Model.Name.ShouldBe("FH");
            truck.Model.Brand.ShouldBe("Volvo");
            truck.Model.Color.ShouldBe("black");
            truck.Model.Horsepower.ShouldBe(540);
            truck.Properties.Wheels.ShouldBe(8);
            truck.Properties.Passengers.ShouldBe(2);
            truck.Properties.TowingCapacity.MaxKg.ShouldBe(18000);
            truck.Properties.TowingCapacity.MaxPounds.ShouldBe(39683);
            truck.Trailer.Name.ShouldBe("my trailer");
            truck.Trailer.Wheels.ShouldBe(8);
        }
    }
}

If we run this test now...it will FAIL.

System.NotSupportedException
Deserialization of types without a parameterless constructor, a singular parameterized constructor, or a parameterized constructor annotated with 'JsonConstructorAttribute' is not supported. Type 'JOS.SystemTextJsonPolymorphism.Polymorphic.Vehicle'. Path: $[0] | LineNumber: 1 | BytePositionInLine: 3.
   at System.Text.Json.ThrowHelper.ThrowNotSupportedException(ReadStack& state, Utf8JsonReader& reader, NotSupportedException ex)
   at System.Text.Json.ThrowHelper.ThrowNotSupportedException_DeserializeNoConstructor(Type type, Utf8JsonReader& reader, ReadStack& state)
   at System.Text.Json.Serialization.Converters.ObjectDefaultConverter`1.OnTryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value)
   at System.Text.Json.Serialization.JsonConverter`1.TryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value)
   at System.Text.Json.Serialization.Converters.IEnumerableDefaultConverter`2.OnTryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, TCollection& value)
   at System.Text.Json.Serialization.JsonConverter`1.TryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value)
   at System.Text.Json.Serialization.JsonConverter`1.ReadCore(Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
   at System.Text.Json.JsonSerializer.ReadCore[TValue](JsonConverter jsonConverter, Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
   at System.Text.Json.JsonSerializer.ReadCore[TValue](JsonReaderState& readerState, Boolean isFinalBlock, ReadOnlySpan`1 buffer, JsonSerializerOptions options, ReadStack& state, JsonConverter converterBase)
   at System.Text.Json.JsonSerializer.ReadAsync[TValue](Stream utf8Json, Type returnType, JsonSerializerOptions options, CancellationToken cancellationToken)
   at JOS.SystemTextJsonPolymorphism.Tests.PolymorphicTests.ShouldDeserializeVehiclesCorrectly()

We are getting the error because we are trying to deserialize to an abstract immutable class, we need to help the serializer a bit here by creating a custom JsonConverter.

Custom JsonConverter.

I've written about System.Text.Json and custom JsonConverters before. It's a really flexible way of handling some custom logic when serializing/deserializing.

public class VehicleJsonConverter : JsonConverter<Vehicle>
{
    public override bool CanConvert(Type type)
    {
        return type.IsAssignableFrom(typeof(Vehicle));
    }
    
    public override Vehicle Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (JsonDocument.TryParseValue(ref reader, out var doc))
        {
            if (doc.RootElement.TryGetProperty("type", out var type))
            {
                var typeValue = type.GetString();
                var rootElement = doc.RootElement.GetRawText();

                return typeValue switch
                {
                    "car" => JsonSerializer.Deserialize<Car.Car>(rootElement, options),
                    "truck" => JsonSerializer.Deserialize<Truck.Truck>(rootElement, options),
                    _ => throw new JsonException($"{typeValue} has not been mapped to a custom type yet!")
                };
            }

            throw new JsonException("Failed to extract type property, it might be missing?");
        }

        throw new JsonException("Failed to parse JsonDocument");
    }

    public override void Write(Utf8JsonWriter writer, Vehicle value, JsonSerializerOptions options)
    {
        // We don't care about writing JSON.
        throw new NotImplementedException();
    }
}

Let's break it down:

  1. We say that this converter handles everything that's assignable from a Vehicle
  2. We parse the JSON to a JsonDocument
  3. If successful, we look for a type property.
  4. If a type property exists, we switch on it and deserialize to the correct type (Car or Truck in our case).

The only thing left to do now is to tell the JsonSerializer to use our custom converter. We can do this in a couple of different ways, I'm choosing to use the attribute.

[JsonConverter(typeof(VehicleJsonConverter))]
public abstract class Vehicle
{
    protected Vehicle(string type, string name, VehicleModel model)
    {
        Type = type ?? throw new ArgumentNullException(nameof(type));
        Name = name ?? throw new ArgumentNullException(nameof(name));
        Model = model ?? throw new ArgumentNullException(nameof(model));
    }
        
    public string Type { get; }
    public string Name { get; }
    public VehicleModel Model { get; }
}

Now if we run the test again, it's green!

Bonus - C# 9 - Covariant return types

One problem with how we structured our Vehicle class above is that the non-generic Vehicle class doesn't have a Properties property. So this would not work for example:

var result = await JsonSerializer.DeserializeAsync<List<Polymorphic.Vehicle>>(jsonFile, DefaultJsonSerializerOptions.Instance);

result.Select(x => x.Properties);

That code will not compile because we are deserializing to the non-generic Vehicle class and it does not contain any property named Properties.

This can be fixed in a number of ways but since I like to use the latest and greatest™, I will solve it by using covariant return types that was introduced in C# 9.

We need to do some modifications to our Vechicle, Car and Truck classes.

[JsonConverter(typeof(VehicleJsonConverter))]
public abstract class Vehicle
{
    protected Vehicle(string type, string name, VehicleModel model)
    {
        Type = type ?? throw new ArgumentNullException(nameof(type));
        Name = name ?? throw new ArgumentNullException(nameof(name));
        Model = model ?? throw new ArgumentNullException(nameof(model));
    }
        
    public string Type { get; }
    public string Name { get; }
    public VehicleModel Model { get; }
    public abstract VehicleProperties Properties { get; }
}

We've now removed the generic Vehicle class altogether and instead added the Properties property to the non-generic Vechicle class and marked it abstract.

public class Car : Vehicle
{
    public Car(string type, string name, VehicleModel model, CarProperties properties) : base(type, name, model)
    {
        Properties = properties ?? throw new ArgumentNullException(nameof(properties));
    }

    public override CarProperties Properties { get; }
}
public class Truck : Vehicle
{
    public Truck(
        string type,
        string name,
        VehicleModel model,
        TruckProperties properties,
        TruckTrailer trailer) : base(type,
        name,
        model)
    {
        Trailer = trailer;
        Properties = properties;
    }
        
    public TruckTrailer Trailer { get; }
    public override TruckProperties Properties { get; }
}

We can now change the type of the Properties property by overriding it, HOW COOL?

If we now try do access the Properties property like this...

var result = await JsonSerializer.DeserializeAsync<List<Polymorphic.Vehicle>>(jsonFile, DefaultJsonSerializerOptions.Instance);

result.Select(x => x.Properties);

...it will compile and just work :).

All code in this post can be found over at Github.

Azure Storage - Zip multiple files using Azure Functions

$
0
0
Azure Storage - Zip multiple files using Azure Functions

Photo by Tomas Sobek

Introduction

Recently I was assigned the following task:

Our customers need to be able to create a zip file containing multiple files from Azure Storage and then download it. It needs to be FAST.

The only thing that matters to us is speed, we will not care about the file size of the generated zip file. So the plan is just to take all the files and put them into the zip archive without compressing them. This task will be very I/O dependent (CPU, network and storage).

Here's my plan:

  1. POST a list of file paths to a Azure Function (Http trigger)
  2. Create a queue message containing the file paths and put on a storage queue.
  3. Listen to said storage queue with another Azure function (Queue trigger).
  4. Stream each file from Azure Storage -> Add it to a Zip stream -> Stream it back to Azure storage.

We need to support very large files (100 GB+) so it's important that we don't max out the memory. The benefits of using streams here is that the memory consumption will be very low and stable.

Prerequisites

  • Azure storage account (Standard V2)
  • Azure storage queue ("create-zip-file")
  • App Service Plan (we will use a dedicated one).
  • SharpZipLib - library for working with zip files.

Code

Http portion

Let's start with the first function, the one responsible for validating and publishing messages to the storage queue.

Example body that we will POST to our function, all files exists in my Azure Storage account.

{
    "containerName": "fileupload",
    "filePaths": [
        "8k-sample-movie.mp4",
        "dog.jpg",
        "dog2.jpg",
        "ubuntu-18.04.5-desktop-amd64.iso",
        "ubuntu-20.10-desktop-amd64.iso"]
}

Note that I've dumbed this down a little just to make it easier to follow. In production we will send in SAS URIs instead of file paths and container name.

public class CreateZipHttpFunction
{
    [FunctionName("CreateZipHttpFunction")]
    public ActionResult Run(
        [HttpTrigger(AuthorizationLevel.Function, "POST", Route = "zip")] CreateZipFileInput input,
        [Queue(Queues.CreateZipFile, Connection = "FilesStorageConnectionString")] ICollector<CreateZipFileMessage> queueCollector,
        HttpRequest request)
    {
        var validationResults = new List<ValidationResult>();
        if (!Validator.TryValidateObject(input, new ValidationContext(input), validationResults))
        {
            return new BadRequestObjectResult(new
            {
                errors = validationResults.Select(x => x.ErrorMessage)
            });
        }
            
        queueCollector.Add(new CreateZipFileMessage
        {
            ContainerName = input.ContainerName,
            FilePaths = input.FilePaths
        });
            
        return new AcceptedResult();
    }
}
  1. Validate the input and return Bad Request if it's invalid
  2. Create a new CreateZipFileMessage and add it to the queue

Queue portion

public class CreateZipFileWorkerQueueFunction
{
    private readonly ICreateZipFileCommand _createZipFileCommand;
    private readonly ILogger<CreateZipFileWorkerQueueFunction> _logger;

    public CreateZipFileWorkerQueueFunction(
        ICreateZipFileCommand createZipFileCommand,
        ILogger<CreateZipFileWorkerQueueFunction> logger)
    {
        _createZipFileCommand = createZipFileCommand ?? throw new ArgumentNullException(nameof(createZipFileCommand));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }
        
    [FunctionName("CreateZipFileWorkerQueueFunction")]
    public async Task Run([QueueTrigger(
        queueName: Queues.CreateZipFile,
        Connection = "FilesStorageConnectionString")]
        CreateZipFileMessage message,
        string id,
        CancellationToken cancellationToken)
    {
        _logger.LogInformation("Starting to process message {MessageId}", id);
        await _createZipFileCommand.Execute(message.ContainerName, message.FilePaths, cancellationToken);
        _logger.LogInformation("Processing of message {MessageId} done", id);
    }
}

I don't do anything fancy here, I just execute the ICreateZipFileCommand. The reason for doing this is that I wan't to keep my functions free from business logic.

Zip portion

Somewhat inspired by Tip 141.

public class AzureBlobStorageCreateZipFileCommand : ICreateZipFileCommand
{
    private readonly UploadProgressHandler _uploadProgressHandler;
    private readonly ILogger<AzureBlobStorageCreateZipFileCommand> _logger;
    private readonly string _storageConnectionString;
    private readonly string _zipStorageConnectionString;
        
    public AzureBlobStorageCreateZipFileCommand(
        IConfiguration configuration,
        UploadProgressHandler uploadProgressHandler,
        ILogger<AzureBlobStorageCreateZipFileCommand> logger)
    {
        _uploadProgressHandler = uploadProgressHandler ?? throw new ArgumentNullException(nameof(uploadProgressHandler));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        _storageConnectionString = configuration.GetValue<string>("FilesStorageConnectionString") ?? throw new Exception("FilesStorageConnectionString was null");
        _zipStorageConnectionString = configuration.GetValue<string>("ZipStorageConnectionString") ?? throw new Exception("ZipStorageConnectionString was null");
    }
        
    public async Task Execute(
        string containerName,
        IReadOnlyCollection<string> filePaths,
        CancellationToken cancellationToken)
    {
        var zipFileName = $"{DateTime.UtcNow:yyyyMMddHHmmss}.{Guid.NewGuid().ToString().Substring(0, 4)}.zip";
        var stopwatch = Stopwatch.StartNew();

        try
        {
            using (var zipFileStream = await OpenZipFileStream(zipFileName, cancellationToken))
            {
                using (var zipFileOutputStream = CreateZipOutputStream(zipFileStream))
                {
                    var level = 0;
                    _logger.LogInformation("Using Level {Level} compression", level);
                    zipFileOutputStream.SetLevel(level);
                    foreach (var filePath in filePaths)
                    {
                        var blockBlobClient = new BlockBlobClient(_storageConnectionString, containerName, filePath);
                        var properties = await blockBlobClient.GetPropertiesAsync(cancellationToken: cancellationToken);
                        var zipEntry = new ZipEntry(blockBlobClient.Name)
                        {
                            Size = properties.Value.ContentLength
                        };
                        zipFileOutputStream.PutNextEntry(zipEntry);
                        await blockBlobClient.DownloadToAsync(zipFileOutputStream, cancellationToken);
                        zipFileOutputStream.CloseEntry();
                    }
                }
            }

            stopwatch.Stop();
            _logger.LogInformation("[{ZipFileName}] DONE, took {ElapsedTime}",
                zipFileName,
                stopwatch.Elapsed);
        }
        catch (TaskCanceledException)
        {
            var blockBlobClient = new BlockBlobClient(_zipStorageConnectionString, "zips", zipFileName);
            await blockBlobClient.DeleteIfExistsAsync();
            throw;
        }
    }

    private async Task<Stream> OpenZipFileStream(
        string zipFilename,
        CancellationToken cancellationToken)
    {
        var zipBlobClient = new BlockBlobClient(_zipStorageConnectionString, "zips", zipFilename);
            
        return await zipBlobClient.OpenWriteAsync(true, options: new BlockBlobOpenWriteOptions
        {
            ProgressHandler = _uploadProgressHandler,
            HttpHeaders = new BlobHttpHeaders
            {
                ContentType = "application/zip"
            }
        }, cancellationToken: cancellationToken);
    }

    private static ZipOutputStream CreateZipOutputStream(Stream zipFileStream)
    {
        return new ZipOutputStream(zipFileStream)
        {
            IsStreamOwner = false
        };
    }
}
  1. Create a filename for our new Zip file
  2. Create and open a writeable stream to Azure storage (for our new Zip file)
  3. Create a ZipOutputStream and pass the zip file stream to it
  4. Set the level to 0 (no compression, level 6 is the default level).
  5. Foreach file path, fetch some metadata for the file, create a ZipEntry and "download" the blob stream to the ZipOutputStream.
  6. Done!

I've also created a custom IProgress implementation that logs the progess of the zip creation every 30 seconds.

public class UploadProgressHandler : IProgress<long>
{
    private bool _initialized;
    private readonly Stopwatch _stopwatch;
    private readonly ILogger<UploadProgressHandler> _logger;
    private DateTime _lastProgressLogged;

    public UploadProgressHandler(ILogger<UploadProgressHandler> logger)
    {
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        _lastProgressLogged = DateTime.UtcNow;
        _stopwatch = new Stopwatch();
    }
        
    public void Report(long value)
    {
        if (!_initialized)
        {
            _initialized = true;
            _lastProgressLogged = DateTime.UtcNow;
            _stopwatch.Start();
        }
            
        if ((DateTime.UtcNow - _lastProgressLogged).TotalSeconds >= 30)
        {
            _lastProgressLogged = DateTime.UtcNow;
            var megabytes = (value > 0 ? value : 1) / 1048576;
            var seconds = _stopwatch.ElapsedMilliseconds / 1000d;
            var averageUploadMbps = Math.Round((value / seconds) / 125000d, 2);
            _logger.LogInformation("Progress: {UploadedMB}MB Average Upload: {AverageUpload}Mbps", megabytes, averageUploadMbps);
        }
    }
}

Performance

I've done some quick benchmarking with the following App Service plans:

  • S1
  • S2
  • S3
  • P3V2

There are four tests per App Service plan:

  • 1 request -> Writing the Zip to Standard Blob storage
  • 5 concurrent requests -> Writing the Zip to Standard Blob storage
  • 1 request -> Writing the Zip to Premium Blob storage
  • 5 concurrent requests -> Writing the Zip to Premium Blob storage

I've uploaded the following files to my storage account:
Azure Storage - Zip multiple files using Azure Functions
We will use all files in our tests, meaning that the total size of the files we are going to zip will be ~ 8.3 GB.

No surprises here really, the more CPU we have, the less is the difference between 1 and 5 concurrent requests. One thing to note here though is the difference between Standard and Premium storage, Premium storage significally lowers the processing time. I assume this has to do with HDD vs SSD storage.

Same story here, more CPU == faster throughput (ofc :) ).
Noteworthy: the P3V2 App Service Plan together with Premium storage achieves ~ 1.5 Gbit/s throughput when sending 5 concurrent requests.

This numbers has been taken from the Azure Portal so they are kinda rough. We can see that both S1 and S2 are quite close to maxing out the CPU when processing 5 concurrent requests.

Here we can see that the memory usage is stable throughout, it doesn't matter if we process 1 or 5 requests, the consumption is roughly the same.

All code found in this post is available at GitHub.

My take on the "Result class"

$
0
0
My take on the

Photo by Agence Olloweb

Introduction

Don't use exceptions for flow control!

This is a quote I stumbled on quite a bit during my first years as a programmer. Let's imagine the following scenario:

  • We have one endpoint that takes a name and returns a hamburger with that name.
  • If we can't find a hamburger with that name, we need to return 404 Not found
  • If something else goes wrong, we need to return 500 Internal Server Error.

Let's implement our first solution:

public interface IGetHamburgerQuery
{
    Hamburger Execute(string name);
}
public class InMemoryGetHamburgerQuery : IGetHamburgerQuery
{
    private static readonly IReadOnlyCollection<Hamburger> Hamburgers = new List<Hamburger>
    {
        new Hamburger("Double Cheese"),
        new Hamburger("Big Mac"),
        new Hamburger("Hamburger")
    };

    public Hamburger Execute(string name)
    {
        var hamburger = Hamburgers.FirstOrDefault(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase));

        if (hamburger == null)
        {
            throw new NotFoundException();
        }

        return hamburger;
    }
}
[HttpGet("{name}")]
public ActionResult<Hamburger> Get(string name)
{
    try
    {
        var hamburger = _getHamburgerQuery.Execute(name);
        return new OkObjectResult(hamburger);
    }
    catch (NotFoundException)
    {
        return new NotFoundResult();
    }
    catch
    {
        return new StatusCodeResult(500);
    }
}

Nothing fancy going on here, we look in our "database" for the hamburger, if the "database" returns null, we throw a NotFoundException. In our action method we then use a try catch where we catch NotFoundException and return the correct status code.

What's "wrong" with this code then? Well, if we should "obey" the quote above, we should not rely on exceptions for controlling the flow of our program. One could also argue that not finding any hamburger in our database is in fact nothing...exceptional at all. I would rather say that it's EXPECTED. I tend to only use exceptions for errors which I can't recover from (database connection errors...).

Introducing Result.cs

I've used a modified version of Vladimir Khorikov's Result class throughout the years and it has worked great. Let's try it out. Note that I prefix Vladimirs implementation to avoid confusion with my own implementation that I will show further down.

public interface IGetHamburgerResultQuery
{
    Vladimir.Result<Hamburger> Execute(string name);
}
public class InMemoryGetHamburgerResultQuery : IGetHamburgerResultQuery
{
    private static readonly IReadOnlyCollection<Hamburger> Hamburgers = new List<Hamburger>
    {
        new Hamburger("Double Cheese"),
        new Hamburger("Big Mac"),
        new Hamburger("Hamburger")
    };

    public Vladimir.Result<Hamburger> Execute(string name)
    {
        var hamburger = Hamburgers.FirstOrDefault(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase));

        if (hamburger == null)
        {
            return Vladimir.Result.Fail<Hamburger>($"Could not find any hamburger named '{name}'");
        }

        return Vladimir.Result.Ok<Hamburger>(hamburger);
    }
}
[HttpGet("result/{name}")]
public ActionResult<Hamburger> GetResult(string name)
{
    var hamburgerResult = _getHamburgerResultQuery.Execute(name);
    if (hamburgerResult.Success)
    {
        return new OkObjectResult(hamburgerResult.Value);
    }

    if (hamburgerResult.Error.Equals($"Could not find any hamburger named '{name}'"))
    {
        return new NotFoundResult();
    }

    return new StatusCodeResult(500);
}

Now we are not using exceptions anymore. If we can't find any hamburger we are returning Result.Fail.
We are then inspecting the Success property in our controller to see if everything went OK. If it didn't, we inspect the Error property to see if we should return a 404 or not.
This is one of the things that bothered me quite a bit, with the current implementation of the Result class, there is no easy/good way of telling WHAT went wrong. You just know that SOMETHING went wrong and you can only act on the error message. That's what I'm trying to fix with my own implementation.

My take on the Result class

I will just paste in the Result class here. Basically it boils down to the following:

  • Two classes, SuccessResult and ErrorResult.
  • When something goes OK -> return a SuccessResult
  • When something goes wrong -> return a ErrorResult OR something that inherits from ErrorResult to provide some more context.
public abstract class Result
{
    public bool Success { get; protected set; }
    public bool Failure => !Success;
}

public abstract class Result<T> : Result
{
    private T _data;

    protected Result(T data)
    {
        Data = data;
    }

    public T Data
    {
        get => Success ? _data : throw new Exception($"You can't access .{nameof(Data)} when .{nameof(Success)} is false");
        set => _data = value;
    }
}

public class SuccessResult : Result
{
    public SuccessResult()
    {
        Success = true;
    }
}

public class SuccessResult<T> : Result<T>
{
    public SuccessResult(T data) : base(data)
    {
        Success = true;
    }
}

public class ErrorResult : Result, IErrorResult
{
    public ErrorResult(string message) : this(message, Array.Empty<Error>())
    {
    }

    public ErrorResult(string message, IReadOnlyCollection<Error> errors)
    {
        Message = message;
        Success = false;
        Errors = errors ?? Array.Empty<Error>();
    }

    public string Message { get; }
    public IReadOnlyCollection<Error> Errors { get; }
}

public class ErrorResult<T> : Result<T>, IErrorResult
{
    public ErrorResult(string message) : this(message, Array.Empty<Error>())
    {    
    }

    public ErrorResult(string message, IReadOnlyCollection<Error> errors) : base(default)
    {
        Message = message;
        Success = false;
        Errors = errors ?? Array.Empty<Error>();
    }

    public string Message { get; set; }
    public IReadOnlyCollection<Error> Errors { get; }
}

public class Error
{
    public Error(string details) : this(null, details)
    {

    }

    public Error(string code, string details)
    {
        Code = code;
        Details = details;
    }

    public string Code { get; }
    public string Details { get; }
}

internal interface IErrorResult
{
    string Message { get; }
    IReadOnlyCollection<Error> Errors { get; }
}

Our hamburger example will now look like this:

public interface IGetHamburgerJosResultQuery
{
    Result<Hamburger> Execute(string name);
}
public class InMemoryGetHamburgerJosResultQuery : IGetHamburgerJosResultQuery
{
    private static readonly IReadOnlyCollection<Hamburger> Hamburgers = new List<Hamburger>
    {
        new Hamburger("Double Cheese"),
        new Hamburger("Big Mac"),
        new Hamburger("Hamburger")
    };

    public Result<Hamburger> Execute(string name)
    {
        var hamburger = Hamburgers.FirstOrDefault(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase));

        if (hamburger == null)
        {
            return new NotFoundResult<Hamburger>("Could not find any hamburgers");
        }

        return new SuccessResult<Hamburger>(hamburger);
    }
}
[HttpGet("jos-result/{name}")]
public ActionResult<Hamburger> GetJosResult(string name)
{
    var hamburgerResult = _getHamburgerJosResultQuery.Execute(name);
    return hamburgerResult switch
    {
        SuccessResult<Hamburger> successResult => new OkObjectResult(successResult.Data),
        NotFoundResult<Hamburger> notFoundResult => new NotFoundResult(),
        ErrorResult<Hamburger> errorResult => new StatusCodeResult(500),
        _ => new StatusCodeResult(500)
    };
}

We are taking advantage of the switch expression and by doing so the code in our action method becomes quite nice and readable (in my opinion... :) ). Now we don't need to rely on a error message to return a 404 Not Found.

The whole idea of my implementation of the Result class is that you should create specific classes that adds some context that can help you further up in the chain to make the correct decision. By using classes instead of for example an enum the solution becomes more flexible. Now I can put the Result class in a nuget package and then use it in some other project where I might need to handle different kinds of errors. The only thing I need to do then is inherit from ErrorResult.

You need to handle validation errors?

public class ValidationErrorResult : ErrorResult
{
    public ValidationErrorResult(string message) : base(message)
    {
    }

    public ValidationErrorResult(string message, IReadOnlyCollection<ValidationError> errors) : base(message, errors)
    {
    }
}

public class ValidationError : Error
{
    public ValidationError(string propertyName, string details) : base(null, details)
    {
        PropertyName = propertyName;
    }

    public string PropertyName { get; }
}
public Result ValidateMyData(MyData data)
{
    if(data.IsValid)
    {
        return new SuccessResult();
    }
    return new ValidationErrorResult("Error when validating...", data.Errors.Select(x => new ValidationError(x.PropertyName, x.Message)))
}

Or you need access to the http status code to see if you should retry?

public class HttpErrorResult : ErrorResult
{
    public HttpStatusCode StatusCode { get; }

    public HttpErrorResult(string message, HttpStatusCode statusCode) : base(message)
    {
        StatusCode = statusCode;
    }

    public HttpErrorResult(string message, IReadOnlyCollection<Error> errors, HttpStatusCode statusCode) : base(message, errors)
    {
        StatusCode = statusCode;
    }
}
...
var result = GetResult();
if(result is HttpErrorResult httpErrorResult)
{
   if(httpErrorResult.StatusCode == HttpStatusCode.BadGateway)
   {
       Retry();
   }
}
....

Usage

In 99% of the cases you will use it like this:

var result = GetData();
if(result.Success)
{
   // Do something
}
else
{
   // Handle error
}

But as soon as you need more context/info about what went wrong you will use it like this:

var result = GetData();

return result switch
{
    SuccessResult<Hamburger> successResult => HandleSuccess(successResult),
    NotFoundResult<Hamburger> notFoundResult => HandleNotFoundResult(notFoundResult),
    ErrorResult<Hamburger> errorResult => HandleErrorResult(errorResult),
        _ => result.MissingPatternMatch();
};

Or like this:

var result = GetData();

if(result.Failure)
{
    if(result is NotFoundResult notFoundResult)
    {
       // Handle not found result
    }
    // Handle "unknown" error
}

This gives you great flexibility imo!

Handling exceptions

The following code can still (let's pretend at least) throw exceptions:

public class InMemoryGetHamburgerJosResultQuery : IGetHamburgerJosResultQuery
{
    private static readonly IReadOnlyCollection<Hamburger> Hamburgers = new List<Hamburger>
    {
        new Hamburger("Double Cheese"),
        new Hamburger("Big Mac"),
        new Hamburger("Hamburger")
    };

    public Result<Hamburger> Execute(string name)
    {
        var hamburger = Hamburgers.FirstOrDefault(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase));

        if (hamburger == null)
        {
            return new NotFoundResult<Hamburger>("Could not find any hamburgers");
        }

        return new SuccessResult<Hamburger>(hamburger);
    }
}

I usually wrap my queries in a try catch like this:

public class InMemoryGetHamburgerJosResultQuery : IGetHamburgerJosResultQuery
{
    private static readonly IReadOnlyCollection<Hamburger> Hamburgers = new List<Hamburger>
    {
        new Hamburger("Double Cheese"),
        new Hamburger("Big Mac"),
        new Hamburger("Hamburger")
    };

    public Result<Hamburger> Execute(string name)
    {
        try
        {
            var hamburger = Hamburgers.FirstOrDefault(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase));

            if (hamburger == null)
            {
                return new NotFoundResult<Hamburger>("Could not find any hamburgers");
            }

            return new SuccessResult<Hamburger>(hamburger);
        }
        catch (SomeRecoverableDatabaseException e)
        {
            return new DatabaseErrorResult(e);
        }

        return new ErrorResult<Hamburger>("Error when trying to fetch hamburger");
    }
}

Note that I only tend to catch exceptions that I can recovery from. This is just a matter of taste though, it's completely fine to catch all exceptions and return a Result as well, the important thing is that you handle the error.

Closing thoughts

This was just some quick thoughts about how I usually work with errors in my applications. I find that I produce fewer bugs since I'm forced to handle all different scenarios:

Compare this...

public string GetName()
{
    var data = GetName();
    return data.Name; // Says boom if data is null;
}

...to this

public string GetName()
{
    var dataResult = GetName();
    if(dataResult is SuccessResult<Data> successResult)
    {
        return successResult.Data.Name;
    }
    return string.Empty; // Or throw, or null, or maybe return a RESULT... :)
}

In the first example I don't know if GetName has succeeded. In the second example I need to check if it was successful before accessing the .Data property.

Using a Result class is not a silver bullet when it comes to error handling, you will still need to handle exceptions but I find that I tend to handle them much closer to the source. Gone are all try catchs in my controllers that catches some exception that happened in the 35:th layer of code... :)

All code in this post can be found here.

Select action method based on header value - ASP.NET Core

$
0
0
Select action method based on header value - ASP.NET Core

Photo by Constantin Shimonenko

Scenario

We are using a third party system for managing our users. Whenever something happens with a user (added/updated/removed...), the system will notify us by using a webhook. The system that we are using can only post to one endpoint. To be able to identify what kind of event that happend, they are also sending a header, X-Operation.

Let's see how we're currently handling this:

One action method - switch case

[ApiController]
[Route("[controller]")]
public IActionResult HandleWebhook()
{
    Request.Headers.TryGetValue("X-Operation", out var operationHeader);

    switch (operationHeader)
    {
        case "UserAdded":
            // Handle UserAdded here
            return new OkObjectResult("UserAdded operation was handled");
        case "UserRemoved":
            // Handle UserRemoved here
            return new OkObjectResult("UserRemoved operation was handled");
        case "UserUpdated":
            // Handle UserUpdated here
            return new OkObjectResult("UserUpdated operation was handled");
        default:
            return new BadRequestObjectResult($"{operationHeader} is not supported");
    }
}

As you can see, we're having one action method that parses out the X-Operation header and we're then using a switch case to handle the different events. This works OK, but I feel like it's a bit clumsy. It would feel much better if we could have one action method per operation.

IActionConstraint

It's quite easy to achieve this in ASP.NET Core by using IActionConstraint.

Supports conditional logic to determine whether or not an associated action is valid to be selected for the given request.

I will not go into any depth regarding IActionConstraint, if you want to learn more about it I recommend this post by Filip W.

So, we need to be able to say something like THIS action method handles THIS header with THIS value.

To be able to do that we'll start by creating a new attribute that we'll use to decorate our action methods.

public class WebhookOperationAttribute : Attribute, IActionConstraint
{
    private readonly string _header;
    private readonly string _value;

    public WebhookOperationAttribute(string header, string value, int order = 0)
    {
        _header = header;
        _value = value;
        Order = order;
    }

    public bool Accept(ActionConstraintContext context)
    {
        if (!context.RouteContext.HttpContext.Request.Headers.ContainsKey(_header))
        {
            return false;
        }

        var headerValue = context.RouteContext.HttpContext.Request.Headers[_header];
        return headerValue.Equals(_value);
    }

    public int Order { get; }
}
  1. If the request doesn't contain the specified header -> return false.
  2. Extract the header value.
  3. Check if the header value matches the specified value.

One thing that's good to keep in mind here is that the above code will be executed for every decorated action method with the same Order.

Once the stage order is identified, each action has all of its constraints in that stage executed. If any constraint does not match, then that action is not a candidate for selection. If any actions with constraints in the current state are still candidates, then those are the 'best' actions and this process will repeat with the next stage on the set of 'best' actions. If after processing the subsequent stages of the 'best' actions no candidates remain, this process will repeat on the set of 'other' candidate actions from this stage (those without a constraint).

The only thing left for us to do now is to create our action methods and decorate them accordingly.

[ApiController]
[Route("[controller]")]
public class WebhookController : ControllerBase
{
    [HttpPost]
    [WebhookOperation(WebhookHeader.Operation, WebhookOperation.UserAdded)]
    public IActionResult UserAdded()
    {
        return new OkObjectResult($"Hello from {nameof(UserAdded)} action");
    }

    [HttpPost]
    [WebhookOperation(WebhookHeader.Operation, WebhookOperation.UserRemoved)]
    public IActionResult UserRemoved()
    {
        return new OkObjectResult($"Hello from {nameof(UserRemoved)} action");
    }

    [HttpPost]
    [WebhookOperation(WebhookHeader.Operation, WebhookOperation.UserUpdated)]
    public IActionResult UserUpdated()
    {
        return new OkObjectResult($"Hello from {nameof(UserUpdated)} action");
    }
}

We can then verify that it works as intended with the following integration test.

public class WebhookIntegrationTests : IClassFixture<WebApplicationFactory<Startup>>
{
    private readonly WebApplicationFactory<Startup> _webApplicationFactory;

    public WebhookIntegrationTests(WebApplicationFactory<Startup> webApplicationFactory)
    {
        _webApplicationFactory = webApplicationFactory ?? throw new ArgumentNullException(nameof(webApplicationFactory));
    }

    [Theory]
    [InlineData(WebhookOperation.UserAdded)]
    [InlineData(WebhookOperation.UserUpdated)]
    [InlineData(WebhookOperation.UserRemoved)]
    public async Task ShouldUseCorrectActionForWebhookBasedOnOperationHeader(string operation)
    {
        var client = _webApplicationFactory.CreateClient();
        var request = new HttpRequestMessage(HttpMethod.Post, "webhook")
        {
            Headers = {{WebhookHeader.Operation, operation}}
        };

        var response = await client.SendAsync(request);
        var responseBody = await response.Content.ReadAsStringAsync();

        responseBody.ShouldBe($"Hello from {operation} action");
    }
}

This was just a quick write up of how to route to a specific action method based on a header value. If you want to learn more about IActionConstraint I recommend the following posts:

If you are using Endpoint routing, you might want to have a look at this issue on GitHub.

All code in this post can be found here.

WOPI Integration with ASP.NET Core - lessons learned

$
0
0
WOPI Integration with ASP.NET Core - lessons learned

Photo by Nikhita S

Introduction

Recently I've been working on adding "Office for the Web" support to our system. This will enable us to edit office documents uploaded to our system directly in the browser. To add support for Office for the web you need to implement a protocol known as WOPI. Sadly I can't provide a complete (code) solution for this since the code I've written is proprietary...but I can still share a bunch of tips and tricks!

WOPI

There's a few steps that you need to do before Office for the web will work.

  1. Be a member of the Office 365 - Cloud Storage Partner Program
  2. Implement the WOPI protocol - a set of REST endpoints that expose information about the documents that you want to view or edit in Office for the web.
  3. Add support for WOPI Discovery
  4. Create a HTML page that will embedd the Office for the web iframe.

My tips will focus on step 2 - The REST endpoints (using ASP.NET Core).

Lessons learned

Use multiple endpoints

The WOPI protocol uses the same URL path for multiple different operations. They provide a header, X-WOPI-Override, that you can inspect and then decide which code to run. In our old solution we only had one endpoint that mapped to this path and the code is quite messy because of that (a lot of switching/if/else).

IMO, a better solution is to inspect the header and then map it to a corresponding action method instead to achieve a better separation of concerns. In ASP.NET Core this is quite easy to achieve using IActionConstraint. You can check out my previous blog post for a complete example.

PascalCase your JSON responses

I spent a few hours wondering why my WOPI integration didn't work, I had green integration tests and everything. Turns out I returned camelCased JSON. WOPI requires PascalCased JSON. Big facepalm when I finally figured that one out.

Write Integration tests

Whenever I build an API I always create integration tests for my endpoints. I use them mainly for veryfing that my API behaves as expected (returning correct status codes, response body and so on). It's also really easy to write integration tests in ASP.NET core so there's really no reason for not doing it :).

Let's take the GetLock WOPI operation as an example. The specification says that your API should return the following status codes:

  • 200 OK – Success; an X-WOPI-Lock response header containing the value of the current lock on the file must always be included when using this response code
  • 401 Unauthorized – Invalid access token
  • 404 Not Found – Resource not found/user unauthorized
  • 409 Conflict – Lock mismatch/locked by another interface; an X-WOPI-Lock response header containing the value of the current lock on the file must always be included when using this response code
  • 500 Internal Server Error – Server error
  • 501 Not Implemented – Operation not supported

I then proceed to create a test for all of the different cases (the only thing I mock/fake in my integration tests are external dependencies like databases) to ensure that my API follows the specification.
WOPI Integration with ASP.NET Core - lessons learned

Get the test application up and running ASAP

WOPI provides something they call Interactive Validation.
WOPI Integration with ASP.NET Core - lessons learned
This is an automated tool that will send requests towards your application and validate your implementation. It's AWESOME...

...but beware of (lack of) SessionContext

The validation application does not support SessionContext.
This also wasted a few hours for me. Luckily for me I could work around this with a hack, I basically look for the SessionContext header, if it's not present I will construct it manually using the currently authenticated user data.

SessionContext

I use session context for storing information about the document currently being viewed/edited. The key I use for this looks like this UserId-FileId.

One thing the documentation fails to mention is that the session context value returned from WOPI is base64 encoded.

Authentication

WOPI will pass on an access token in all their requests towards your endpoints. You are responsible for creating this access token and then passing it to WOPI via the IFRAME.

I wrote a custom AuthenticationHandler that reads the access token from the header/querystring and then creates an identity based on the information in the access token. The access token is simply a key that I use to look up additional information regarding the user that I've stored in redis.

InMemoryFileService

We have our documents in Azure blob storage. To access the documents we have a microservice that's responsible for reading/writing data from/to Azure.

When doing the integration I focused on getting the wopi endpoints working before doing the integration towards our microservice. What I did was that I created a interface in my WOPI code called IFileService and then I created an in memory implementation of this interface. The in memory implementation contained a few different basic office files. I then used this implementation when validating my WOPI endpoints. When I was sure that my endpoints worked as they should, I simply switched the implementation from the InMemoryFileService to using the real one.

Return empty header

When reading the documentation you will see something like this:

...the X-WOPI-Lock response header should be set to the empty string or omitted completely.

I chose to omit it completley but then the validation application failed a couple of tests. So I then chose to return an empty header instead and updated my integration tests. All green, nice. But when I ran the validation application it still failed the tests with the same error, weird? Turns out that when you're running your application behind IIS in a Windows App Service, the empty header will simply be removed.
Luckily the GitHub issue contains a workaround, simply add a space to your empty header and you're good to go.

The reason for using a WINDOWS App Service you might ask...?

Remote Debugging

This was the first time that I used remote debugging. It's a really powerful feature, especially when combining it with the validation application. If I had some failing tests, I could just hook up my debugger and then run the tests again. It was really valuable to be able to inspect the WOPI requests to get a better understanding of the protocol.

Transform C# objects to a flat string dictionary

$
0
0
Transform C# objects to a flat string dictionary

Photo by Kenan Reed

The problem

Because of (third party) reasons I needed to transform some objects to a flat Dictionary<string, string>.

Example

Given the following class...

public class MyClass
{
    public bool Boolean { get; set; }
    public string String { get; set; }
    public Guid Guid { get; set; }
    public int Integer { get; set; }
    public IEnumerable<MyClass> MyClasses { get; set; }
    public IEnumerable<string> Strings { get; set; }
    public MyNestedClass MyNestedClass { get; set; }
}

public class MyNestedClass
{
    public bool Boolean { get; set; }
    public string String { get; set; }
    public Guid Guid { get; set; }
    public int Integer { get; set; }
}

I needed to transform this...

var myClass = new MyClass
{
    Boolean = true,
    Guid = Guid.NewGuid(),
    Integer = 100,
    String = "string"
};

...to this:

...
var result = sut.Execute(myClass, prefix: "Data");

result.ShouldContainKeyAndValue($"Data.Boolean", "true");
result.ShouldContainKeyAndValue($"Data.Guid", myClass.Guid.ToString());
result.ShouldContainKeyAndValue($"Data.Integer", myClass.Integer.ToString());
result.ShouldContainKeyAndValue($"Data.String", myClass.String);

I also needed to support nested classes...

var myClass = new MyClass
{
    Boolean = true,
    Guid = Guid.NewGuid(),
    Integer = 100,
    String = "string",
    MyNestedClass = new 
    {
        Boolean = true,
        Guid = Guid.NewGuid(),
        Integer = 100,
        String = "string"
    }
};

var result = sut.Execute(myClass, prefix: "Data");

result.ShouldContainKeyAndValue("Data.MyNestedClass.Boolean", myClass.MyNestedClass.Boolean.ToString().ToLower());
result.ShouldContainKeyAndValue("Data.MyNestedClass.Guid", myClass.MyNestedClass.Guid.ToString());
result.ShouldContainKeyAndValue("Data.MyNestedClass.Integer", myClass.MyNestedClass.Integer.ToString());
result.ShouldContainKeyAndValue("Data.MyNestedClass.String", myClass.MyNestedClass.String);

And Enumerables

var myClass = new MyClass
{
    Boolean = true,
    Guid = Guid.NewGuid(),
    Integer = 100,
    String = "string",
    MyClasses = new List<MyClass>
    {
        new MyClass
        {
            Boolean = true,
            Guid = Guid.NewGuid(),
            Integer = 100,
            String = "string"
        },
        new MyClass
        {
            Boolean = true,
            Guid = Guid.NewGuid(),
            Integer = 100,
            String = "string"
        }
    }
};

var result = sut.Execute(myClass, prefix: "Data");
var myClassesList = myClass.MyClasses.ToList();

result.ShouldContainKeyAndValue("Data.MyClasses[0].Boolean", myClassesList[0].Boolean.ToString().ToLower());
result.ShouldContainKeyAndValue("Data.MyClasses[0].Guid", myClassesList[0].Guid.ToString());
result.ShouldContainKeyAndValue("Data.MyClasses[0].Integer", myClassesList[0].Integer.ToString());
result.ShouldContainKeyAndValue("Data.MyClasses[0].String", myClassesList[0].String);
result.ShouldContainKeyAndValue("Data.MyClasses[1].Boolean", myClassesList[1].Boolean.ToString().ToLower());
result.ShouldContainKeyAndValue("Data.MyClasses[1].Guid", myClassesList[1].Guid.ToString());
result.ShouldContainKeyAndValue("Data.MyClasses[1].Integer", myClassesList[1].Integer.ToString());
result.ShouldContainKeyAndValue("Data.MyClasses[1].String", myClassesList[1].String);

Implementations

I created the following interface and got to work.

public interface IFlatDictionaryProvider
{
    Dictionary<string, string> Execute(object @object, string prefix = "");
}
Extension methods used in the different implementations
    internal static class Extensions
    {
        internal static bool IsValueTypeOrString(this Type type)
        {
            return type.IsValueType || type == typeof(string);
        }

        internal static string ToStringValueType(this object value)
        {
            return value switch
            {
                DateTime dateTime => dateTime.ToString("o"),
                bool boolean => boolean.ToStringLowerCase(),
                _ => value.ToString()
            };
        }

        internal static bool IsIEnumerable(this Type type)
        {
            return type.IsAssignableTo(typeof(IEnumerable));
        }

        internal static string ToStringLowerCase(this bool boolean)
        {
            return boolean ? "true" : "false";
        }
    }

"Hard coded" implementation

First I wanted to create something that made my tests go green, so I started with the following "hard coded" implementation. Bare with me here, this is just the beginning... :)

public class HardCodedImplementation : IFlatDictionaryProvider
{
    public Dictionary<string, string> Execute(object @object, string prefix = "")
    {
        var myClass = (MyClass)@object;
        var dictionary = new Dictionary<string, string>
        {
            { $"{(string.IsNullOrWhiteSpace(prefix) ? string.Empty : $"{prefix}.")}{nameof(myClass.Boolean)}" , myClass.Boolean.ToString().ToLower()},
            { $"{(string.IsNullOrWhiteSpace(prefix) ? string.Empty : $"{prefix}.")}{nameof(myClass.Integer)}", myClass.Integer.ToString()},
            { $"{(string.IsNullOrWhiteSpace(prefix) ? string.Empty : $"{prefix}.")}{nameof(myClass.Guid)}", myClass.Guid.ToString()},
            { $"{(string.IsNullOrWhiteSpace(prefix) ? string.Empty : $"{prefix}.")}{nameof(myClass.String)}", myClass.String},
            { $"{(string.IsNullOrWhiteSpace(prefix) ? string.Empty : $"{prefix}.")}{nameof(myClass.MyNestedClass)}.{nameof(myClass.MyNestedClass.Boolean)}", myClass.MyNestedClass.Boolean.ToString().ToLower()},
            { $"{(string.IsNullOrWhiteSpace(prefix) ? string.Empty : $"{prefix}.")}{nameof(myClass.MyNestedClass)}.{nameof(myClass.MyNestedClass.Integer)}", myClass.MyNestedClass.Integer.ToString()},
            { $"{(string.IsNullOrWhiteSpace(prefix) ? string.Empty : $"{prefix}.")}{nameof(myClass.MyNestedClass)}.{nameof(myClass.MyNestedClass.Guid)}", myClass.MyNestedClass.Guid.ToString()},
            { $"{(string.IsNullOrWhiteSpace(prefix) ? string.Empty : $"{prefix}.")}{nameof(myClass.MyNestedClass)}.{nameof(myClass.MyNestedClass.String)}", myClass.MyNestedClass.String}
        };

        var counter = 0;

        foreach (var @string in myClass?.Strings ?? Array.Empty<string>())
        {
            dictionary.Add($"{(string.IsNullOrWhiteSpace(prefix) ? string.Empty : $"{prefix}.")}{nameof(myClass.Strings)}[{counter++}]", @string);
        }

        counter = 0;
        foreach (var myClassItem in myClass.MyClasses)
        {
            dictionary.Add($"{(string.IsNullOrWhiteSpace(prefix) ? string.Empty : $"{prefix}.")}{nameof(myClass.MyClasses)}[{counter}].{nameof(myClassItem.Boolean)}", myClassItem.Boolean.ToString().ToLower());
            dictionary.Add($"{(string.IsNullOrWhiteSpace(prefix) ? string.Empty : $"{prefix}.")}{nameof(myClass.MyClasses)}[{counter}].{nameof(myClassItem.Integer)}", myClassItem.Integer.ToString());
            dictionary.Add($"{(string.IsNullOrWhiteSpace(prefix) ? string.Empty : $"{prefix}.")}{nameof(myClass.MyClasses)}[{counter}].{nameof(myClassItem.Guid)}", myClassItem.Guid.ToString());
            dictionary.Add($"{(string.IsNullOrWhiteSpace(prefix) ? string.Empty : $"{prefix}.")}{nameof(myClass.MyClasses)}[{counter}].{nameof(myClassItem.String)}", myClassItem.String);
            dictionary.Add($"{(string.IsNullOrWhiteSpace(prefix) ? string.Empty : $"{prefix}.")}{nameof(myClass.MyClasses)}[{counter}].{nameof(myClassItem.MyNestedClass)}.{nameof(myClassItem.MyNestedClass.Boolean)}", myClassItem.MyNestedClass.Boolean.ToString().ToLower());
            dictionary.Add($"{(string.IsNullOrWhiteSpace(prefix) ? string.Empty : $"{prefix}.")}{nameof(myClass.MyClasses)}[{counter}].{nameof(myClassItem.MyNestedClass)}.{nameof(myClassItem.MyNestedClass.Integer)}", myClassItem.MyNestedClass.Integer.ToString());
            dictionary.Add($"{(string.IsNullOrWhiteSpace(prefix) ? string.Empty : $"{prefix}.")}{nameof(myClass.MyClasses)}[{counter}].{nameof(myClassItem.MyNestedClass)}.{nameof(myClassItem.MyNestedClass.Guid)}", myClassItem.MyNestedClass.Guid.ToString());
            dictionary.Add($"{(string.IsNullOrWhiteSpace(prefix) ? string.Empty : $"{prefix}.")}{nameof(myClass.MyClasses)}[{counter}].{nameof(myClassItem.MyNestedClass)}.{nameof(myClassItem.MyNestedClass.String)}", myClassItem.MyNestedClass.String);
            counter++;
        }

        return dictionary;
    }
}

So, obviously, this is not a good solution to the problem at all. It only supports MyClass and everytime a new property gets added/removed I will need to update this code. And let's not even talk about all the nameof and string shenanigans...

Implementation1

This was my first attempt of a more dynamic solution.

  1. Get all properties for the passed in object.
  2. Loop through all properties.
  3. Get the property value using reflection.
  4. If the value is a value type or a string, call .ToString() on it and add it to the dictionary.
  5. If not, check if it's an IEnumerable.
    If IEnumerable -> loop through the items and call Flatten recursively.
    If not -> call Flatten recursively.
public class Implementation1 : IFlatDictionaryProvider
{
    public Dictionary<string, string> Execute(object @object, string prefix = "")
    {
        var dictionary = new Dictionary<string, string>();
        Flatten(dictionary, @object, prefix);
        return dictionary;
    }

    private static void Flatten(
        IDictionary<string, string> dictionary,
        object source,
        string name)
    {
        var properties = source.GetType().GetProperties().Where(x => x.CanRead);
        foreach (var property in properties)
        {
            var key = string.IsNullOrWhiteSpace(name) ? property.Name : $"{name}.{property.Name}";
            var value = property.GetValue(source, null);

            if (value == null)
            {
                dictionary[key] = null;
                continue;
            }

            if (property.PropertyType.IsValueTypeOrString())
            {
                dictionary[key] = value.ToStringValueType();
            }
            else
            {
                if (value is IEnumerable enumerable)
                {
                    var counter = 0;
                    foreach (var item in enumerable)
                    {
                        var itemKey = $"{key}[{counter++}]";
                        if (item.GetType().IsValueTypeOrString())
                        {
                            dictionary.Add(itemKey, item.ToStringValueType());
                        }
                        else
                        {
                            Flatten(dictionary, item, itemKey);
                        }
                    }
                }
                else
                {
                    Flatten(dictionary, value, key);
                }
            }
        }
    }
}

Implementation2

This is more or less the same implementation as Implementation1, the only difference is that I'm caching the properties per Type to avoid doing the GetProperties() call more than once.

public class Implementation2 : IFlatDictionaryProvider
{
    private static readonly ConcurrentDictionary<Type, PropertyInfo[]> CachedTypeProperties;

    static Implementation2()
    {
        CachedTypeProperties = new ConcurrentDictionary<Type, PropertyInfo[]>();
    }

    public Dictionary<string, string> Execute(object @object, string prefix = "")
    {
        var dictionary = new Dictionary<string, string>();
        Flatten(dictionary, @object, prefix);
        return dictionary;
    }

    private static void Flatten(
        IDictionary<string, string> dictionary,
        object source,
        string name)
    {
        var properties = GetProperties(source.GetType());
        foreach (var property in properties)
        {
            var key = string.IsNullOrWhiteSpace(name) ? property.Name : $"{name}.{property.Name}";
            var value = property.GetValue(source, null);

            if (value == null)
            {
                dictionary[key] = null;
                continue;
            }

            if (property.PropertyType.IsValueTypeOrString())
            {
                dictionary[key] = value.ToStringValueType();
            }
            else
            {
                if (value is IEnumerable enumerable)
                {
                    var counter = 0;
                    foreach (var item in enumerable)
                    {
                        var itemKey = $"{key}[{counter++}]";
                        if (item.GetType().IsValueTypeOrString())
                        {
                            dictionary.Add(itemKey, item.ToStringValueType());
                        }
                        else
                        {
                            Flatten(dictionary, item, itemKey);
                        }
                    }
                }
                else
                {
                    Flatten(dictionary, value, key);
                }
            }
        }
    }

    private static IEnumerable<PropertyInfo> GetProperties(Type type)
    {
        if (CachedTypeProperties.TryGetValue(type, out var result))
        {
            return result;
        }

        var properties = type.GetProperties().Where(x => x.CanRead).ToArray();
        CachedTypeProperties.TryAdd(type, properties);
        return properties;
    }
}

Implementation3

The first two implementations solved the problem and all my tests were green, nice. However, I felt that I could do better. There were still a bunch of reflection calls that I wanted to avoid, if possible.

My idea was to cache the properties getter methods using a compiled lambda. I've done that before to solve similar problems when performance was important.

Something like this:

  1. Get all the properties for the type
    A Check if they are cached, if so, return them
    B They are not cached, get them and populate the cache with the PropertyInfo and the getter method for that property.
  2. Loop through all properties
  3. Get the value of the property using the getter method.
  4. If the value is a value type or a string, call .ToString() on it.
  5. If not, check if it's an IEnumerable.
    If IEnumerable -> loop through the items and call ExecuteInternal recursively.
    If not -> call ExecuteInternal recursively.
public class Implementation3 : IFlatDictionaryProvider
{
    private static readonly ConcurrentDictionary<Type, Dictionary<PropertyInfo, Func<object, object>>> CachedProperties;

    static Implementation3()
    {
        CachedProperties = new ConcurrentDictionary<Type, Dictionary<PropertyInfo, Func<object, object>>>();
    }

    public Dictionary<string, string> Execute(object @object, string prefix = "")
    {
        return ExecuteInternal(@object, prefix: prefix);
    }

    private static Dictionary<string, string> ExecuteInternal(
        object @object,
        Dictionary<string, string> dictionary = default,
        string prefix = "")
    {
        dictionary ??= new Dictionary<string, string>();
        var type = @object.GetType();
        var properties = GetProperties(type);

        foreach (var (property, getter) in properties)
        {
            var key = string.IsNullOrWhiteSpace(prefix) ? property.Name : $"{prefix}.{property.Name}";
            var value = getter(@object);

            if (value == null)
            {
                dictionary.Add(key, null);
                continue;
            }

            if (property.PropertyType.IsValueTypeOrString())
            {
                dictionary.Add(key, value.ToStringValueType());
            }
            else
            {
                if (value is IEnumerable enumerable)
                {
                    var counter = 0;
                    foreach (var item in enumerable)
                    {
                        var itemKey = $"{key}[{counter++}]";
                        var itemType = item.GetType();
                        if (itemType.IsValueTypeOrString())
                        {
                            dictionary.Add(itemKey, item.ToStringValueType());
                        }
                        else
                        {
                            ExecuteInternal(item, dictionary, itemKey);
                        }
                    }
                }
                else
                {
                    ExecuteInternal(value, dictionary, key);
                }
            }
        }

        return dictionary;
    }

    private static Dictionary<PropertyInfo, Func<object, object>> GetProperties(Type type)
    {
        if (CachedProperties.TryGetValue(type, out var properties))
        {
            return properties;
        }

        CacheProperties(type);
        return CachedProperties[type];
    }
    
    private static void CacheProperties(Type type)
    {
        if (CachedProperties.ContainsKey(type))
        {
            return;
        }

        CachedProperties[type] = new Dictionary<PropertyInfo, Func<object, object>>();
        var properties = type.GetProperties().Where(x => x.CanRead);
        foreach (var propertyInfo in properties)
        {
            var getter = CompilePropertyGetter(propertyInfo);
            CachedProperties[type].Add(propertyInfo, getter);
            if (!propertyInfo.PropertyType.IsValueTypeOrString())
            {
                if (propertyInfo.PropertyType.IsIEnumerable())
                {
                    var types = propertyInfo.PropertyType.GetGenericArguments();
                    foreach (var genericType in types)
                    {
                        if (!genericType.IsValueTypeOrString())
                        {
                            CacheProperties(genericType);
                        }
                    }
                }
                else
                {
                    CacheProperties(propertyInfo.PropertyType);
                }
            }
        }
    }

    // Inspired by Zanid Haytam
    // https://blog.zhaytam.com/2020/11/17/expression-trees-property-getter/
    private static Func<object, object> CompilePropertyGetter(PropertyInfo property)
    {
        var objectType = typeof(object);
        var objectParameter = Expression.Parameter(objectType);
        var castExpression = Expression.TypeAs(objectParameter, property.DeclaringType);
        var convertExpression = Expression.Convert(
            Expression.Property(typeAsExpression, property),
            objectType);
            
        return Expression.Lambda<Func<object, object>>(
            convertExpression,
            objectParameter).Compile();
    }
}

The interesting stuff happens in the CacheProperties and CompilePropertyGetter methods, let's break them down.

CacheProperties

private static void CacheProperties(Type type)
{
    if (CachedProperties.ContainsKey(type))
    {
        return;
    }

    CachedProperties[type] = new Dictionary<PropertyInfo, Func<object, object>>();
    // Get all the properties with reflection
    var properties = type.GetProperties().Where(x => x.CanRead);
    foreach (var propertyInfo in properties)
    {
        // Create a delegate for the property getter method
        var getter = CompilePropertyGetter(propertyInfo);
        // Cache the delegate
        CachedProperties[type].Add(propertyInfo, getter);
        // If it's not a string or value type...
        if (!propertyInfo.PropertyType.IsValueTypeOrString())
        {
            if (propertyInfo.PropertyType.IsIEnumerable())
            {
                // Get all types for the IEnumerable
                var types = propertyInfo.PropertyType.GetGenericArguments();
                foreach (var genericType in types)
                {
                    // If it's a "reference type", cache the properties for said type
                    if (!genericType.IsValueTypeOrString())
                    {
                        CacheProperties(genericType);
                    }
                }
            }
            else
            {
                // It's a reference type, cache the properties for said type
                CacheProperties(propertyInfo.PropertyType);
            }
        }
    }
}

I'm caching all properties of the type together with the property getter method. If a property is a reference type, I recursively call the CacheProperties method for that type so that I cache the properties for that type as well.

CompilePropertyGetter

private static Func<object, object> CompilePropertyGetter(PropertyInfo property)
{
    var objectType = typeof(object);
    // This is the type that we will pass to the delegate (object)
    var objectParameter = Expression.Parameter(objectType);
    // Casts the passed in object to the properties type
    var castExpression = Expression.TypeAs(objectParameter, property.DeclaringType);
    // Gets the value from the property and converts it to object
    var convertExpression = Expression.Convert(
        Expression.Property(typeAsExpression, property),
        objectType);
        
    // Creates a compiled lambda that we will cache.
    return Expression.Lambda<Func<object, object>>(
        convertExpression,
        objectParameter).Compile();
}

Benchmarks

BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042
AMD Ryzen 9 3900X, 1 CPU, 24 logical and 12 physical cores
.NET Core SDK=6.0.100-preview.3.21202.5
  [Host]        : .NET Core 5.0.4 (CoreCLR 5.0.421.11614, CoreFX 5.0.421.11614), X64 RyuJIT
  .NET Core 5.0 : .NET Core 5.0.4 (CoreCLR 5.0.421.11614, CoreFX 5.0.421.11614), X64 RyuJIT

Job=.NET Core 5.0  Runtime=.NET Core 5.0  


|                           Method |     Mean |     Error |    StdDev | Ratio |  Gen 0 |  Gen 1 | Gen 2 | Allocated |
|--------------------------------- |---------:|----------:|----------:|------:|-------:|-------:|------:|----------:|
|         Implementation1Benchmark | 9.243 μs | 0.0620 μs | 0.0580 μs |  1.00 | 0.7324 | 0.0153 |     - |   5.98 KB |
|         Implementation2Benchmark | 8.139 μs | 0.0494 μs | 0.0462 μs |  0.88 | 0.6714 | 0.0153 |     - |   5.49 KB |
|         Implementation3Benchmark | 4.320 μs | 0.0222 μs | 0.0173 μs |  0.47 | 0.6485 | 0.0153 |     - |    5.3 KB |
| ImplementationHardCodedBenchmark | 4.582 μs | 0.0302 μs | 0.0252 μs |  0.50 | 0.6485 | 0.0153 |     - |   5.34 KB |

By caching the properties in Implementation2 we can see that the allocations was reduced and it also performed a bit faster, no suprises there.

Implementation3 allocates less AND runs faster than the "hard coded" implementation...let's call that a success shall we? :)

A complete solution (with tests) can be found at GitHub


New major version of JOS.ContentSerializer - Version 5.0

$
0
0

I finally had some time to fix a long standing bug in JOS.ContentSerializer.

The bug

If you passed in a custom IContentSerializerSettings to the Serialize or GetStructuredData method on IContentSerializer, it was simply ignored... :)

The default implementation of IPropertyManager did not pass on the IContentSerializerSettings to the IPropertyHandlers. Instead, the default settings registered in DI was used.

PropertyManager.cs

public Dictionary<string, object> GetStructuredData(
    IContentData contentData,
    IContentSerializerSettings settings) // As you can see, settings is not used at all...
{
    var properties = this._propertyResolver.GetProperties(contentData);
    var structuredData = new Dictionary<string, object>();

    foreach (var property in properties)
    {
        var propertyHandler = this._propertyHandlerService.GetPropertyHandler(property);
        if (propertyHandler == null)
        {
            Trace.WriteLine($"No PropertyHandler was found for type '{property.PropertyType}'");
            continue;
        }

        var method = propertyHandler.GetType().GetMethod(nameof(IPropertyHandler<object>.Handle));
        if (method != null)
        {
            var key = this._propertyNameStrategy.GetPropertyName(property);
            var value = property.GetValue(contentData);
            var result = method.Invoke(propertyHandler, new[] { value, property, contentData });
            structuredData.Add(key, result);
        }
    }
    return structuredData;
}

That's not weird though, because the Handle method didn't have a IContentSerializerSetting parameter.
IPropertyHandler<>

public interface IPropertyHandler<in T>
{
    object Handle(T value, PropertyInfo property, IContentData contentData);
}

The fix

IPropertyHandler<>

public interface IPropertyHandler<in T>
{
    object Handle(T value, PropertyInfo property, IContentData contentData);
    object Handle(T value, PropertyInfo property, IContentData contentData, IContentSerializerSettings settings);
}

PropertyManager.cs

public Dictionary<string, object> GetStructuredData(
    IContentData contentData,
    IContentSerializerSettings settings)
{
    var properties = this._propertyResolver.GetProperties(contentData);
    var structuredData = new Dictionary<string, object>();

    foreach (var property in properties)
    {
        var propertyHandler = this._propertyHandlerService.GetPropertyHandler(property);
        if (propertyHandler == null)
        {
            Trace.WriteLine($"No PropertyHandler was found for type '{property.PropertyType}'");
            continue;
        }

        // I've now refactored this so that I cache the methods as well
        var method = GetMethodInfo(propertyHandler);
        var key = this._propertyNameStrategy.GetPropertyName(property);
        var value = property.GetValue(contentData);
        // We are now using the new overload on IPropertyHandler that takes a IContentSerializerSettings
        var result = method.Invoke(propertyHandler, new[] { value, property, contentData, settings });
        structuredData.Add(key, result);
    }
    return structuredData;
}

private static MethodInfo GetMethodInfo(object propertyHandler)
{
    var type = propertyHandler.GetType();
    if (CachedHandleMethodInfos.ContainsKey(type))
    {
        CachedHandleMethodInfos.TryGetValue(type, out var cachedMethod);
        return cachedMethod;
    }

    var method = propertyHandler.GetType().GetMethods()
        .Where(x => x.Name.Equals(nameof(IPropertyHandler<object>.Handle)))
        .OrderByDescending(x => x.GetParameters().Length)
        .First();

    CachedHandleMethodInfos.TryAdd(type, method);
    return method;
}

The breaking change

Since I've introduced a new method on the IPropertyHandler<> interface this is a breaking change. If you have any custom property handlers you will need to implement the new method as well. Also, remember that the default implementation of IPropertyManager will only call the new method.

Here's an example of how I fixed the default implementation for the Url property.

Before

public class UrlPropertyHandler : IPropertyHandler<Url>
{
    private readonly IUrlHelper _urlHelper;
    private readonly IContentSerializerSettings _contentSerializerSettings;
    private const string MailTo = "mailto";

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

    public object Handle(Url url, PropertyInfo propertyInfo, IContentData contentData)
    {
        if (url == null)
        {
            return null;
        }

        if (url.Scheme == MailTo) return url.OriginalString;

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

            return url.PathAndQuery;
        }

        return this._urlHelper.ContentUrl(url, this._contentSerializerSettings.UrlSettings);
    }
}

After
As you can see, I'm just calling the new method from the old one and passing along the IContentSerializerSettings from DI to the new method.

public class UrlPropertyHandler : IPropertyHandler<Url>
{
    private readonly IUrlHelper _urlHelper;
    private readonly IContentSerializerSettings _contentSerializerSettings;
    private const string MailTo = "mailto";

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

    public object Handle(
        Url url,
        PropertyInfo propertyInfo,
        IContentData contentData)
    {
        return Handle(url, propertyInfo, contentData, _contentSerializerSettings);
    }

    public object Handle(
        Url url,
        PropertyInfo property,
        IContentData contentData,
        IContentSerializerSettings settings)
    {
        if (url == null)
        {
            return null;
        }

        if (url.Scheme == MailTo) return url.OriginalString;

        if (url.IsAbsoluteUri)
        {
            if (settings.UrlSettings.UseAbsoluteUrls)
            {
                return url.OriginalString;
            }

            return url.PathAndQuery;
        }

        return this._urlHelper.ContentUrl(url, settings.UrlSettings);
    }
}

How C# Records will change my life

$
0
0
How C# Records will change my life

Photo by Eric Krull

Introduction

Whenever I code I prefer to have immutable domain models.
Example:

public class Game
{
    public Game(Team homeTeam, Team awayTeam, GameResult result)
    {
        HomeTeam = homeTeam ?? throw new ArgumentNullException(nameof(homeTeam));
        AwayTeam = awayTeam ?? throw new ArgumentNullException(nameof(awayTeam));
        Result = result ?? throw new ArgumentNullException(nameof(result));
    }

    public Team HomeTeam { get; }
    public Team AwayTeam { get; }
    public GameResult Result { get; }
}

Here you can see that it's only possible to set the properties when creating the object (get only properties).

By doing this you can make sure that you don't accidentally mutate the state of the object. This would not compile for example:

...
var game = new Game(homeTeam, awayTeam, result);
game.AwayTeam = new Team(Guid.NewGuid(), "Arsenal"); // Compilation error

Immutability comes with a lot of benefits, but sometimes it can be a bit cumbersome to deal with when you only want to update some properties. Since the object is immutable, you need to create a copy with all the existing values and the new updated one.

I will show you how Records in C# 9 will greatly simplify this

Let's set the scene

Let's say that we are building an application related to betting. A user can place bets on different games.

Our current domain model (before records) looks something like this (greatly simplified):

public class Bet
{
    public Bet(string id, string userId, IReadOnlyCollection<BetRow> rows)
    {
        Id = id ?? throw new ArgumentNullException(nameof(id));
        UserId = userId ?? throw new ArgumentNullException(nameof(userId));
        Rows = rows ?? throw new ArgumentNullException(nameof(rows));
    }

    public string Id { get; }
    public string UserId {get; }
    public IReadOnlyCollection<BetRow> Rows { get; }
}

public class BetRow
{
    public BetRow(string gameId, int homeScore, int awayScore) : this(gameId, homeScore, awayScore, BetRowOutcome.Unsettled)
    {
    }

    public BetRow(string gameId, int homeScore, int awayScore, BetRowOutcome outcome)
    {
        GameId = gameId ?? throw new ArgumentNullException(nameof(gameId));
        HomeScore = homeScore;
        AwayScore = awayScore;
        Outcome = outcome;
    }

    public string GameId { get; }
    public int HomeScore { get; }
    public int AwayScore { get; }
    public BetRowOutcome Outcome { get; }
}

A Bet contains a collection of BetRows and also some other data like Id and UserId. A BetRow also holds a BetRowOutcome property. This property will be updated when a game is finished.

public enum BetRowOutcome
{
    Unsettled,
    Win,
    Loss
}

Now we want to check if the user has won or not. We need to check and update the Outcome property for each BetRow.

Without records

I will now show you one of the "pain points" when it comes to immutability and c#. If we weren't using immutability, the following code would work. We just loop through the bet rows and update (mutate) the Outcome property.
BetService

public Bet SettleBet(Dictionary<string, Game> finishedGames, Bet bet)
{
    foreach (var betRow in bet.Rows)
    {
        var result = finishedGames[betRow.GameId].Result;

        if (betRow.HomeScore == result.HomeScore && betRow.AwayScore == result.AwayScore)
        {
            betRow.Outcome = BetRowOutcome.Win;
        }
        else
        {
            betRow.Outcome = BetRowOutcome.Loss;
        }
    }

    return bet;
}

Since the above code doesn't compile, we need to add a bit more code to make it work.

public class BetService
{
    public Bet SettleBet(Dictionary<string, Game> finishedGames, Bet bet)
    {
        var updatedBetRows = new List<BetRow>();
        foreach (var betRow in bet.Rows)
        {
            var result = finishedGames[betRow.GameId].Result;

            BetRowOutcome betRowOutcome;
            if (betRow.HomeScore == result.HomeScore && betRow.AwayScore == result.AwayScore)
            {
                betRowOutcome = BetRowOutcome.Win;
            }
            else
            {
                betRowOutcome = BetRowOutcome.Loss;
            }

            var updatedBetRow = new BetRow(betRow.GameId, betRow.HomeScore, betRow.AwayScore, betRowOutcome);
            updatedBetRows.Add(updatedBetRow);
        }

        return new Bet(bet.Id, bet.UserId, updatedBetRows);
    }
}
  1. We added a list that we use to keep track of the updated BetRows.
  2. Foreach row we create a new BetRow object and add it to the list.
  3. We return a new Bet with the updated rows.

This works great but imagine that your object has a bunch of different properties, you could easily end up with something like this:

return new Bet(
  bet.Id,
  bet.UserId,
  updatedBetRows,
  more,
  properties,
  here,
  updateThisAsWell,
  easy,
  to,
  forget,
  and,
  also,
  make,
  errors)

Hopefully you get my point. :)
Basically we want to avoid passing all the existing parameters to the constructor.

With records

I have now converted the following classes to records.

public record Bet
{
    public Bet(string id, string userId, IReadOnlyCollection<Record.BetRow> rows)
    {
        Id = id ?? throw new ArgumentNullException(nameof(id));
        UserId = userId ?? throw new ArgumentNullException(nameof(userId));
        Rows = rows ?? throw new ArgumentNullException(nameof(rows));
    }

    public string Id { get; }
    public string UserId { get; }
    public IReadOnlyCollection<Record.BetRow> Rows { get; init; }
}
public record BetRow
{
    public BetRow(string gameId, int homeScore, int awayScore) : this(gameId, homeScore, awayScore, BetRowOutcome.Unsettled)
    {
    }

    public BetRow(string gameId, int homeScore, int awayScore, BetRowOutcome outcome)
    {
        GameId = gameId ?? throw new ArgumentNullException(nameof(gameId));
        HomeScore = homeScore;
        AwayScore = awayScore;
        Outcome = outcome;
    }

    public string GameId { get; }
    public int HomeScore { get; }
    public int AwayScore { get; }
    public BetRowOutcome Outcome { get; init; }
}

The key thing here is that I changed class to record and added the init keyword to the Rows and Outcome properties. This allows me to do the following:
BetService

public Bet SettleBet(Dictionary<string, Game> finishedGames, Bet bet)
{
    var updatedBetRows = new List<Record.BetRow>();
    foreach (var betRow in bet.Rows)
    {
        var result = finishedGames[betRow.GameId].Result;

       BetRowOutcome betRowOutcome;
       if (betRow.HomeScore == result.HomeScore && betRow.AwayScore == result.AwayScore)
       {
           betRowOutcome = BetRowOutcome.Win;
       }
       else
       {
           betRowOutcome = BetRowOutcome.Loss;
       }

       var updatedBetRow = betRow with { Outcome = betRowOutcome }; // No more new
       updatedBetRows.Add(updatedBetRow);
    }
    return bet with { Rows = updatedBetRows }; // No more new
}

Now I can use the with keyword and only specify the properties that I would like to update. I will not mutate the existing Bet object, I will get a completely new object with updated Rows, all other properties will keep their old values.

This was just a quick example of how to use records, if you want to read more about records have a look here

New major version of JOS.ContentSerializer - Version 5.0

$
0
0

I finally had some time to fix a long standing bug in JOS.ContentSerializer.

The bug

If you passed in a custom IContentSerializerSettings to the Serialize or GetStructuredData method on IContentSerializer, it was simply ignored... :)

The default implementation of IPropertyManager did not pass on the IContentSerializerSettings to the IPropertyHandlers. Instead, the default settings registered in DI was used.

PropertyManager.cs

public Dictionary<string, object> GetStructuredData(
    IContentData contentData,
    IContentSerializerSettings settings) // As you can see, settings is not used at all...
{
    var properties = this._propertyResolver.GetProperties(contentData);
    var structuredData = new Dictionary<string, object>();

    foreach (var property in properties)
    {
        var propertyHandler = this._propertyHandlerService.GetPropertyHandler(property);
        if (propertyHandler == null)
        {
            Trace.WriteLine($"No PropertyHandler was found for type '{property.PropertyType}'");
            continue;
        }

        var method = propertyHandler.GetType().GetMethod(nameof(IPropertyHandler<object>.Handle));
        if (method != null)
        {
            var key = this._propertyNameStrategy.GetPropertyName(property);
            var value = property.GetValue(contentData);
            var result = method.Invoke(propertyHandler, new[] { value, property, contentData });
            structuredData.Add(key, result);
        }
    }
    return structuredData;
}

That's not weird though, because the Handle method didn't have a IContentSerializerSetting parameter.
IPropertyHandler<>

public interface IPropertyHandler<in T>
{
    object Handle(T value, PropertyInfo property, IContentData contentData);
}

The fix

IPropertyHandler<>

public interface IPropertyHandler<in T>
{
    object Handle(T value, PropertyInfo property, IContentData contentData);
    object Handle(T value, PropertyInfo property, IContentData contentData, IContentSerializerSettings settings);
}

PropertyManager.cs

public Dictionary<string, object> GetStructuredData(
    IContentData contentData,
    IContentSerializerSettings settings)
{
    var properties = this._propertyResolver.GetProperties(contentData);
    var structuredData = new Dictionary<string, object>();

    foreach (var property in properties)
    {
        var propertyHandler = this._propertyHandlerService.GetPropertyHandler(property);
        if (propertyHandler == null)
        {
            Trace.WriteLine($"No PropertyHandler was found for type '{property.PropertyType}'");
            continue;
        }

        // I've now refactored this so that I cache the methods as well
        var method = GetMethodInfo(propertyHandler);
        var key = this._propertyNameStrategy.GetPropertyName(property);
        var value = property.GetValue(contentData);
        // We are now using the new overload on IPropertyHandler that takes a IContentSerializerSettings
        var result = method.Invoke(propertyHandler, new[] { value, property, contentData, settings });
        structuredData.Add(key, result);
    }
    return structuredData;
}

private static MethodInfo GetMethodInfo(object propertyHandler)
{
    var type = propertyHandler.GetType();
    if (CachedHandleMethodInfos.ContainsKey(type))
    {
        CachedHandleMethodInfos.TryGetValue(type, out var cachedMethod);
        return cachedMethod;
    }

    var method = propertyHandler.GetType().GetMethods()
        .Where(x => x.Name.Equals(nameof(IPropertyHandler<object>.Handle)))
        .OrderByDescending(x => x.GetParameters().Length)
        .First();

    CachedHandleMethodInfos.TryAdd(type, method);
    return method;
}

The breaking change

Since I've introduced a new method on the IPropertyHandler<> interface this is a breaking change. If you have any custom property handlers you will need to implement the new method as well. Also, remember that the default implementation of IPropertyManager will only call the new method.

Here's an example of how I fixed the default implementation for the Url property.

Before

public class UrlPropertyHandler : IPropertyHandler<Url>
{
    private readonly IUrlHelper _urlHelper;
    private readonly IContentSerializerSettings _contentSerializerSettings;
    private const string MailTo = "mailto";

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

    public object Handle(Url url, PropertyInfo propertyInfo, IContentData contentData)
    {
        if (url == null)
        {
            return null;
        }

        if (url.Scheme == MailTo) return url.OriginalString;

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

            return url.PathAndQuery;
        }

        return this._urlHelper.ContentUrl(url, this._contentSerializerSettings.UrlSettings);
    }
}

After
As you can see, I'm just calling the new method from the old one and passing along the IContentSerializerSettings from DI to the new method.

public class UrlPropertyHandler : IPropertyHandler<Url>
{
    private readonly IUrlHelper _urlHelper;
    private readonly IContentSerializerSettings _contentSerializerSettings;
    private const string MailTo = "mailto";

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

    public object Handle(
        Url url,
        PropertyInfo propertyInfo,
        IContentData contentData)
    {
        return Handle(url, propertyInfo, contentData, _contentSerializerSettings);
    }

    public object Handle(
        Url url,
        PropertyInfo property,
        IContentData contentData,
        IContentSerializerSettings settings)
    {
        if (url == null)
        {
            return null;
        }

        if (url.Scheme == MailTo) return url.OriginalString;

        if (url.IsAbsoluteUri)
        {
            if (settings.UrlSettings.UseAbsoluteUrls)
            {
                return url.OriginalString;
            }

            return url.PathAndQuery;
        }

        return this._urlHelper.ContentUrl(url, settings.UrlSettings);
    }
}

How C# Records will change my life

$
0
0
How C# Records will change my life

Photo by Eric Krull

Introduction

Whenever I code I prefer to have immutable domain models.
Example:

public class Game
{
    public Game(Team homeTeam, Team awayTeam, GameResult result)
    {
        HomeTeam = homeTeam ?? throw new ArgumentNullException(nameof(homeTeam));
        AwayTeam = awayTeam ?? throw new ArgumentNullException(nameof(awayTeam));
        Result = result ?? throw new ArgumentNullException(nameof(result));
    }

    public Team HomeTeam { get; }
    public Team AwayTeam { get; }
    public GameResult Result { get; }
}

Here you can see that it's only possible to set the properties when creating the object (get only properties).

By doing this you can make sure that you don't accidentally mutate the state of the object. This would not compile for example:

...
var game = new Game(homeTeam, awayTeam, result);
game.AwayTeam = new Team(Guid.NewGuid(), "Arsenal"); // Compilation error

Immutability comes with a lot of benefits, but sometimes it can be a bit cumbersome to deal with when you only want to update some properties. Since the object is immutable, you need to create a copy with all the existing values and the new updated one.

I will show you how Records in C# 9 will greatly simplify this

Let's set the scene

Let's say that we are building an application related to betting. A user can place bets on different games.

Our current domain model (before records) looks something like this (greatly simplified):

public class Bet
{
    public Bet(string id, string userId, IReadOnlyCollection<BetRow> rows)
    {
        Id = id ?? throw new ArgumentNullException(nameof(id));
        UserId = userId ?? throw new ArgumentNullException(nameof(userId));
        Rows = rows ?? throw new ArgumentNullException(nameof(rows));
    }

    public string Id { get; }
    public string UserId {get; }
    public IReadOnlyCollection<BetRow> Rows { get; }
}

public class BetRow
{
    public BetRow(string gameId, int homeScore, int awayScore) : this(gameId, homeScore, awayScore, BetRowOutcome.Unsettled)
    {
    }

    public BetRow(string gameId, int homeScore, int awayScore, BetRowOutcome outcome)
    {
        GameId = gameId ?? throw new ArgumentNullException(nameof(gameId));
        HomeScore = homeScore;
        AwayScore = awayScore;
        Outcome = outcome;
    }

    public string GameId { get; }
    public int HomeScore { get; }
    public int AwayScore { get; }
    public BetRowOutcome Outcome { get; }
}

A Bet contains a collection of BetRows and also some other data like Id and UserId. A BetRow also holds a BetRowOutcome property. This property will be updated when a game is finished.

public enum BetRowOutcome
{
    Unsettled,
    Win,
    Loss
}

Now we want to check if the user has won or not. We need to check and update the Outcome property for each BetRow.

Without records

I will now show you one of the "pain points" when it comes to immutability and c#. If we weren't using immutability, the following code would work. We just loop through the bet rows and update (mutate) the Outcome property.
BetService

public Bet SettleBet(Dictionary<string, Game> finishedGames, Bet bet)
{
    foreach (var betRow in bet.Rows)
    {
        var result = finishedGames[betRow.GameId].Result;

        if (betRow.HomeScore == result.HomeScore && betRow.AwayScore == result.AwayScore)
        {
            betRow.Outcome = BetRowOutcome.Win;
        }
        else
        {
            betRow.Outcome = BetRowOutcome.Loss;
        }
    }

    return bet;
}

Since the above code doesn't compile, we need to add a bit more code to make it work.

public class BetService
{
    public Bet SettleBet(Dictionary<string, Game> finishedGames, Bet bet)
    {
        var updatedBetRows = new List<BetRow>();
        foreach (var betRow in bet.Rows)
        {
            var result = finishedGames[betRow.GameId].Result;

            BetRowOutcome betRowOutcome;
            if (betRow.HomeScore == result.HomeScore && betRow.AwayScore == result.AwayScore)
            {
                betRowOutcome = BetRowOutcome.Win;
            }
            else
            {
                betRowOutcome = BetRowOutcome.Loss;
            }

            var updatedBetRow = new BetRow(betRow.GameId, betRow.HomeScore, betRow.AwayScore, betRowOutcome);
            updatedBetRows.Add(updatedBetRow);
        }

        return new Bet(bet.Id, bet.UserId, updatedBetRows);
    }
}
  1. We added a list that we use to keep track of the updated BetRows.
  2. Foreach row we create a new BetRow object and add it to the list.
  3. We return a new Bet with the updated rows.

This works great but imagine that your object has a bunch of different properties, you could easily end up with something like this:

return new Bet(
  bet.Id,
  bet.UserId,
  updatedBetRows,
  more,
  properties,
  here,
  updateThisAsWell,
  easy,
  to,
  forget,
  and,
  also,
  make,
  errors)

Hopefully you get my point. :)
Basically we want to avoid passing all the existing parameters to the constructor.

With records

I have now converted the following classes to records.

public record Bet
{
    public Bet(string id, string userId, IReadOnlyCollection<Record.BetRow> rows)
    {
        Id = id ?? throw new ArgumentNullException(nameof(id));
        UserId = userId ?? throw new ArgumentNullException(nameof(userId));
        Rows = rows ?? throw new ArgumentNullException(nameof(rows));
    }

    public string Id { get; }
    public string UserId { get; }
    public IReadOnlyCollection<Record.BetRow> Rows { get; init; }
}
public record BetRow
{
    public BetRow(string gameId, int homeScore, int awayScore) : this(gameId, homeScore, awayScore, BetRowOutcome.Unsettled)
    {
    }

    public BetRow(string gameId, int homeScore, int awayScore, BetRowOutcome outcome)
    {
        GameId = gameId ?? throw new ArgumentNullException(nameof(gameId));
        HomeScore = homeScore;
        AwayScore = awayScore;
        Outcome = outcome;
    }

    public string GameId { get; }
    public int HomeScore { get; }
    public int AwayScore { get; }
    public BetRowOutcome Outcome { get; init; }
}

The key thing here is that I changed class to record and added the init keyword to the Rows and Outcome properties. This allows me to do the following:
BetService

public Bet SettleBet(Dictionary<string, Game> finishedGames, Bet bet)
{
    var updatedBetRows = new List<Record.BetRow>();
    foreach (var betRow in bet.Rows)
    {
        var result = finishedGames[betRow.GameId].Result;

       BetRowOutcome betRowOutcome;
       if (betRow.HomeScore == result.HomeScore && betRow.AwayScore == result.AwayScore)
       {
           betRowOutcome = BetRowOutcome.Win;
       }
       else
       {
           betRowOutcome = BetRowOutcome.Loss;
       }

       var updatedBetRow = betRow with { Outcome = betRowOutcome }; // No more new
       updatedBetRows.Add(updatedBetRow);
    }
    return bet with { Rows = updatedBetRows }; // No more new
}

Now I can use the with keyword and only specify the properties that I would like to update. I will not mutate the existing Bet object, I will get a completely new object with updated Rows, all other properties will keep their old values.

This was just a quick example of how to use records, if you want to read more about records have a look here

New major version of JOS.ContentSerializer - Version 5.0

$
0
0

I finally had some time to fix a long standing bug in JOS.ContentSerializer.

The bug

If you passed in a custom IContentSerializerSettings to the Serialize or GetStructuredData method on IContentSerializer, it was simply ignored... :)

The default implementation of IPropertyManager did not pass on the IContentSerializerSettings to the IPropertyHandlers. Instead, the default settings registered in DI was used.

PropertyManager.cs

public Dictionary<string, object> GetStructuredData(
    IContentData contentData,
    IContentSerializerSettings settings) // As you can see, settings is not used at all...
{
    var properties = this._propertyResolver.GetProperties(contentData);
    var structuredData = new Dictionary<string, object>();

    foreach (var property in properties)
    {
        var propertyHandler = this._propertyHandlerService.GetPropertyHandler(property);
        if (propertyHandler == null)
        {
            Trace.WriteLine($"No PropertyHandler was found for type '{property.PropertyType}'");
            continue;
        }

        var method = propertyHandler.GetType().GetMethod(nameof(IPropertyHandler<object>.Handle));
        if (method != null)
        {
            var key = this._propertyNameStrategy.GetPropertyName(property);
            var value = property.GetValue(contentData);
            var result = method.Invoke(propertyHandler, new[] { value, property, contentData });
            structuredData.Add(key, result);
        }
    }
    return structuredData;
}

That's not weird though, because the Handle method didn't have a IContentSerializerSetting parameter.
IPropertyHandler<>

public interface IPropertyHandler<in T>
{
    object Handle(T value, PropertyInfo property, IContentData contentData);
}

The fix

IPropertyHandler<>

public interface IPropertyHandler<in T>
{
    object Handle(T value, PropertyInfo property, IContentData contentData);
    object Handle(T value, PropertyInfo property, IContentData contentData, IContentSerializerSettings settings);
}

PropertyManager.cs

public Dictionary<string, object> GetStructuredData(
    IContentData contentData,
    IContentSerializerSettings settings)
{
    var properties = this._propertyResolver.GetProperties(contentData);
    var structuredData = new Dictionary<string, object>();

    foreach (var property in properties)
    {
        var propertyHandler = this._propertyHandlerService.GetPropertyHandler(property);
        if (propertyHandler == null)
        {
            Trace.WriteLine($"No PropertyHandler was found for type '{property.PropertyType}'");
            continue;
        }

        // I've now refactored this so that I cache the methods as well
        var method = GetMethodInfo(propertyHandler);
        var key = this._propertyNameStrategy.GetPropertyName(property);
        var value = property.GetValue(contentData);
        // We are now using the new overload on IPropertyHandler that takes a IContentSerializerSettings
        var result = method.Invoke(propertyHandler, new[] { value, property, contentData, settings });
        structuredData.Add(key, result);
    }
    return structuredData;
}

private static MethodInfo GetMethodInfo(object propertyHandler)
{
    var type = propertyHandler.GetType();
    if (CachedHandleMethodInfos.ContainsKey(type))
    {
        CachedHandleMethodInfos.TryGetValue(type, out var cachedMethod);
        return cachedMethod;
    }

    var method = propertyHandler.GetType().GetMethods()
        .Where(x => x.Name.Equals(nameof(IPropertyHandler<object>.Handle)))
        .OrderByDescending(x => x.GetParameters().Length)
        .First();

    CachedHandleMethodInfos.TryAdd(type, method);
    return method;
}

The breaking change

Since I've introduced a new method on the IPropertyHandler<> interface this is a breaking change. If you have any custom property handlers you will need to implement the new method as well. Also, remember that the default implementation of IPropertyManager will only call the new method.

Here's an example of how I fixed the default implementation for the Url property.

Before

public class UrlPropertyHandler : IPropertyHandler<Url>
{
    private readonly IUrlHelper _urlHelper;
    private readonly IContentSerializerSettings _contentSerializerSettings;
    private const string MailTo = "mailto";

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

    public object Handle(Url url, PropertyInfo propertyInfo, IContentData contentData)
    {
        if (url == null)
        {
            return null;
        }

        if (url.Scheme == MailTo) return url.OriginalString;

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

            return url.PathAndQuery;
        }

        return this._urlHelper.ContentUrl(url, this._contentSerializerSettings.UrlSettings);
    }
}

After
As you can see, I'm just calling the new method from the old one and passing along the IContentSerializerSettings from DI to the new method.

public class UrlPropertyHandler : IPropertyHandler<Url>
{
    private readonly IUrlHelper _urlHelper;
    private readonly IContentSerializerSettings _contentSerializerSettings;
    private const string MailTo = "mailto";

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

    public object Handle(
        Url url,
        PropertyInfo propertyInfo,
        IContentData contentData)
    {
        return Handle(url, propertyInfo, contentData, _contentSerializerSettings);
    }

    public object Handle(
        Url url,
        PropertyInfo property,
        IContentData contentData,
        IContentSerializerSettings settings)
    {
        if (url == null)
        {
            return null;
        }

        if (url.Scheme == MailTo) return url.OriginalString;

        if (url.IsAbsoluteUri)
        {
            if (settings.UrlSettings.UseAbsoluteUrls)
            {
                return url.OriginalString;
            }

            return url.PathAndQuery;
        }

        return this._urlHelper.ContentUrl(url, settings.UrlSettings);
    }
}
Viewing all 244 articles
Browse latest View live