Building a ServiceClient Factory for Multi-Tenant Dynamics 365 / Dataverse Apps

by Apr 18, 2025

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!

Remy van Duijkeren

Remy van Duijkeren

Power Platform Advisor

Microsoft Power Platform Advisor with over 25 years of experience in IT with a focus on (marketing) automation and integration.

Helping organizations with scaling their business by automating processes on the Power Platform (Dynamics 365).

Expert in Power Platform, Dynamics 365 (Marketing & Sales) and Azure Integration Services. Giving advice and strategy consultancy.

Services:
– Strategy and tactics advise
– Automating and integrating

Subscribe to
The Daily Friction

A daily newsletter on automation and eliminating friction

Related Content

External authentication with Dataverse ServiceClient

For a while now we can use the new Dataverse ServiceClient that replaced the old CrmServiceClient. It has three big improvements: Works for .NET 5.0 and up (.NET Core) Uses the newer MSAL.NET instead of ADAL.NET (which is out of support) for authentication Support for...

read more

Everyone got ALM wrong in Dynamics 365 / Dataverse

For ages, we've been ferociously encouraging the integration of developer practices, such as source control and ALM, into the Dynamics 365/Dataverse realm. The ultimate truth The revered 'Master Branch' in source control, has always been the sole fountainhead from...

read more
Early-Bound Classes for .NET 4.6.2 and 6.0

Early-Bound Classes for .NET 4.6.2 and 6.0

You like to use strong types in .NET when working with Dataverse / Dynamics 365? Are you into Early-Bound Classes? Generating entity classes? You can use CrmSvcUtil for this, but I personally like to use XrmContext from Delegate to do this, because it creates smaller...

read more