This repo contains the Atc.Cosmos
library for configuring containers in Cosmos and providing an easy way to read and write document resources.
The library is installed by adding the nuget package Atc.Cosmos
to your project.
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
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:
-
As an argument to the
ConfigureCosmos()
extension method. -
As a
Func<IServiceProvider, CosmosOptions>
factory method argument on theConfigureCosmos()
extension method. -
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.
For each Cosmos resource you want to access using the ICosmosReader<T>
and ICosmosWriter<T>
you will need to:
-
Create class representing the Cosmos document resource.
The class should implement the abstract
CosmosResource
base-class, which requiresGetDocumentId()
andGetPartitionKey()
methods to be implemented.The class will be serialized to Cosmos using the
System.Text.Json.JsonSerializer
, so theSystem.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. -
Configure the container used for the Cosmos document resource.
This is done on the
ICosmosBuilder
made available using theConfigureCosmos()
extension on theIServiceCollection
, like this:public void ConfigureServices(IServiceCollection services) { services.ConfigureCosmos(b => b.AddContainer<MyResource>(containerName)); }
-
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.
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:
-
Create an initializer by implementing the
ICosmosContainerInitializer
interface.Usually the implementation will call the
CreateContainerIfNotExistsAsync()
method on the providedDatabase
object with the desiredContainerProperties
. -
Setup the initializer to be run during initialization
This is done on the
ICosmosBuilder
made available using theConfigureCosmos()
extension on theIServiceCollection
, like this:public void ConfigureServices(IServiceCollection services) { services.ConfigureCosmos(b => b.AddContainer<MyInitializer>(containerName)); }
-
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(); }
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.
The library supports adding change feed processors for a container.
To do this you will need to:
-
Create a processor by implementing the
IChangeFeedProcessor
interface. -
Setup the change feed processor during initialization
This is done on the
ICosmosBuilder<T>
made available using theConfigureCosmos()
extension on theIServiceCollection
, 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.
The library also has a preview version that exposes some of CosmosDB preview features.
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.
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.
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);
}