Skip to content

The DataverseClientFactory helps minimize the cost of opening or cloning Dataverse connections through connection pooling.

License

Notifications You must be signed in to change notification settings

emerbrito/dataverse-client-factory

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

EmBrito.Dataverse.Extensions.ClientFactory

Nuget GitHub Workflow Status

DataverseClientFactory and Connection Pooling

The DataverseClientFactory helps minimize the cost of openning or cloning Dataverse connections through connection pooling.

In this article, the term "connection" refers to a connected ServiceClient instance (which can be used interchangeably anywhere an IOrganizationService or IOrganizationServiceAsync2 is used).

Whenever a user requests a new connection, the pooler looks for an available connection in the pool. If a pooled connection is available it returns it instead of opening a new one. When the application calls Dispose in the connection, the pooler returns it to the set of pooled connections instead of actually closing it. Once the connection is available in the pool it is ready to be used again.

sequenceDiagram
    participant C as Consumer
    participant F as ClientFactory
    participant P as ConnectionPool
    C->>F: requests first connection
    F->>+C: clone internal client
    C-->>C: update records
    C->>-F: dispose() connection    
    F->>P: add connection to pool
    Note over C,P: connection is sent back to factory when Dispose() is called.
    C->>F: request new connection
    alt pool has available connections
    P-->>C: serve connection from pool
    else pool is empty 
    F->>+C: clone internal client
    end    
Loading

In the real world each application is different and the impact or performance gain will depend on the application however, any application can benefit from the layer of abstraction provided by the DataverseClientFactory.

IDataverseClient

As previoulsy mentioned to benefit from the connection pool you must call Dispose in the opened connection however, it is common for applications to accept an IOrganizationServiceAsync2 which doesn't implement it.

To provide the required methods the DataverseClientFactory returns an IDataverseClient instead. The only purpose of the custom interface is to provide the Dispose method.

This is the IDataverseClient source:

public interface IDataverseClient : IOrganizationServiceAsync2, IDisposable
{
}

With the exception of the Dispose method, when refactoring your code you should be able to replace any instances of IDataverseClient with IOrganizationServiceAsync2 and vice versa.

Using the DataverseClientFactory

Basic usage, NOT leveraging the connection pool:

DataverseClientFactory factory = new(myAppId, myAppSecret, myInstanceUrl, logger);
IOrganizationServiceAsync2 client = factory.CreateClient();

client.Create(someEntity);

Leveraging the connection pool by disposing of the client when it is no longer needed:

var factory = new DataverseClientFactory(myAppId, myAppSecret, myInstanceUrl, logger);

using (var client = factory.CreateClient())
{
    client.Create(someEntity);
}

You should always dispose of the connection when it is no longer required so that the connection will be returned to the pool. You can do this by calling the Dispose method or by opening the connection inside a using statement. Connections that are not explicitely disposed are not returned to the connection pool.

Dependency Injection

An extension method is provided to simplify the process of registering a DataverseClientFactory.

public override void Configure(IFunctionsHostBuilder builder)
{

    builder.Services.AddOptions();
    builder.Services.AddLogging();
    builder.Services.AddDataverseClientFactory(options => 
    {
        options.ClientId = "clientId";
        options.ClientSecret = "clientSecret";
        options.DataverseInstanceUri = "clientUrl";

    });
}

And in your code:

public class MyCustomService
{
    readonly IDataverseClientFactory _factory;

    public MyCustomService(IDataverseClientFactory clientFactory)
    {
        _factory = clientFactory ?? throw new ArgumentNullException(nameof(clientFactory));
    }

}

Injecting an IDataverseClient

In some cases, depending on your application architecture you can inject an IDataverseClient instead of the factory and still leverage connection pooling since a container is responsible for the cleaning up of types it creates and will call Dispose on IDisposable instances.

Here is an example:

