Skip to content

Latest commit

 

History

History
315 lines (228 loc) · 14.9 KB

README.md

File metadata and controls

315 lines (228 loc) · 14.9 KB

General Project Info

Github top language Github stars Github forks Github size Issues Open

Packages

Github Version NuGet Version NuGet Downloads

Build Status

Pre-Integration Post-Integration Release

Code Quality

Maintainability Rating Reliability Rating Security Rating Bugs Vulnerabilities

Atc.Cosmos

This repo contains the Atc.Cosmos library for configuring containers in Cosmos and providing an easy way to read and write document resources.

Installation

The library is installed by adding the nuget package Atc.Cosmos to your project.

Getting started

Once the library is added to your project, you will have access to the following interfaces, used for reading and writing Cosmos document resources:

A document resource is represented by a class deriving from the CosmosResource base-class, or by implementing the underlying ICosmosResource interface directly.

To configure where each resource will be stored in Cosmos, the ConfigureCosmos(builder) extension method is used on the IServiceCollection when setting up dependency injection (usually in a Startup.cs file).

This will be explained in the following sections:

  • Configure Cosmos connection
  • Configure containers
  • Initialize containers
  • Using the readers and writers

Configure Cosmos connection

For configuring how the library connects to Cosmos, the library uses the CosmosOptions class. This includes the following settings:

Name Description
AccountEndpoint Url to the Cosmos Account.
AccountKey Key for Cosmos Account.
DatabaseName Name of the Cosmos database (will be provisioned by the library).
DatabaseThroughput The throughput provisioned for the database in measurement of Request Units per second in the Azure Cosmos DB service.
SerializerOptions The JsonSerializerOptions used for the System.Text.Json.JsonSerializer.
Credential The TokenCredential used for accessing cosmos with an Azure AD token. Please note that setting this property will ignore any value specified in AccountKey.

There are 3 ways to provide the CosmosOptions to the library:

  1. As an argument to the ConfigureCosmos() extension method.

  2. As a Func<IServiceProvider, CosmosOptions> factory method argument on the ConfigureCosmos() extension method.

  3. As a IOptions<CosmosOptions> instance configured using the Options framework and registered in dependency injection.

    This could be done by e.g. reading the CosmosOptions from configuration, like this:

    services.Configure<CosmosOptions>(
      Configuration.GetSection(configurationSectionName));

    Or by using a factory class implementing the IConfigureOptions<CosmosOptions> interface and register it like this:

    services.ConfigureOptions<ConfigureCosmosOptions>();

    The latter is the recommended approach.

Configure containers

For each Cosmos resource you want to access using the ICosmosReader<T> and ICosmosWriter<T> you will need to:

  1. Create class representing the Cosmos document resource.

    The class should implement the abstract CosmosResource base-class, which requires GetDocumentId() and GetPartitionKey() methods to be implemented.

    The class will be serialized to Cosmos using the System.Text.Json.JsonSerializer, so the System.Text.Json.Serialization.JsonPropertyNameAttribute can be used to control the actual property name in the json document.

    This can e.g. be useful when referencing the name of the id and partition key properties in a ICosmosContainerInitializer implementation which is described further down.

  2. Configure the container used for the Cosmos document resource.

    This is done on the ICosmosBuilder made available using the ConfigureCosmos() extension on the IServiceCollection, like this:

    public void ConfigureServices(IServiceCollection services)
    {
      services.ConfigureCosmos(b => b.AddContainer<MyResource>(containerName));
    }
  3. If you want to connect to multiple databases you would need to scope your container to a new CosmosOptions instance in the following way:

    public void ConfigureServices(IServiceCollection services)
    {
      services.ConfigureCosmos(
          b => b.AddContainer<MyResource>(containerName)
                .ForDatabase(secondDbOptions)
                  .AddContainer<MySecondResource>(containerName));
    }

    The first call to AddContainer will be scoped to the default options as the passed builder 'b' is always scoped to the default options. The subsequent call to ForDatabase will return a new builder scoped for the options passed to this method and any subsequent calls to this builder will have the same scope.

Initialize containers

The library supports adding initializers for each container, that can then be used to create the container, and configure it with the correct keys and indexes.

To do this you will need to:

  1. Create an initializer by implementing the ICosmosContainerInitializer interface.

    Usually the implementation will call the CreateContainerIfNotExistsAsync() method on the provided Database object with the desired ContainerProperties.

  2. Setup the initializer to be run during initialization

    This is done on the ICosmosBuilder made available using the ConfigureCosmos() extension on the IServiceCollection, like this:

    public void ConfigureServices(IServiceCollection services)
    {
      services.ConfigureCosmos(b => b.AddContainer<MyInitializer>(containerName));
    }
  3. Chose a way to run the initialization

    For an AspNet Core services, a HostedService can be used, like this:

    public void ConfigureServices(IServiceCollection services)
    {
      services.ConfigureCosmos(b => b.UseHostedService()));
    }

    For Azure Functions, the AzureFunctionInitializeCosmosDatabase() extension method can be used to execute the initialization (synchronously) like this:

    public void Configure(IWebJobsBuilder builder)
    {
        ConfigureServices(builder.Services);
        builder.Services.AzureFunctionInitializeCosmosDatabase();
    }

