2

I am creating an Azure Function App in Visual Studio with C# and .NET 6.

I have a service I created (CosmosDBService) that implements the interface ICosmosDBService:

public class CosmosDbService : ICosmosDbService
{
    private Container? _container = null;

    public CosmosDbService(
        CosmosClient cosmosDbClient,
        string databaseName,
        string containerName)
    {
        _container = cosmosDbClient.GetContainer(databaseName, containerName);
    }

I want to pass two different instances of this service into the Function App. Each service instance would represent a different container.

How would I set this up in Startup:FunctionsApp class using the FunctionsHostBuilder?

Guru Stron
  • 102,774
  • 10
  • 95
  • 132
Mike Lenart
  • 767
  • 1
  • 5
  • 19
  • See [Dependency Injection Unity - Conditional Resolving](https://stackoverflow.com/a/32415954). Although that answer involves Unity, the design doesn't depend on any Unity-specific functionality to work. See Nick's answer for how to register multiple services with the same interface in .NET Core. – NightOwl888 May 24 '22 at 19:48

3 Answers3

3

Default DI container does not support named such scenarios so you have next options - either create separate interfaces and implementations (and register/resolve them) for each databaseName-containerName pair or create a factory and use it to generate desired CosmosDbService instance:

public interface ICosmosDbServiceFactory
{
    ICosmosDbService Create(string databaseName, string containerName);
}

class CosmosDbServiceFactory : ICosmosDbServiceFactory
{
    private readonly IServiceProvider _serviceProvider;

    public CosmosDbServiceFactory(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public ICosmosDbService Create(string databaseName, string containerName) => new CosmosDbService(
        _serviceProvider.GetRequiredService<CosmosClient>(),
        databaseName,
        containerName
    );
}

Register it with appropriate lifetime and inject it into corresponding class and use it in the constructor to resolve required ICosmosDbService instances.

Guru Stron
  • 102,774
  • 10
  • 95
  • 132
1

You can do this, but I wouldn't recommend it. For instance in your start up if you had the following code:

services.AddSingleton<ICosmosDbService, CosmosDbService>();
services.AddSingleton<ICosmosDbService, OtherCosmosDbService>();

both instances would be registered in the Di container. If you had a class that depends on this interface, the following constructor would result in OtherCosmosDbService being injected:

public class SomeClass {
   private readonly ICosmosDbService _service;
   public SomeClass(ICosmosDbService service){
      _service = service; // This would be OtherCosmosDbService
   }
}

Both would be registered and in this instance, the last one registered "wins". If you wanted to get both then you could change the constructor to this:

public SomeClass(IEnumerable<ICosmosDbService> services){
   // Write logic to handle finding which one you want
}

Honestly, I would go with Guru Stron's suggestion of creating separate interfaces for each container and registering them separately.

Nick Cipollina
  • 501
  • 3
  • 13
0

I think your design needs to be more granular - trying to access multiple containers (possibly in multiple COSMOS databases) with one interface/class goes against the SOLID principle of single responsibilty. Think about your domain and work from there 'downwards'

public interface ICustomers
{
    public IEnumerable<Customer> GetCustomers();
}

public interface IProducts
{
    public IEnumerable<Product> GetProducts();
}

public class CustomersInCosmosDatabase : ICustomers
{
    private readonly CosmosClient cosmosClient;

    public CustomersInCosmosDatabase(CosmosClient cosmosClient)
    {
        this.cosmosClient = cosmosClient;
    }

    public IEnumerable<Customer> GetCustomers()
    {
        var container = cosmosClient.GetContainer("customerDatabaseId", "customerContainerId");

        return container.GetItemLinqQueryable<Customer>();
    }
}

public class ProductsInCosmosDatabase : IProducts
{
    private readonly CosmosClient cosmosClient;

    public ProductsInCosmosDatabase(CosmosClient cosmosClient)
    {
        this.cosmosClient = cosmosClient;
    }

    public IEnumerable<Product> GetProducts()
    {
        var container = cosmosClient.GetContainer("productDatabaseId", "prodcutContainerId");

        return container.GetItemLinqQueryable<Product>();
    }
}

and your registrations become:

    serviceCollection.AddAzureClients(clientBuilder =>
    {
        clientBuilder.AddClient<CosmosClient, CosmosClientOptions>((o) =>
            new CosmosClient("connectionString", new DefaultAzureCredential()));
    });

    serviceCollection.AddTransient<ICustomers, CustomersInCosmosDatabase>();
    serviceCollection.AddTransient<IProducts, ProductsInCosmosDatabase>();

You are then in the business of injecting Customer collections and Product collections everywhere NOT CosmosDBServices.

  • Could you please explain why you use `.AddAzureClients(...)` with `clientBuilder.AddClient` rather than adding `CosmosClient` as a singleton like in https://stackoverflow.com/a/69880032/1114918? – hansmbakker May 24 '23 at 15:39
  • Apologies @hansmbakker - I can't believe my answer to the original post was a year ago! For anyone still interested: MS provide extension methods to cater for the relatively complex/nuanced issues around clients like this. There's nothing stopping you from registering one as you see fit but if you just want the 'off-the-shelf default' this will do it. – Steve Grattan Jun 06 '23 at 12:18
  • I meant that `serviceCollection.AddAzureClients` comes from the newer SDK generation (namespace starting with `Azure.`). This is available for e.g. the Storage SDK or KeyVault SDK but they seem to have abandoned updating the CosmosDB SDK to that modern generation since 2020 - see https://www.nuget.org/packages/Azure.Cosmos/#versions-body-tab and https://github.com/Azure/azure-cosmos-dotnet-v3/tree/releases/4.0.0-preview3. – hansmbakker Jun 07 '23 at 08:13