ASP.NET Core singleton acting as an HttpClient provider
Alternative proposal

I think, I'd approach it differently, but I may miss some important details.

As long as the configuration change does not happen too frequently OR client construction is cheap, we can actually create a new instance of the HttpClient on every configuration change event, rather than trying to keep a single object in a good state. IMO, this approach is cleaner in the functional programming sense: the Client property now becomes a pure function in a way -- no messing with state of the object.

Now, you probably still want to keep the same HttpClient instance for as long as the config does not change -- to avoid an unnecessary object construction. Your HttpClientProvider can then work as a client factory and client "cache" in a way. This class will be responsible for providing either an existing properly configured HttpClient instance, or generating a new one since configuration has changed.

As a side note, I assume you use an IoC container for injecting HttpClientProvider into the consumers. The provider is probably registered as a Singleton with the IoC container, right?

Nitpick

There's no need to immediately create a client instance on MachineDatabaseHttpClientProvider construction. This can be deferred until the Client instance is being accessed by the consumer (e.g. Lazy&lt;HttpClient&gt; may work).

P. S.

I know that my answer is twisting the problem definition. Hopefully, this is not an issue (especially if there are no other requirements that are forcing you to have a single instance of an HttpClient throughout the entire application lifetime).

--------------- UPDATE 1 ---------------

I am not a guru of multi-threading but I don't think you should have any issue in most of the cases even with a thread-unsafe code here. I have different concerns though (explained below).

Simplicity

If you lock around the _currentHttpClient object [obviously, not lock (_currentHttpClient) but rather lock (_currentHttpClientLock)], you will guarantee that NO unnecessary client is created "ever".
Otherwise, you may have an extra object created n times per configuration change when there're n threads asking for a client "simultaneously".

Warnings/Pitfalls

Non-locking approach is not an issue only as long as it's okay to swap the "hot" client.
I.e. the one used by a consumer for performing multiple client calls within a single body of work.
It also depends a lot on how the consumer is using the HttpClient.
Here are three different scenarios with their own problems.

Option 1

Here's a consumer that does not hold an HttpClient reference, but keeps retrieving the client from provider for EACH external call:

class ConsumingController
{
    private MachineDatabaseHttpClientProvider _provider;

    ConsumingController(MachineDatabaseHttpClientProvider provider)
    {
        _provider = provider;
    }

    // ...

    [HttpGet]
    public Task&lt;ComplexObject&gt; GetComplexObject(string id)
    {
        ComplexObject complexObject;

        // Moment 1
        complexObject = _myOwnCopyOfClient.Client.GetObject("base" + id);

        // Moment 2 
        complexObject.Details = _myOwnCopyOfClient.Client.GetObject("datails-for-" + id);

        return complexObject;
    }
}


If the configuration changes between the moments 1 and 2 (say endpoint changes), the two requests will target different databases and it may result in funny results.
And I mean, funny in a bad way.

In a good case, you'll get an HTTP 404 for the second request, which is safe. All the consuming application needs to do is repeat the request...

In a worse case, there could be "detail" objects in the second DB that match the id from the first DB. This will result in construction of an object that presents data from two incompatible sources.

You application should be able to handle this situation.

Option 2

Another consumer design will simply hold the reference to the HttpClient instance:

class ConsumingController
{
    private HttpClient _myOwnCopyOfClient;

    ConsumingController(MachineDatabaseHttpClientProvider provider)
    {
        _myOwnCopyOfClient = provider.Client;
    }

    // ...

    [HttpGet]
    public Task&lt;ComplexObject&gt; GetComplexObject(string id)
    {
        ComplexObject complexObject;
        complexObject = _myOwnCopyOfClient.GetObject("base" + id);
        complexObject.Details = _myOwnCopyOfClient.GetObject("datails-for-" + id);
        return complexObject;
    }
}


Notice that if the consumer persists the reference to an HttpClient, there's NO effective WAY for the Provider to update the configuration of that instance.

Option 3

This may be the best option...

class ConsumingController
{
    private MachineDatabaseHttpClientProvider _provider;

    ConsumingController(MachineDatabaseHttpClientProvider provider)
    {
        _provider = provider;
    }

    // ...

    [HttpGet]
    public Task&lt;ComplexObject&gt; GetComplexObject(string id)
    {
        // This client instance is bound to the method execution scope...
        var requestScopeClient = _myOwnCopyOfClient.Client;

        ComplexObject complexObject;

        // Moment 1
        complexObject = requestScopeClient.GetObject("base" + id);

        // Moment 2 
        complexObject.Details = requestScopeClient.GetObject("datails-for-" + id);

        return complexObject;
    }
}