Working with more than one Power Platform or Dynamics 365 environment can quickly become a headache—especially when you’re building a SaaS solution that needs to connect to many different customer instances and it needs to run in Azure.
In my case, I have a multi-tenant app that uses the same client credentials to connect to different customer Dataverse environments. The only thing that differs between them is the instance URL.
How to manage all the Dataverse connections? The multiple instances of ServiceClient
/ OrganizationService
in .NET?
At first, this setup sounds manageable. But in practice, managing connection lifetimes, reusing them efficiently, and avoiding unnecessary re-authentication is more difficult than it looks.
Scoped vs Singleton
My first idea was to avoid a Singleton and instead create a new ServiceClient
on every request (scoped). Just reuse the credentials and pass different URLs into the ServiceClient
, right?
The problem is that creating a new ServiceClient
can be slow (often >300ms) due to the authentication process.
Each scoped instance also consumes a connection from the connection pool. This becomes especially problematic in consumption-based Azure Functions, where you’re limited to 600 outbound connections. Each new ServiceClient
reserves a connection, and it can take up to 5 minutes of inactivity before it is released. That doesn’t scale under load (I’ve seen it firsthand 😔).
A Singleton, on the other hand, is created once and reused across calls—even across Azure Function invocations on the same instance. There’s no startup penalty per request, and it helps stay within connection limits. However, it does require extra handling to detect and recover from failed or expired tokens (which expire after 60 minutes).
Despite that, Singleton is the better choice. It’s slightly more work to add resilience, but the performance and stability gains—especially under high load—are worth it.
Dependency Injection (DI): Named Clients Support
Another challenge is creating and retrieving a ServiceClient
specific to each customer based on the incoming request.
You can’t register multiple ServiceClient
instances in the default .NET Dependency Injection container and retrieve the right one dynamically. What I needed was a way to resolve instances by name or by InstanceUrl
—just like how Named Clients (HttpClient
) work via IHttpClientFactory
. Internally it uses the NamedClientFactory
to achieve this.
So the idea is to create my own factory, the OrganizationServiceFactory
. It creates ServiceClient
instances based on the instance URL. The interface is very simple:
public interface IOrganizationServiceFactory
{
public IOrganizationServiceAsync2 CreateClient(Uri instanceUrl);
}
Design Goals
When I started building this factory, I set out a few key requirements:
- Instantiate clients lazily (only when needed)
- Reuse existing clients to avoid hitting connection limits (Singleton)
- Evict them when they haven’t been used for a while or when the token expires
- Stable under high load
That’s it. No unnecessary abstraction. No overengineering.
Implementation
So I built something simple: a lightweight wrapper around the Dataverse ServiceClient
to manage named connections efficiently—without heavy frameworks.
The key to keeping it simple is relying on IMemoryCache
. It handles the eviction and expiration so I don’t have to write the fragile logic myself.
Here is the OrganizationServiceFactory
implementation:
⚠️ One thing to note about using IMemoryCache
:
I couldn’t use the built-in GetOrCreate or GetOrCreateAsync methods that take a factory delegate. Under high load, the factory delegate can be called multiple times concurrently.
Third-party libraries like LazyCache or the new HybridCache in .NET 9 solve this (stampede protection). But HybridCache doesn’t have a way (yet) to dispose the client when evicted from the cache, so for now I do it by manual locking and double-checking when adding it client to the cache:
// We use MemoryCache because we can configure the expiration very easy. We do our own manual locking with
// double checking, because GetOrCreate doesn't protect from running the delegate multiple times.
if (!cache.TryGetValue(cacheKey, out ServiceClient? client))
{
lock (s_lock)
{
if (!cache.TryGetValue(cacheKey, out client))
{
client = CreateServiceClient();
}
}
}
The heavy lifting happens in the local function CreateServiceClient
. You can tweak this if you want to use a certificate or another method of authenticating:
// Create a new ServiceClient instance and store it in the cache
ServiceClient CreateServiceClient()
{
client = new ServiceClient(instanceUrl, clientId, clientSecret, useUniqueInstance: true, logger: logger)
{
// Disabled internal cross thread safeties, this will gain much higher performance.
// This class does the pooling manual.
DisableCrossThreadSafeties = true,
// Disable 'prefer' a given node on each request https://markcarrington.dev/2021/05/26/improving-bulk-dataverse-performance-with-enableaffinitycookie/
EnableAffinityCookie = false
};
logger.LogInformation("ServiceClient created for {InstanceUrl} ({OrganizationFriendlyName}), OrganizationId: {OrganizationId}",
instanceUrl, client.ConnectedOrgFriendlyName, client.ConnectedOrgId);
// entry will be added when disposed
using var entry = cache.CreateEntry(cacheKey);
entry.AbsoluteExpirationRelativeToNow = s_maxConnectionLifetime;
entry.SlidingExpiration = s_slidingConnectionExpiration;
entry.Priority = CacheItemPriority.NeverRemove;
entry.RegisterPostEvictionCallback(AfterCacheEviction);
entry.Value = client;
return client;
}
To clean up, I also register a PostEvictionCallback
to properly dispose of each client.
How to Use
Register the OrganizationServiceFactory
with Dependency Injection:
services.AddSingleton<IOrganizationServiceFactory>(provider =>
new OrganizationServiceFactory(
context.Configuration["Dataverse:ClientId"]),
ServiceClient.MakeSecureString(context.Configuration["Dataverse:ClientSecret"]),
provider.GetRequiredService<IMemoryCache>(),
provider.GetRequiredService<ILogger<OrganizationServiceFactory>>()));
Now you can use it in, for example, a controller:
[ApiController]
[Route("[controller]")]
public class MyController : ControllerBase
{
private readonly IOrganizationServiceFactory _orgServiceFactory;
public MyController(
IOrganizationServiceFactory organizationServiceFactory)
{
_organizationServiceFactory = organizationServiceFactory;
}
[HttpGet("action")]
public IActionResult DoAction()
{
// Find instanceUrl based on the incoming request
var instanceUrl = new Uri("https://example.crm4.dynamics.com")
IOrganizationServiceAsync2 orgService = _orgServiceFactory.CreateClient(instanceUrl);
// Logic with orgService
...
return Ok();
}
}
Your Turn
If you’ve been juggling Dataverse clients across multiple environments or tenants—how do you handle it?
Would love to hear your take, especially if you’ve gone full DI container or something more advanced.
Let me know!