// startup.cs
public override void Configure(IFunctionsHostBuilder builder)
{

    builder.Services.AddOptions();
    builder.Services.AddLogging();
    builder.Services.AddDataverseClientFactory(options => 
    {
        options.ClientId = "clientId";
        options.ClientSecret = "clientSecret";
        options.DataverseInstanceUri = "clientUrl";

    });
    builder.Services.AddTransient<IDataverseClient>(provider =>
    {
        var factory = provider.GetRequiredService<DataverseClientFactory>();
        return factory.CreateDataverseClient();
    });    
}

// MyCustomService.cs
public class MyCustomService
{
    readonly IDataverseClient _client;

    public MyCustomService(IDataverseClient client)
    {
        _client = client ?? throw new ArgumentNullException(nameof(client));
    }

}

Connecting to Multiple Dataverse Instances

To handle connections to different dataverse instances you can create specialized factories that derives from DataverseClientFactory as demonstrated below. These are meant to take in connection parameteres that are specific to each instance.

Creating two specialized factories:

public class ContosoClientFacotry : DataverseClientFactory
{
    public ContosoClientFacotry(ILogger<ContosoClientFacotry> logger) 
        : base("ContosoAppId", "ContosoAppSecret", "ContosoUrl", logger)
    {
    }
}

public class FabrikamClientFacotry : DataverseClientFactory
{
    public FabrikamClientFacotry(ILogger<FabrikamClientFacotry> logger)
        : base("FabrikamAppId", "FabrikamAppSecret", "FabrikamUrl", logger)
    {
    }
}

Using both factories:

var contosoFactory = new ContosoClientFacotry(contosoLogger);
var fabrikamFactory= new FabrikamClientFacotry(fabrikamLogger);

using (var contosoClient = contosoFactory.CreateClient())
using (var fabrikamClient = fabrikamFactory.CreateClient())
{
    var contact = contosoClient.Retrieve("contact", contactId);
    fabrikamClient.Create(contact);
}

Under the Hood: IDataverseClient

The IDataverseClient is an instance of the ServiceClient wraped by a DispatchProxy. The dispatchProxy allow us to intercept and repurpose calls to Dispose.

Benchmarks

These benchmarks where created using a Microsoft sample code that uses parallel tasks to increase throughput when connecting to a dataverse instance and performing data operations.

Because we want to focus on the time spent instantiating and serving connections, no data operations were performed.

The simulations used an IEnumerable containing 10, 100 and 500 records. A maximun degree or parallelism of 8 was set, meaning at any giving time we could have 8 connections opened simultaneously.

In these particular tests you can see how we went from spending around 3.3 seconds cloning and serving 500 connections to 12 μs (microseconds) by using connection pooling. This would have saved us over 3 seconds in execution time if this was a real world application.

BenchmarkDotNet=v0.13.3, OS=Windows 11 (10.0.22621.963)
Intel Core i7-1065G7 CPU 1.30GHz, 1 CPU, 8 logical and 4 physical cores
.NET SDK=6.0.307
  [Host]     : .NET 6.0.12 (6.0.1222.56807), X64 RyuJIT AVX2
  DefaultJob : .NET 6.0.12 (6.0.1222.56807), X64 RyuJIT AVX2
Method TotalEntities Mean Error StdDev Rank Gen0 Gen1 Allocated
UseConnectionPool 10 5.783 μs 0.0295 μs 0.0230 μs 1 1.0300 - 4.16 KB
UseConnectionPool 100 7.445 μs 0.0516 μs 0.0457 μs 2 1.2207 - 4.96 KB
UseConnectionPool 500 12.553 μs 0.2470 μs 0.4455 μs 3 1.6174 - 6.5 KB
CloneForEachThread 100 3,330.354 μs 58.0096 μs 54.2622 μs 4 292.9688 113.2813 1414.49 KB
CloneForEachThread 500 3,339.151 μs 38.1336 μs 35.6702 μs 4 289.0625 109.3750 1414.75 KB
CloneForEachThread 10 3,519.453 μs 67.8566 μs 103.6242 μs 5 292.9688 105.4688 1413.53 KB

About

The DataverseClientFactory helps minimize the cost of opening or cloning Dataverse connections through connection pooling.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages