<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/scripts/pretty-feed-v3.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:h="http://www.w3.org/TR/html4/"><channel><title>KodeNinja</title><description>Software developer - KodeNinja</description><link>https://kodeninja.dev</link><item><title>2025 Wrapped</title><link>https://kodeninja.dev/blog/2025-wrapped</link><guid isPermaLink="true">https://kodeninja.dev/blog/2025-wrapped</guid><description>When life gives you lemons, make lemonade</description><pubDate>Mon, 15 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;This one is not a technical post, but a personal one. As 2025 comes to a close, I wanted to take a moment to look back and reflect on a year that turned out to be far more eventful than I ever expected. It has been a true rollercoaster – a mix of stability, sudden setbacks, and ultimately, new beginnings. I went from feeling fully in control, to losing my job overnight, to landing a role I had only dreamed about.&lt;/p&gt;
&lt;p&gt;So let’s dive in.&lt;/p&gt;
&lt;h2&gt;The year in retrospect&lt;/h2&gt;
&lt;p&gt;Professionally, 2025 started strong, with meaningful progress, technical growth, and new responsibilities.&lt;/p&gt;
&lt;p&gt;Since joining Novo Nordisk, I have been part of the Digital Checklist team. Originally built to support a single business area, we realized the product had potential far beyond its initial scope. Early in the year, we were given an opportunity to introduce the product into another large business area — provided we could deliver a set of key features. The challenge was to evolve the product significantly without disrupting the existing user base that relied on it daily.&lt;/p&gt;
&lt;p&gt;We used this opportunity to modernize our technical foundation. Our goal was to make the application easier to develop, scale, and maintain in the long term. We moved away from Azure Function Apps and adopted a vertical slice architecture using ASP.NET Core hosted on Azure App Services. We also introduced Azure Cosmos DB, which turned out to be a strong fit for our use case and resulted in significant cost savings compared to our previous PostgreSQL setup.&lt;/p&gt;
&lt;p&gt;We were already using Azure Web PubSub for the collaborative / real-time aspects of the application, but due to limitations around message ordering and delivery guarantees, it was initially used only as a trigger mechanism. This led to frequent HTTP calls and unnecessary latency. When Azure introduced the reliable sub-protocol, we were able to push state updates directly to the client. This change significantly improved performance and reduced load on the backend API, resulting in a noticeably faster user experience.&lt;/p&gt;
&lt;p&gt;One of the highlights of the year was publishing a new NuGet package through Novo Nordisk’s open-source GitHub. We needed a way to load reference data from a JSON file stored in Azure Blob Storage and keep it up to date at runtime. I ended up building an extension to the .NET Configuration API to solve this. I wrote a &lt;a href=&quot;https://kodeninja.dev/blog/reloadable-configuration-provider/&quot;&gt;dedicated blog post&lt;/a&gt; about the implementation, which goes into more detail.&lt;/p&gt;
&lt;p&gt;Within the team, there was a temporary imbalance between backend and frontend capacity. Although my background is primarily backend-focused, I had prior experience with frontend development in Angular. I volunteered to step in and help rebalance the workload by working full-stack. This meant learning Vue.js, but with prior experience in TypeScript, Angular, and some React, the transition was relatively smooth.&lt;/p&gt;
&lt;p&gt;At the time, we were integrating the checklist editor directly into the application and rewriting it in Vue. To keep the editor responsive, I proposed using optimistic updates — updating the client state immediately rather than waiting for a backend response, with rollback logic in case of errors. I built a proof of concept that was accepted into the codebase and went on to influence how we designed and implemented the editor as a whole.&lt;/p&gt;
&lt;p&gt;In parallel, a new Novo Nordisk initiative was launched: AiNOMALY. The project began as a proof of concept focused on using computer vision models on production lines. As the concept proved its value and the project matured, I was given the opportunity to lead the software development effort. At that point, it became a matter of coordinating the handover of my responsibilities in Digital Checklist and planning the transition to the new project.&lt;/p&gt;
&lt;p&gt;Outside of work, I was also in the process of buying a newly built, larger apartment. I had been in discussions with both the real estate agent and the bank, and it seemed like we were close to reaching an agreement. Everything was moving in the right direction — until it suddenly wasn’t.&lt;/p&gt;
&lt;h2&gt;The transformation&lt;/h2&gt;
&lt;p&gt;As some people might know, Novo Nordisk has been going through a &quot;transformation&quot;. The CEO was replaced, and the new CEO announced company-wide layoffs: 9000 people globally - 5000 in Denmark. An announcement of that scale inevitably affects day-to-day work and morale. Even though I felt quite safe, the wait was long. Our team supported the production shop floor, hence we did not expect to be affected – but a lot can happen in such a transformation.&lt;/p&gt;
&lt;p&gt;At the end of September, the business side of our department went through the process, which brought both good and bad news. The good news was that Digital Checklist was unaffected, and the business committed to the product. The bad news was that my new team (AiNOMALY) was shut down, and the team members were let go.&lt;/p&gt;
&lt;p&gt;On one hand, I was sad to see good colleagues let go, and my planned next career step was put on hold. On the other hand, I remained assigned to the Digital Checklist team, and as the business side committed to the project, our jobs felt safe.&lt;/p&gt;
&lt;p&gt;The week after it was our turn. On Monday morning, managers would learn their fate. On Tuesday, it was our turn - if you received an email on Tuesday morning (between 6:30-8:00) to get into the office, you were being laid off. On Monday, we were called into a meeting where our manager told us that he — along with most of the department’s managers — had been laid off. At this point, reality hit: Even if we were to stay, it would be a very different workplace going forward.&lt;/p&gt;
&lt;p&gt;I did not want to spend Tuesday morning staring at my inbox, so I chose to sleep in and check my email at 08:00. The message was there — I was being laid off. Based on the Teams chat, most of the IT side was let go; only a few remained.&lt;/p&gt;
&lt;p&gt;My first feeling was relief. Months of waiting were over. I found comfort in the realization that the company I joined was no longer the company I was leaving.&lt;/p&gt;
&lt;p&gt;Those of us affected were asked to come into the office after lunch to receive the formal notice. We were gathered in a room, and the head of department would read us the statement about being let go. The reason: Novo Nordisk did not want internally developed software anymore, and would focus on off-the-shelf software and external suppliers.&lt;/p&gt;
&lt;p&gt;After getting the formal notice, we went down to the reception, to be greeted (and supported) by our colleagues and ex-managers – and then it was time for &quot;gravøl&quot; (directly translated: &quot;Grave beer&quot;, but closest translation would probably be &quot;Wake&quot;). This is where I truly felt the support and unity of the community we built, and I am proud to have been part of it.&lt;/p&gt;
&lt;p&gt;What will happen to those still at the company is uncertain. But for the rest of us, a lot of talent was put on the job market.&lt;/p&gt;
&lt;h2&gt;The job hunt&lt;/h2&gt;
&lt;p&gt;Fortunately, we received decent severance packages, giving us options. However, I am not someone who is good at sitting still. Every instinct told me to immediately get back out there and find a new job (and some of my former colleagues did just that). But on the other hand, finding the right long-term fit was vital. I tried to keep calm and use the time to figure out exactly what I wanted to do next.&lt;/p&gt;
&lt;p&gt;It sounds easy to take a step back and not rush it — but it rarely is. Especially when I started out by applying at some of my dream companies, and got some rejections. Doubt started to creep in, and I started stressing over the deadline for when my severance package would run out (even though it was quite far in the future). This was when I started to apply to more jobs - also jobs that might not be my &quot;ideal&quot; job.&lt;/p&gt;
&lt;p&gt;This landed me a decent number of interviews, and it is comforting seeing that even though the job market is not necessarily something you &lt;em&gt;want&lt;/em&gt; to be a part of, there are jobs out there to be had. But the talent pool in the job market is also quite big right now, as not only Novo Nordisk had layoffs, but also other big companies in Denmark. So even though there are jobs out there, the coveted jobs receive a lot of applications.&lt;/p&gt;
&lt;h3&gt;The rant&lt;/h3&gt;
&lt;p&gt;At this point in the story, I want to reflect on some of the challenges I encountered while job hunting. I fully understand and acknowledge that hiring is difficult and time-consuming, but I do feel that something has shifted in the culture. In some cases, the hiring process no longer seems to respect the candidate&apos;s time and effort. That matters, because the hiring process is not just about selecting a candidate – it is a window into a company, and it is often a candidate&apos;s first real interaction with a company. First impressions count.&lt;/p&gt;
&lt;p&gt;One of the clearest examples of this shift is ghosting. Unfortunately, it has become increasingly common – not just in dating, but in hiring as well. I understand that companies receive large volumes of applications (especially in times of mass layoffs), but at a minimum, there should be an automated rejection when the process moves forward with other candidates. Silence leaves applicants guessing and creates unnecessary frustration.&lt;/p&gt;
&lt;p&gt;This becomes even more problematic once a candidate has invested real time. If someone has been through one or more rounds of interviews, they deserve closure. Ideally, that would be a short phone call or a personal email with feedback, but at the very least, a clear rejection. Ghosting at that stage feels dismissive, and it makes it far less likely that a candidate will apply again in the future.&lt;/p&gt;
&lt;p&gt;In one case, I reached the final round at a company (which I will not name). I had spent multiple hours in interviews, personality assessments, and logical tests — only to never hear back. Ironically, a former colleague of mine eventually got the role. I was not frustrated about missing out on the role; the right candidate was selected. What disappointed me was the lack of closure after such a lengthy process. Experiences like that inevitably shape how candidates perceive a company going forward.&lt;/p&gt;
&lt;p&gt;Clear communication is another area where the process often breaks down. Timelines matter. I experienced several multi-week waiting periods after interviews, often without any warning. If a company is upfront — for example, explaining that they need to finish first-round interviews before responding — I am perfectly fine waiting. But without that context, silence is indistinguishable from rejection, and mentally, candidates will move on.&lt;/p&gt;
&lt;p&gt;Then there is feedback that is difficult to act on. When I am rejected, I genuinely want to learn something from it. Being told I am not the right cultural fit is fair. Being told another candidate had deeper experience in certain areas is useful. But being told that I am competent, a strong match for the role, and likely to succeed — yet “too young” — leaves me unsure how to respond. Especially when my age was known from the very beginning.&lt;/p&gt;
&lt;p&gt;For me, interviews are a two-way street. It is not only about whether a company wants me, but also whether I want to work there. I understand the use of external recruiters for initial screening, but when a full first-round interview is conducted by someone who cannot answer basic questions about the company, the process starts to feel inefficient. In one case, I was promised follow-up answers that never came — until the company reached out three weeks later to invite me to a second-round interview.&lt;/p&gt;
&lt;h3&gt;The good process&lt;/h3&gt;
&lt;p&gt;Being on the candidate side this year clarified what I personally value in a hiring process. These priorities will naturally differ from person to person, but after going through multiple processes, certain patterns became very clear to me.&lt;/p&gt;
&lt;p&gt;Above all, I value clear and consistent communication. I want to understand what I am getting into: what the process looks like, when I can expect to hear back, and what is expected of me at each step. Communication goes both ways. As a candidate, I want to learn about the company, the role, and the team — and I want space to ask questions and get meaningful answers.&lt;/p&gt;
&lt;p&gt;Some of the best processes I experienced had a dedicated point of contact throughout the journey. Having someone who proactively shared timelines, explained what the next round would involve, and was available for questions made a noticeable difference. It created a sense of structure and trust, even when the process itself took time.&lt;/p&gt;
&lt;p&gt;Every application and interview process is an investment of time. I invest time in preparing applications, attending interviews, completing assessments, and sometimes working on cases. While I am willing to make that investment, it has to be balanced. Even when I was between jobs, I still had other responsibilities — and often other interview processes running in parallel. When a case or assignment is part of the process, clear expectations and reasonable timelines matter. Knowing how much time is expected allows candidates to engage properly and fairly.&lt;/p&gt;
&lt;p&gt;The more time a candidate invests, the more important closure becomes. Not in the sense of entitlement to an offer, but in the form of clear communication. If the outcome is a rejection, I value receiving a response — and when possible, feedback that helps me understand the decision.&lt;/p&gt;
&lt;p&gt;Not all the interview processes I took part in were negative. Many included one or more of the elements above. The process that ultimately led me to my next role checked every box. It felt structured, respectful of my time, and genuinely two-way. Not only did I end up where I wanted – it was a great experience to get there.&lt;/p&gt;
&lt;h2&gt;The new beginning&lt;/h2&gt;
&lt;p&gt;And so we arrive at December. As a personal Christmas gift, I am happy to share that I will be joining &lt;strong&gt;the LEGO Group&lt;/strong&gt; in the new year. Working at the LEGO Group has been a long-standing goal of mine, and something I have applied for multiple times over the years. This time, it finally worked out. I will be joining the &lt;strong&gt;Greenfield Packing Automation&lt;/strong&gt; team, and I am genuinely excited to get started.&lt;/p&gt;
&lt;p&gt;Looking back, it is hard to believe how quickly this year shifted direction — from stability and long-term plans, to uncertainty and job hunting, and ultimately to a new opportunity I did not know was coming. Although I didn&apos;t get that new apartment, the past months have been a reminder that even when things feel out of control, progress is still happening – just not always in the way you expect.&lt;/p&gt;
&lt;p&gt;For now, though, it is time for a Christmas break. A break without applications, interviews, or waiting for emails. Just time to reset, reflect, and enjoy the calm before a new chapter begins. I am heading into 2026 rested, grateful, and ready for new challenges.&lt;/p&gt;
&lt;p&gt;To round it off, I want to give thanks to my old colleagues from Novo Nordisk, for an amazing culture and community; especially my team and my manager. I hope to work with all of you again someday. Another big thanks goes to everybody who supported during this transition.&lt;/p&gt;
&lt;p&gt;Merry Christmas.&lt;/p&gt;</content:encoded><h:img src="/_astro/hero.B3c-CxMK.png"/><enclosure url="/_astro/hero.B3c-CxMK.png"/></item><item><title>A reloadable configuration provider for Azure Blob Storage</title><link>https://kodeninja.dev/blog/reloadable-configuration-provider</link><guid isPermaLink="true">https://kodeninja.dev/blog/reloadable-configuration-provider</guid><description>How to make your own configuration provider for dotnet</description><pubDate>Mon, 23 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Like many others, our applications have some master data. These data are not user-configurable/user-changeable, so we have not found the need to put them into the database. So far, we have put them in JSON files in our solution, and used the dotnet &lt;a href=&quot;https://learn.microsoft.com/en-us/dotnet/core/extensions/configuration&quot;&gt;Configuration API&lt;/a&gt;, and this has worked flawlessly.&lt;/p&gt;
&lt;p&gt;However, every once in a while we have to update some of this master data - and with the configuration packed and deployed with the application, this requires a new deployment to production. We wanted a solution where we would change the master data, without needing to re-deploy the application.&lt;/p&gt;
&lt;p&gt;Since we are hosted in Azure, we looked into &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/azure-app-configuration/overview&quot;&gt;Azure App Configuration&lt;/a&gt;. However, Azure App Configuration has a size limit for each key-value pair of &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/azure-app-configuration/faq#are-there-any-size-limitations-on-keys-and-values-stored-in-app-configuration&quot;&gt;10KB&lt;/a&gt;. This is probably fine in most cases. Still, one of our master data records is a long list of locations (with a size of 57 KB) - and we could not find a good way to split this data up. This ruled out Azure App Configuration.&lt;/p&gt;
&lt;p&gt;Another option is to host the configuration files on blob storage - like &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/storage/blobs/storage-blobs-introduction&quot;&gt;Azure Blob Storage&lt;/a&gt;. However, Microsoft has not provided us with a configuration provider for Azure Blob Storage. Fortunately, the &lt;a href=&quot;https://learn.microsoft.com/en-us/dotnet/core/extensions/custom-configuration-provider&quot;&gt;Configuration API is extensible&lt;/a&gt;. This lead to us creating the &lt;a href=&quot;https://www.nuget.org/packages/NovoNordisk.Configuration.AzureBlob&quot;&gt;NovoNordisk.Configuration.AzureBlob&lt;/a&gt; NuGet package.&lt;/p&gt;
&lt;p&gt;Let&apos;s dive into how to create your own configuration provider.&lt;/p&gt;
&lt;h2&gt;Configuration API&lt;/h2&gt;
&lt;p&gt;You can think of the Configuration API / &lt;code&gt;IConfiguration&lt;/code&gt; as a key-value pair collection. You can load data into it with configuration providers, and then bind configuration values to objects. Dotnet has a bunch of built-in configuration providers to get configuration values from files, environment variables, command-line arguments et cetera. Let&apos;s go through an example using a JSON file.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;Settings&quot;: {
    &quot;SomeString&quot;: &quot;Completely new string&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can then load this JSON file into &lt;code&gt;IConfiguration&lt;/code&gt; using the JSON file configuration provider:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;var builder = Host.CreateApplicationBuilder(args);
builder.Configuration.AddJsonFile(&quot;config.json&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we have the &quot;Settings&quot; key in &lt;code&gt;IConfiguration&lt;/code&gt;, which we can then bind to an object.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;public record Settings
{
    public string? SomeString { get; init; }
}

builder.Services.Configure&amp;#x3C;Settings&gt;(builder.Configuration.GetSection(&quot;Settings&quot;));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With the configuration values bound to the &lt;code&gt;Settings&lt;/code&gt; object, we can now use dependency injection to use the values in our code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;public class SomeService(IOptions&amp;#x3C;Settings&gt; settings)
{
    public string? SomeMethod()
    {
        return settings.Value.SomeString;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;Please note that we use &lt;code&gt;IOptions&amp;#x3C;T&gt;&lt;/code&gt; to inject the configuration objects here. You cannot just inject the &lt;code&gt;Settings&lt;/code&gt; object, as it is only registered using the Options interfaces. &lt;code&gt;IOptions&amp;#x3C;T&gt;&lt;/code&gt; is not the only interface we can use, and we will make use of the others later on.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;For our use case, we want to retrieve a file from Azure Blob Storage, and load the contents into &lt;code&gt;IConfiguration&lt;/code&gt;. We can do that by making our own configuration provider.&lt;/p&gt;
&lt;h2&gt;Making the configuration provider&lt;/h2&gt;
&lt;p&gt;Starting out, we need a configuration source. This will contain the settings needed for our configuration provider, so we will include properties like the URL to the blob, a polling interval et cetera.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;using Azure.Core;
using Microsoft.Extensions.Configuration;

public class BlobJsonConfigurationSource : IConfigurationSource
{
    public required Uri BlobUrl { get; init; }

    public bool ReloadOnChange { get; init; } = false;

    public TimeSpan PollingInterval { get; init; } = TimeSpan.FromSeconds(30);

    public Action&amp;#x3C;Exception&gt;? ExceptionHandler { get; init; }

    public TokenCredential? Credential { get; init; }

    public IConfigurationProvider Build(IConfigurationBuilder builder)
    {
        return new BlobJsonConfigurationProvider(this);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let&apos;s cover the properties included:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;BlobUrl&lt;/code&gt; is the URL for the Azure Blob to load.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ReloadOnChange&lt;/code&gt; is for turning on/off the periodic change detection.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PollingInterval&lt;/code&gt; is how often we should check for changes.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ExceptionHandler&lt;/code&gt; is a callback/action to be called when an exception occurs during a reload.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Credential&lt;/code&gt; is the Azure credentials needed to access the blob, if not public.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When creating a configuration provider, there are several ways to do so. If we are working with JSON data, the easiest path is to inherit from the &lt;code&gt;JsonConfigurationProvider&lt;/code&gt; - this will provide us with a method, where we can hand it a stream of JSON data, and it takes care of the rest. However, this also means that our configuration source needs to inherit from &lt;code&gt;JsonConfigurationSource&lt;/code&gt;, which in turn inherits from &lt;code&gt;FileConfigurationSource&lt;/code&gt;. This includes properties such as a file provider, file path et cetera. Properties we have no use for, as we are not loading from the local file system.&lt;/p&gt;
&lt;p&gt;Another way is to implement the &lt;code&gt;IConfigurationProvider&lt;/code&gt; interface directly.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;public interface IConfigurationProvider
{
  bool TryGet(string key, out string? value);
  void Set(string key, string? value);
  IChangeToken GetReloadToken();
  void Load();
  IEnumerable&amp;#x3C;string&gt; GetChildKeys(IEnumerable&amp;#x3C;string&gt; earlierKeys, string? parentPath);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But there is a middle-ground between these approaches - we can inherit from the &lt;code&gt;ConfigurationProvider&lt;/code&gt; base class. This base class takes care of the core behaviours of configuration providers - like handling change tokens. We just have to fill the &lt;code&gt;Data&lt;/code&gt; property with our configuration data and call the &lt;code&gt;OnReload()&lt;/code&gt; method to signal to the configuration system that the data has changed.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;using Azure.Storage.Blobs;
using Microsoft.Extensions.Configuration;

public class BlobJsonConfigurationProvider(BlobJsonConfigurationSource source) : ConfigurationProvider
{
    public override void Load()
    {
        var client = new BlobClient(source.BlobUrl, source.Credential);
        using var stream = new MemoryStream();
        using var response = client.DownloadTo(stream);
        stream.Seek(0, SeekOrigin.Begin);

        if (!response.IsError)
        {
            Data = /* TODO: Parse JSON */

            OnReload();
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After we download the JSON file from Azure Blob Storage, we have to parse it in order to hand it over to the configuration system. The &lt;code&gt;Data&lt;/code&gt; property is just a dictionary of strings, and while we could do this by hand, there is an easier approach. The &lt;code&gt;JsonConfigurationFileParser&lt;/code&gt; is part of the dotnet runtime. However, it is not public, hence we cannot access it directly. Fortunately for us, the dotnet runtime is open-source, and this file is licensed under the MIT license - so we can just go ahead and &lt;a href=&quot;https://github.com/dotnet/runtime/blob/210a7a9feec64b7fac5147656cddb199ec90cf75/src/libraries/Microsoft.Extensions.Configuration.Json/src/JsonConfigurationFileParser.cs&quot;&gt;grab it&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;...
if (!response.IsError)
{
    Data = JsonConfigurationFileParser.Parse(stream);

    OnReload();
}
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;While we technically have what we need, with the configuration source and configuration provider, we might want to match the existing Configuration API (e.g. &lt;code&gt;builder.AddJsonFile()&lt;/code&gt;). To do this, we can create an extension method to help with registering the configuration source and provider.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;using Azure.Core;
using Microsoft.Extensions.Configuration;

public static class ConfigurationBuilderExtensions
{
    public static IConfigurationBuilder AddJsonBlob(this IConfigurationBuilder builder, Uri blobUrl, bool reloadOnChange = false, TimeSpan? pollingInterval = null, TokenCredential? tokenCredential = null, Action&amp;#x3C;Exception&gt;? exceptionHandler = null)
    {
        return builder.Add(new BlobJsonConfigurationSource
        {
            BlobUrl = blobUrl,
            ReloadOnChange = reloadOnChange,
            PollingInterval = pollingInterval ?? TimeSpan.FromSeconds(30),
            Credential = tokenCredential,
            ExceptionHandler = exceptionHandler
        });
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With this method, we can add configuration files from Azure Blob Storage just like we would any other configuration file.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;builder.Configuration.AddJsonFile(&quot;localfile.json&quot;);
builder.Configuration.AddJsonBlob(new Uri(blobUrl),
    reloadOnChange: true,
    pollingInterval: TimeSpan.FromSeconds(30),
    tokenCredential: new DefaultAzureCredential());
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Reload configuration on change&lt;/h2&gt;
&lt;p&gt;To make our configuration provider reloadable, we can periodically check whether the blob has been updated. One way to do this is to use a &lt;code&gt;System.Threading.Timer&lt;/code&gt; to trigger some code to run at an interval.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;public class BlobJsonConfigurationProvider : ConfigurationProvider, IDisposable, IAsyncDisposable
{
    private readonly BlobJsonConfigurationSource _source;
    private readonly Timer? _timer;
    ...

    public BlobJsonConfigurationProvider(BlobJsonConfigurationSource source)
    {
        _source = source;
        
        if (_source.ReloadOnChange)
        {
            _timer = new Timer(CheckForChanges, null, _source.PollingInterval, _source.PollingInterval); // We cover the CheckForChanges method in a minute
        }
    }

    ...
    
    public void Dispose()
    {
        _timer?.Dispose();
    }

    public async ValueTask DisposeAsync()
    {
        if (_timer != null) await _timer.DisposeAsync();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first parameter passed to the timer is the callback method to run (in this case &lt;code&gt;CheckForChanges()&lt;/code&gt;, which we will cover in a minute). The second parameter is a state object, which we will not use. The third and fourth parameter is the delay before the timer fires for the first time, and at which interval we wish to trigger the callback.&lt;/p&gt;
&lt;p&gt;Now that we have a setup for periodically checking for updates, we have to consider &lt;em&gt;how&lt;/em&gt; we check for changes. We want to avoid downloading the entire file each time, as this could be costly on our Azure bill.&lt;/p&gt;
&lt;p&gt;Azure Blob Storage has ETags (Entity Tags) on our files. An ETag is an identifier for a specific version of a file - this ETag changes every time the file changes. Using the ETag, we can detect if a blob has changed.&lt;/p&gt;
&lt;p&gt;Let&apos;s first update our &lt;code&gt;Load()&lt;/code&gt; method to save the ETag when we download the file.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;public class BlobJsonConfigurationProvider : ConfigurationProvider, IDisposable, IAsyncDisposable
{
    private string? _etag;
    ...
    public override void Load()
    {
        var client = new BlobClient(_source.BlobUrl, _source.Credential);
        using var stream = new MemoryStream();
        using var response = client.DownloadTo(stream);
        stream.Seek(0, SeekOrigin.Begin);

        if (!response.IsError)
        {
            Data = JsonConfigurationFileParser.Parse(stream);
            _etag = response.Headers.ETag.ToString()?.Trim(&apos;&quot;&apos;);
            
            OnReload();
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then we can write our &lt;code&gt;CheckForChanges()&lt;/code&gt; method. We will get the blob properties, and check the ETag. If the ETag does not match, we will trigger the &lt;code&gt;Load()&lt;/code&gt; method.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;private void CheckForChanges(object? state)
{
    try
    {
        var client = new BlobClient(_source.BlobUrl, _source.Credential);
        var props = client.GetProperties();
        if (props is null || !props.HasValue)
        {
            throw new RequestFailedException(&quot;Failed to retrieve blob properties&quot;);
        }

        if (_etag != props.Value.ETag.ToString().Trim(&apos;&quot;&apos;))
        {
            Load();
        }
    }
    catch (Exception e)
    {
        _source.ExceptionHandler?.Invoke(e);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We wrap the code in a try/catch, as the &lt;code&gt;System.Threading.Timer&lt;/code&gt; callback runs on a threadpool thread, and an uncaught exception might cause the timer to stop firing, or even cause the application to crash. In the catch block, we can then invoke the exception handler action from our configuration source, and this can then be used to log or handle errors gracefully.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;You might have wondered why we save the ETag as a string (instead of using the &lt;code&gt;ETag&lt;/code&gt; type) and trim away the double quote. This string manipulation is a workaround for observed inconsistencies in how Azure SDK methods return ETag values. Of the two methods we use (&lt;code&gt;GetProperties()&lt;/code&gt; and &lt;code&gt;DownloadTo()&lt;/code&gt;), one returns a quoted ETag and the other returns an unquoted ETag. This results in the ETag not matching, hence it will load the entire file on every check.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Using the updated configuration&lt;/h2&gt;
&lt;p&gt;Since we were already using &lt;code&gt;IOptions&lt;/code&gt; for our configuration, switching from a local file to an Azure Blob Storage file required minimal changes to our codebase. The primary thing to note is that &lt;code&gt;IOptions&lt;/code&gt; is registered as a singleton, hence it never updates - so even though you enable reloading, the &lt;code&gt;IOptions&lt;/code&gt; instance never gets the updated configuration.&lt;/p&gt;
&lt;p&gt;For getting updated configuration, you have two options (&lt;em&gt;pun intended&lt;/em&gt;): &lt;code&gt;IOptionsSnapshot&lt;/code&gt; and &lt;code&gt;IOptionsMonitor&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;IOptionsSnapshot&lt;/code&gt; provides, as the name implies, a snapshot of the current data. This is useful for scenarios where your configuration should be recomputed on every request. This is registered as Scoped, which means you will not be able to inject it into a class registered as Singleton.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;public class SomeService(IOptionsSnapshot&amp;#x3C;Settings&gt; settings)
{
    public string? SomeMethod()
    {
        return settings.Value.SomeString;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;IOptionsSnapshot&lt;/code&gt; will probably be fine in most use cases, but if you need to inject into a Singleton, or you have some complicated processing of the configuration, that you wish to cache, &lt;code&gt;IOptionsMonitor&lt;/code&gt; might be the solution for you. This is registered as a Singleton, hence you can inject it into any service lifetime. Like &lt;code&gt;IOptionsSnapshot&lt;/code&gt;, &lt;code&gt;IOptionsMonitor&lt;/code&gt; provides a property with the current value. But it also provides the possibility of registering an &lt;code&gt;OnChange&lt;/code&gt; listener, that will be triggered when the configuration reloads.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;public class SomeService
{
    private readonly IOptionsMonitor&amp;#x3C;Settings&gt; _settings;

    public SomeService(IOptionsMonitor&amp;#x3C;Settings&gt; settings)
    {
        _settings = settings;
        settings.OnChange((s) =&gt;
        {
            Console.WriteLine(&quot;Config changed: SomeString={0}&quot;,
                s.SomeString);
        });
    }
    
    public string? SomeMethod()
    {
        return _settings.CurrentValue.SomeString;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;If you want to know more about the use of &lt;code&gt;IOptions&lt;/code&gt; vs &lt;code&gt;IOptionsSnapshot&lt;/code&gt; vs &lt;code&gt;IOptionsMonitor&lt;/code&gt;, Andrew Lock has &lt;a href=&quot;https://andrewlock.net/reloading-strongly-typed-options-in-asp-net-core-1-1-0/&quot;&gt;some&lt;/a&gt; &lt;a href=&quot;https://andrewlock.net/creating-singleton-named-options-with-ioptionsmonitor/&quot;&gt;great&lt;/a&gt; &lt;a href=&quot;https://andrewlock.net/the-dangers-and-gotchas-of-using-scoped-services-when-configuring-options-with-options-builder/&quot;&gt;articles&lt;/a&gt; on this matter, that goes into a lot more details than I do here.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;Conclusion&lt;/h1&gt;
&lt;p&gt;Hopefully, this provides an overview of how you can implement your own configuration provider. We have been using this in production for a few months, without any issues. This version polls Azure Blob Storage to check whether or not the ETag has changed. There are other options for handling changes in Azure Blob Storage.&lt;/p&gt;
&lt;p&gt;You can enable a change feed for the storage account, and then dive into the change feed, and react when the specific blob has been updated. When a blob has changed, it is reflected in the change feed within minutes.&lt;/p&gt;
&lt;p&gt;If &quot;within minutes&quot; is not fast enough, you can also get Blob Storage events. These events are pushed onto Azure Event Grid in near-realtime and allow for reacting to changes much faster. However we do not currently use Azure Event Grid, and we did not find it feasible to do so, just for this.&lt;/p&gt;
&lt;p&gt;We can make further improvements to the configuration provider: We could make it async; We could make sure we will not trigger multiple updates at once. This is outside the scope of this article, but you can always take a look at the &lt;a href=&quot;https://github.com/NovoNordisk-OpenSource/configuration-azure-blob/blob/main/src/NovoNordisk.Configuration.AzureBlob/BlobJsonConfigurationProvider.cs&quot;&gt;code&lt;/a&gt; for the &lt;a href=&quot;https://github.com/NovoNordisk-OpenSource/configuration-azure-blob&quot;&gt;NovoNordisk.Configuration.AzureBlob&lt;/a&gt; NuGet package to see how we ended up doing it - most should look familiar after reading this article.&lt;/p&gt;
&lt;h1&gt;References&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://mousavi310.github.io/posts/a-refreshable-sql-server-configuration-provider-for-net-core/&quot;&gt;Morteza Mousavi - A Refreshable SQL Server Configuration Provider for .NET Core&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/qinezh/Microsoft.Extensions.Configuration.AzureBlob&quot;&gt;qinezh - Microsoft.Extensions.Configuration.AzureBlob&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/milestonetg/extensions-configuration-s3&quot;&gt;MilestoneTG - extensions-configuration-s3&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="/_astro/hero._2f05aWv.png"/><enclosure url="/_astro/hero._2f05aWv.png"/></item></channel></rss>