Search code examples
c#.netazureazure-cosmosdb

How can I connect to Cosmos DB only once at Startup in Azure Function?


I have a .NET 6 Azure Function written in C# that does not have any Program.cs nor Startup.cs classes. It only has all the endpoints that I need in .cs files (GET, POST, PUT and so on).

The function is running correctly but I have a problem with my connection to the Cosmos DB. I want to connect only once at startup of the function and then use that same connection throughout all the other endpoints.

How can I do that? Right now it seems I am creating a new DB object each time and that does not seem to be optimal at all.


Solution

  • The Azure SDK states that you should use a singleton for your Cosmos DB client.

    Here's a sample from Microsoft that uses static methods: https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/performance-tips-dotnet-sdk-v3?tabs=trace-net-core#sdk-usage. However, I would go with function dependency injection.

    My .csproj:

    <Project Sdk="Microsoft.NET.Sdk">
      <PropertyGroup>
        <TargetFramework>net6.0</TargetFramework>
        <AzureFunctionsVersion>v4</AzureFunctionsVersion>
      </PropertyGroup>
      <ItemGroup>
        <PackageReference Include="Microsoft.Azure.Cosmos" Version="3.32.2" />
        <PackageReference Include="Microsoft.Azure.Functions.Extensions" Version="1.1.0" />
        <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
        <PackageReference Include="Microsoft.NET.Sdk.Functions" Version="4.0.1" />
      </ItemGroup>
      <ItemGroup>
        <None Update="host.json">
          <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
        </None>
        <None Update="local.settings.json">
          <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
          <CopyToPublishDirectory>Never</CopyToPublishDirectory>
        </None>
      </ItemGroup>
    </Project>
    

    Startup.cs

    [assembly: FunctionsStartup(typeof(Startup))]
    
    namespace MyFunctionApp;
    
    public class Startup : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
            IConfiguration configuration = builder.GetContext().Configuration;
            builder.Services.AddSingleton<CosmosClient>((serviceProvider) =>
            {
                return new CosmosClient(configuration["CONNECTION_STRING"]);
            });
        }
    }
    

    ProductApi.cs

    public class ProductApi 
    {
       private readonly CosmosClient _client;
       public ProductApi(CosmosClient client) 
       {
            _client = client;
       }
    
        [FunctionName("GetProducts")]
        public async Task<IActionResult> Get(
            [HttpTrigger(AuthorizationLevel.Anonymous, methods: "get", Route = "products")] HttpRequest req,
            ILogger log)
        {
            // Get stuff from database/containers.
        }
    }
    

    This is just a quick example. I would not use the CosmosClient directly but instead put it behind some sort of service (don't forget to register it for DI).

    public interface IProductService
    {
        Task<IEnumerable<Product>> Read();
    }
    
    class ProductService : IProductService
    {
        private readonly CosmosClient client;
        private readonly Database database;
        private readonly Container container;
    
        public ProductService(CosmosClient cosmosClient)
        {
            client = cosmosClient;
            database = client.GetDatabase("tailwind");
            container = client.GetContainer(database.Id, "products");
        }
    
        public async Task<IEnumerable<Product>> Read()
        {
            var feedIterator = container.GetItemLinqQueryable<Product>().ToFeedIterator();
            var batches = new List<Product>();
    
            while (feedIterator.HasMoreResults)
                batches.AddRange(await feedIterator.ReadNextAsync().ConfigureAwait(false));
    
            return batches;
        }
    }