Category Archives: C#

The Art of The Bodge

As tech folk, we seem to obsess over best practices, beautiful & maintainable solutions, and using the newest technologies in smart and elegant ways.

But what of the humble “JFDI” bodge? When everything is on the line – when something has failed in production, critical functionality isn’t working and we can’t rollback, or we need to urgently build a tool or run some hacky solution to resolve some regulatory data issue fast – I will say it, I love a good bodge job.

There is something about the context – the high stakes, the pressure, the business importance, complications and genuine impact of it all – of scrambling to chuck together a dirty console app written with hopes and dreams, or getting out the virtual duct tape in some other way, and ultimately saving the day. It’s exciting! And often the most creative solutions come from these moments, where requirements don’t exist aside from “fix it”. I’m unashamed to say it – dirty bodge jobs are unabashedly my favourite part of being a software developer. These are some of the situations that make you feel alive and like you’re using some creative ingenuity to find the best (not “ideal”, but quickest and most effective for the issue) resolution, and having some real impact, rather than just churning out code in sprint after sprint monotony. 

I think I might be in the minority here – but I also think my skills lie, generally, not like many developers who I admire (i.e. I don’t consider myself a super skilled writer of code), but more in being able to find the balance between what is the “best” engineering solution vs what is the best solution for the context

And ultimately, I find I get more satisfaction, these days, from something I did having a good real world impact, rather than the work itself being a satisfying piece of engineering. I’m not knocking the positives of those who prefer the latter, by any stretch of the imagination – it’s just not what personally gets me out of bed in the morning. 

Yes, with perfect solutions and perfect code, we wouldn’t need them, and businesses would be better off without these stressful, scary situations – but while that is realistically unattainable and not all factors and edge cases can ever be fully considered… I am pretty happy to get to do a bodge job!

An easy way to drastically improve JsonSerializer performance (System.Text.Json)

A .NET 6 project that I’ve been contributing to uses the new(ish) System.Text.Json JsonSerializer to serialise/deserialise data prior to storing to, and after retrieving from, a distributed cache.

I started to notice some performance degradation as I increased our use of caching in this application (and therefore more JSON serialising/deserialising going on), so set to work on investigating why this was the case.

To note – we had been retrieving the JsonSerializerOptions object from a static method rather than using a static variable, an approach even used by some tutorials, e.g:

    private static JsonSerializerOptions GetJsonSerializerOptions()
    {
        return new JsonSerializerOptions
        {
            PropertyNamingPolicy = null,
            WriteIndented = true,
            AllowTrailingCommas = true,
            DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
        };
    } 

And one of our usages of this in the cache context (we were also deserialising in the same way when getting from the cache):

 private static Task SetAsync<T>(this IDistributedCache cache, string key, T value, DistributedCacheEntryOptions options)
    {
        var bytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(value, GetJsonSerializerOptions()));
        return cache.SetAsync(key, bytes, options);
    }

Testing – Huge Performance Difference

I’ve written a couple of benchmark tests comparing the use of a “GetJsonSerializerOptions” method to return an JsonSerializerOptions instance on de/serialise vs using a static variable. The code and data used for both tests is identical, aside from the approach with the options instance – it loops through and both serialises and deserialises an object 10,000 times, and logs the time taken and memory allocated:

The test – 10k serialises + deserialises of a dummy “CustomData” object:

Static Method: 45,850.30ms mean time, 1397.81MB Memory Allocated

Static variable: 62.42ms mean time, 32.65MB Memory Allocated

Yep – over this 10k loop, the static method approach allocated over 1.3GB more memory, and took roughly 734.5x more time, vs using a static variable…! I then double checked the same approach with dotTrace – bear in mind there is a margin for error and deviation…


~44.7seconds for the Static Method, 80ms for the Static Variable.

At the bottom of the post is a gist to show that both tests are identical (10k loops of serialising and deserialising the same data, with the only difference being the JsonSerializerOptions).

Why?

This behaviour is due to the way the JsonSerializer fundamentally works. When serialising/deserialising an object, the JsonSerializer has to perform a “warm-up phase“, which involves generating and caching metadata related to how to de/serialise the type. When the JsonSerializerOptions are re-used, it can simply use the same metadata that it previously cached for the next time an object of that type is processed – however, with a method returning a new JsonSerializerOptions instance every time, it cannot use the cached de/serialising metadata for a type that has already been processed, and has to generate this anew every single time it serialises or deserialises any data. This is an expensive process, and results in much slower execution + a huge amount more memory being allocated. If part of your system relies on JsonSerializer before storing or after retrieving data – this could result in slow performance, and potentially memory leaks and timeouts.

Solution

The change is very simple. Replace (and change any references to) any method/anywhere inline where you might be creating a new JsonSerializerOptions object repeatedly, with a static variable, such as:

private static JsonSerializerOptions _serializerOptions = new JsonSerializerOptions
    {
        PropertyNamingPolicy = null,
        WriteIndented = true,
        AllowTrailingCommas = true,
        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
    };

Gist