Using the readers and writers

Once the setup is in place, the readers and writers are registered with the Microsoft.Extensions.DependencyInjection container, and can be obtained via constructor injection on any service.

The registered interfaces are:

Name Description
ICosmosReader<T> Represents a reader that can read Cosmos resources.
ICosmosWriter<T> Represents a writer that can write Cosmos resources.
ICosmosBulkReader<T> Represents a reader that can perform bulk reads on Cosmos resources.
ICosmosBulkWriter<T> Represents a writer that can perform bulk operations on Cosmos resources.

The bulk reader and writer are for optimizing performance when executing many operations towards Cosmos. It works by creating all the tasks and then use the Task.WhenAll() to await them. This will group operations by partition key and send them in batches of 100.

When not operating with bulks, the normal readers are faster as there is no delay waiting for more work.

Change Feeds

The library supports adding change feed processors for a container.

To do this you will need to:

  1. Create a processor by implementing the IChangeFeedProcessor interface.

  2. Setup the change feed processor during initialization

    This is done on the ICosmosBuilder<T> made available using the ConfigureCosmos() extension on the IServiceCollection, like this:

    public void ConfigureServices(IServiceCollection services)
    {
      services.ConfigureCosmos(b => b
        .AddContainer<MyInitializer, MyResource>(containerName)
        .WithChangeFeedProcessor<MyChangeFeedProcessor>());
    }

    or using the ICosmosContainerBuilder<T> like this:

    public void ConfigureServices(IServiceCollection services)
    {
      services.ConfigureCosmos(b => b
        .AddContainer<MyInitializer>(
          containerName,
          c => c
            .AddResource<MyResource>()
            .WithChangeFeedProcessor<MyChangeFeedProcessor>()));
    }

Note: The change feed processor relies on a HostedService, which means that this feature is only available in AspNet Core services.

Preview Features

The library also has a preview version that exposes some of CosmosDB preview features.

Priority Based Execution

When using the preview version, you will have access to the following interfaces, used for reading and writing Cosmos document resources:

Name Description
ILowPriorityCosmosReader<T> Represents a reader that can read Cosmos resources with low priority.
ILowPriorityCosmosWriter<T> Represents a writer that can write Cosmos resources with low priority.
ILowPriorityCosmosBulkReader<T> Represents a reader that can perform bulk reads on Cosmos resources with low priority.
ILowPriorityCosmosBulkWriter<T> Represents a writer that can perform bulk operations on Cosmos resources with low priority.

In order to use these interfaces the "Priority Based Execution" feature needs to be enabled on the CosmosDB account.

This can be done by either enabling it directly in Azure Portal under Settings -> Features tab on the CosmosDB resource.

Alternatively through Azure CLI:

# install cosmosdb-preview Azure CLI extension
az extension add --name cosmosdb-preview

# Enable priority-based execution
az cosmosdb update  --resource-group $ResourceGroup --name $AccountName --enable-priority-based-execution true

See MS Learn for more details.

Delete resources by partition key

The preview version of the library extends the ICosmosWriter and ILowPriorityCosmosWriter with and additional method DeletePartitionAsync to delete all resources in a container based on a partition key. The deletion will be executed in a CosmosDB background service using a percentage of the RU's available. The effect are available immediatly as all resources in the partition will not be available through reads or queries.

In order to use this new method the "Delete All Items By Partition Key" feature needs to be enabled on the CosmosDB account.

This can be done through Azure CLI:

# Delete All Items By Partition Key
az cosmosdb update  --resource-group $ResourceGroup --name $AccountName --capabilities DeleteAllItemsByPartitionKey

or wih bicep:

resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2024-05-15' = {
  name: cosmosName
  properties: {
    databaseAccountOfferType: 'Standard'
    locations: location
    capabilities: [
      {
        name: 'DeleteAllItemsByPartitionKey'
      }
    ]
  }
}

If the feature is not enabled when calling this method then a CosmosException will be thrown.

See MS Learn for more details.

Unit Testing

The reader and writer interfaces can easily be mocked, but in some cases it is nice to have a fake version of a reader or writer to mimic the behavior of the read and write operations. For this purpose the Atc.Cosmos.Testing namespace contains the following fakes:

Name Description
FakeCosmosReader<T> Used for faking an ICosmosReader<T> or ICosmosBulkReader<T>.
FakeCosmosWriter<T> Used for faking an ICosmosWriter<T> or ICosmosBulkWriter<T>.
FakeCosmos<T> Used for getting a FakeCosmosReader and FakeCosmosWriter that share state.

Using the Atc.Test setup a test using the fakes could look like this:

[Theory, AutoNSubstituteData]
public async Task Should_Update_Cosmos_With_NewData(
    [Frozen(Matching.ImplementedInterfaces)]
    FakeCosmos<MyCosmosResource> cosmos,
    MyCosmosService sut,
    MyCosmosResource resource,
    string newData)
{
    cosmos.Documents.Add(resource);

    await service.UpdateAsync(resource.Id, newData);

    resource
        .Data
        .Should()
        .Be(newData);
}