The CoreEx.Cosmos
namespace provides extended Azure Cosmos DB capabilities, specifically focused on the API for NoSQL.
The motivation is to provide supporting Cosmos DB capabilities for CRUD related access that support standardized CoreEx data access patterns. This for the most part will simplify and unify the approach to ensure consistency of implementation where needed.
The requirements for usage are as follows.
- An entity (DTO) that represents the data that must as a minimum implement
IEntityKey
; generally via either the implementation ofIIdentifier
orIPrimaryKey
. - A model being the underlying data representation that will be persisted within Cosmos DB itself.
- An
IMapper
that contains the mapping logic to map to and from the entity and model.
The entity and model are different types to encourage separation between the externalized entity representation and the underlying model; which may be shaped differently, and have different property naming conventions, internalized properties, etc.
To support railway-oriented programming whenever a method name includes WithResult
this indicates that it will return a Result
or Result<T>
including the resulting success or failure information. In these instances an Exception
will only be thrown when considered truly exceptional.
This article provides a good overview of the Azure Cosmos DB resource model; these concepts are important to understand when working with Cosmos DB.
CoreEx provides encapsulated capabilities for each of the following:
- Databases - contains one-or-more Containers.
- Containers (Collections) - contains one-or-more Items.
- Items (Documents) - the JSON object being persisted.
Each of the above are further described, in reverse order, as this is intended to make it easier to understand.
From a Cosmos DB perspective, an Item (aka Document) is a JSON object that represents the data that is being persisted. The are two key patterns for persisting a Document that CoreEx enables:
- Untyped - where a single Document type is persisted within the Container; being one or more Documents of the same type (schema/structure). This is the simpliest pattern, and requires no special support to enable; i.e. works out-of-the-box.
- Typed - where one or more Document types are persisted within the Container; being one or more Documents of different types (schema/structure). This is a more complex pattern, and requires additional support to enable - this is enabled in a consistent manner via the
CosmosDbValue<TModel>
class.
The Typed document JSON structure is as follows (standard Cosmos DB properties have been removed for brevity purposes):
{
"type": "document-type-name", # The unique name of the document type; used to query/filter the documents.
"value": { # The actual document _model_ data.
"property1": "value1",
"property2": "value2"
}
}
From a Cosmos DB perspective, a Container is a logical entity that represents a collection of items. The are two key patterns for interacting with a container that CoreEx enables:
- Entity - pattern in which there is separation between the externalized entity and the underlying model, and the requisite mapping between the two is fully integrated. This is the preferred pattern as it allows for a clear separation of concerns. These capabilities largely exist in the root
CoreEx.Cosmos
namespace. - Model - pattern in which the persisted model is directly interacted with, with the expectation that the developer would handle any mapping manually. This is useful in scenarios where the full entity is an overhead to the operations that needs to be performed. These capabilities largely exist in the
CoreEx.Cosmos.Model
namespace.
A Cosmos DB Container is encapsulated within one of the following CoreEx capabilities depending on the patterns required:
Type | Container Pattern | Document Pattern | IMapper support |
---|---|---|---|
CosmosDbContainer |
Entity | Untyped | Yes |
CosmosDbValueContainer |
Entity | Typed | Yes |
CosmosDbModelContainer |
Model | Untyped | No |
CosmosDbValueModelContainer |
Model | Typed | No |
Where more advanced CosmosDB capabilities are required, for example, Partitioning, etc., then the CosmosDbArgs
enables the configuration of these capabilities, as well as other extended CoreEx capabilities such as multi-tenancy support.
Additionally, given how important Partitioning is to Cosmos DB performance, many methods have been provided with an optional partitionKey
parameter to enable the developer to specify the partition key for the operation.
Finally, where a Container contains multiple typed documents, an advanced query capability is provided to select and return one-or-more types in a single performant operation; see CosmosDb.SelectMultiSetWithResultAsync
. The SelectMultiSetAsync
unit test provides example usage.
From a Cosmos DB perspective, a Database is a means to group one-or-more Containers.
The ICosmoDb
and corresponding CosmosDb
provides the base Database capabilities:
Container<T, TModel>()
- instantiates aCosmosDbContainer
instance.ValueContainer<T, TModel>()
- instantiates aCosmosDbValueContainer
instance.ModelContainer<TModel>()
- instantiates aCosmosDbModelContainer
instance.ValueModelContainer<TModel>()
- instantiates aCosmosDbValueModelContainer
instance.UserAuhtorizeFilter<TModel>()
- enables an authorization filter to be applied to a specified Container.
The following represents an example usage of the CosmosDb
class:
public class CosmosDb : CoreEx.Cosmos.CosmosDb
{
private readonly Lazy<CosmosDbContainer<Account, Model.Account>> _accounts;
private readonly Lazy<CosmosDbContainer<AccountDetail, Model.Account>> _accountDetails;
private readonly Lazy<CosmosDbContainer<Transaction, Model.Transaction>> _transactions;
/// <summary>
/// Initializes a new instance of the <see cref="CosmosDb"/> class.
/// </summary>
public CosmosDb(Mac.Database database, IMapper mapper) : base(database, mapper)
{
// Apply an authorization filter to all operations to ensure only the valid data is available based on the users context; i.e. only allow access to Accounts within list defined on ExecutionContext.
UseAuthorizeFilter<Model.Account>("Account", (q) => ((IQueryable<Model.Account>)q).Where(x => ExecutionContext.Current.Accounts.Contains(x.Id!)));
UseAuthorizeFilter<Model.Account>("Transaction", (q) => ((IQueryable<Model.Transaction>)q).Where(x => ExecutionContext.Current.Accounts.Contains(x.AccountId!)));
// Lazy create the containers.
_accounts = new(() => Container<Account, Model.Account>("Account"));
_accountDetails = new(() => Container<AccountDetail, Model.Account>("Account"));
_transactions = new(() => Container<Transaction, Model.Transaction>("Transaction"));
}
/// <summary>
/// Exposes <see cref="Account"/> entity from <b>Account</b> container.
/// </summary>
public CosmosDbContainer<Account, Model.Account> Accounts => _accounts.Value;
/// <summary>
/// Exposes <see cref="AccountDetail"/> entity from <b>Account</b> container.
/// </summary>
public CosmosDbContainer<AccountDetail, Model.Account> AccountDetails => _accountDetails.Value;
/// <summary>
/// Exposes <see cref="AccountDetail"/> entity from <b>Account</b> container.
/// </summary>
public CosmosDbContainer<Transaction, Model.Transaction> Transactions => _transactions.Value;
}
The entity ICosmosDbContainer<T, TModel>
and model CosmosDbModelContainer<TModel>
provides the base CRUD capabilities as follows.
A query is actioned using the CosmosDbQuery<T, TModel>
and CosmosDbModelQuery<TModel>
which is ostensibly a lightweight wrapper over an IQueryable<TModel>
that automatically maps from the model to the entity (where applicable).
Uses the Container.GetItemLinqQueryable
internally to create.
The following methods provide additional capabilities:
Method | Description |
---|---|
WithPaging |
Adds Skip and Take paging to the query. |
SelectSingleAsync , SelectSingleWithResult |
Selects a single item. |
SelectSingleOrDefaultAsync , SelectSingleOrDefaultWithResultAsync |
Selects a single item or default. |
SelectFirstAsync , SelectFirstWithResultAsync |
Selects first item. |
SelectFirstOrDefaultAsync , SelectFirstOrDefaultWithResultAsync |
Selects first item or default. |
SelectQueryAsync , SelectQueryWithResultAsync |
Select items into or creating a resultant collection. |
SelectResultAsync , SelectResultWithResultAsync |
Select items creating a ICollectionResult which also contains corresponding PagingResult . |
ToArrayAsync , ToArrayWithResultAsync |
Select items into a resulting array. |
Gets (GetAsync
or GetWithResultAsync
) the entity for the specified key mapping from the model. Uses the Container.ReadItemAsync
internally for the model and specified key.
Where the data is not found, then a null
will be returned. Where the model implements ILogicallyDeleted
and IsDeleted
then this acts as if not found and returns a null
.
Creates (CreateAsync
or CreateWithResultAsync
) the entity by firstly mapping to the model. Uses the Container.CreateItemAsync
internally to create.
Where the entity implements IChangeLogAuditLog
generally via ChangeLog
or ChangeLogEx
, then the CreatedBy
and CreatedDate
properties will be automatically set from the ExecutionContext
.
Where the entity and/or model implements ITenantId
then the TenantId
property will be automatically set from the ExecutionContext
.
The inserted model is then re-mapped to the entity and returned; this will ensure all properties updated as part of the insert are included in the refreshed entity.
Updates (UpdateAsync
or UpdateWithResultAsync
) the entity by firstly mapping to the model. Uses the Container.ReplaceItemAsync
internally to update.
First will check existence of the model by performing a Container.ReadItemAsync
. Where the data is not found, then a NotFoundException
will be thrown. Where the model implements ILogicallyDeleted
and IsDeleted
then this acts as if not found and will also result in a NotFoundException
.
Where the entity implements IETag
this will be checked against the just read version, and where not matched a ConcurrencyException
will be thrown. Also, any CosmosException
with a HttpStatusCode.PreconditionFailed
thrown will be converted to a corresponding ConcurrencyException
for consistency.
Where the entity implements IChangeLogAuditLog
generally via ChangeLog
or ChangeLogEx
, then the UpdatedBy
and UpdatedDate
properties will be automatically set from the ExecutionContext
.
Where the entity and/or model implements ITenantId
then the TenantId
property will be automatically set from the ExecutionContext
.
The updated model is then re-mapped to the entity and returned; this will ensure all properties updated as part of the update are included in the refreshed entity.
Deletes (DeleteAsync
or DeleteWithResultAsync
) the entity/model either physically or logically.
First will check existence of the model by performing a Container.ReadItemAsync
. Where the data is not found, then a NotFoundException
will be thrown. Where the model implements ILogicallyDeleted
and IsDeleted
then this acts as if not found and will also result in a NotFoundException
.
Where the model implements ILogicallyDeleted
then an update will occur after setting IsDeleted
to true
. Uses the Container.ReplaceItemAsync
internally to update.
Otherwise, will physically delete. Uses the Container.DeleteItemAsync
internally to delete.
Review the unit tests and/or Beef Cdr.Banking sample implementation.