Skip to content

LarsBauer/dataverse-serviceclient-extensions

Repository files navigation

Dataverse.Extensions.DependencyInjection

CI NuGet NuGet Downloads License

Dependency injection extensions for Microsoft.PowerPlatform.Dataverse.Client. Registers a singleton ServiceClient and a scoped IOrganizationServiceAsync2 (via Clone()) — the correct pattern most people get wrong.

Features

  • One-line DI registration for ServiceClient with proper singleton + scoped Clone() lifecycle
  • Authentication via Azure.Identity (DefaultAzureCredential by default, any TokenCredential supported)
  • Automatic logger wiring from the DI container
  • Options validation at startup — fail fast on misconfiguration
  • Targeted at ASP.NET Core and Azure Functions

Get started

Install the package from NuGet:

dotnet add package BauerApps.Dataverse.Extensions.DependencyInjection

Register the client in Program.cs:

builder.Services.AddDataverseClient(options =>
{
    options.OrganizationUrl = new Uri("https://my-org.crm4.dynamics.com");
});

Inject IOrganizationServiceAsync2 anywhere:

public class AccountsController(IOrganizationServiceAsync2 dataverse) : ControllerBase
{
    [HttpGet("{id:guid}")]
    public async Task<IActionResult> Get(Guid id)
    {
        var entity = await dataverse.RetrieveAsync("account", id,
            new ColumnSet("name", "revenue"));
        return Ok(entity);
    }
}

Configuration

Configure via DataverseClientOptions:

Option Required Default Description
OrganizationUrl Base URL of your Dataverse environment (e.g. https://my-org.crm4.dynamics.com)
TokenCredential DefaultAzureCredential Custom TokenCredential for authentication. Supports any Azure.Identity credential.
DeferConnection false When true, connection to Dataverse is deferred until first use.

Authentication examples

Default (system-assigned managed identity / local dev):

builder.Services.AddDataverseClient(options =>
{
    options.OrganizationUrl = new Uri("https://my-org.crm4.dynamics.com");
});

User-assigned managed identity:

builder.Services.AddDataverseClient(options =>
{
    options.OrganizationUrl = new Uri("https://my-org.crm4.dynamics.com");
    options.TokenCredential = new DefaultAzureCredential(new DefaultAzureCredentialOptions
    {
        ManagedIdentityClientId = "d0f19fa6-76ef-46cb-93ac-fcde5a4a6143"
    });
});

Client secret:

builder.Services.AddDataverseClient(options =>
{
    options.OrganizationUrl = new Uri("https://my-org.crm4.dynamics.com");
    options.TokenCredential = new ClientSecretCredential(tenantId, clientId, clientSecret);
});

Environment-specific configuration

Bind the options directly from a configuration section:

// appsettings.Production.json
{
  "Dataverse": {
    "OrganizationUrl": "https://my-org-prod.crm4.dynamics.com",
    "DeferConnection": false
  }
}
builder.Services.AddDataverseClient(builder.Configuration.GetSection("Dataverse"));

Only non-secret values bind from configuration. Authentication uses DefaultAzureCredential by default; to supply a custom credential, layer it on with the options pattern:

builder.Services.AddDataverseClient(builder.Configuration.GetSection("Dataverse"));
builder.Services.PostConfigure<DataverseClientOptions>(options =>
    options.TokenCredential = new ClientSecretCredential(tenantId, clientId, clientSecret));

Why scoped IOrganizationServiceAsync2?

ServiceClient is registered as a singleton to share the underlying connection, metadata cache, and authentication token. However, using a single instance across concurrent requests can cause issues (e.g., CallerId leaking between requests).

Clone() creates a lightweight copy that shares the parent's connection pool but is safe for per-request use. This library registers IOrganizationServiceAsync2 as scoped, so each request gets its own clone automatically.

License

MIT

About

Dependency injection extensions for Microsoft Dataverse ServiceClient

Resources

License

Stars

Watchers

Forks

Contributors

Languages