From c6da97ebdf7ae2c6f61fd4c9c7206b258aba6a72 Mon Sep 17 00:00:00 2001 From: "Eric Sibly [chullybun]" Date: Tue, 25 Jun 2024 12:39:15 -0700 Subject: [PATCH] v3.21.0 (#105) - *Enhancement*: `CoreEx.Cosmos` improvements: - Added `CosmosDbArgs` to `CosmosDbContainerBase` to allow per container configuration where required. - Partition key specification centralized into `CosmosDbArgs`. - `ITenantId` and `ILogicallyDeleted` support integrated into `CosmosDbContainerBase`, etc. to offer consistent behavior with `EfDb`. --- CHANGELOG.md | 6 +++ Common.targets | 2 +- src/CoreEx.Cosmos/CosmosDb.cs | 24 ++-------- src/CoreEx.Cosmos/CosmosDbArgs.cs | 22 ++++++++- src/CoreEx.Cosmos/CosmosDbContainer.cs | 46 ++++++++++++------ src/CoreEx.Cosmos/CosmosDbContainerBase.cs | 45 +++++++++++------- src/CoreEx.Cosmos/CosmosDbModelQuery.cs | 6 +++ src/CoreEx.Cosmos/CosmosDbQuery.cs | 6 +++ src/CoreEx.Cosmos/CosmosDbValueContainer.cs | 47 ++++++++++++++----- src/CoreEx.Cosmos/CosmosDbValueModelQuery.cs | 6 +++ src/CoreEx.Cosmos/CosmosDbValueQuery.cs | 6 +++ src/CoreEx.Cosmos/CosmosExtensions.cs | 1 - src/CoreEx.Cosmos/ICosmosDb.cs | 12 ++--- .../CosmosDbContainerPartitioningTest.cs | 2 +- 14 files changed, 157 insertions(+), 74 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b2079f4..27059e9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ Represents the **NuGet** versions. +## v3.21.0 +- *Enhancement*: `CoreEx.Cosmos` improvements: + - Added `CosmosDbArgs` to `CosmosDbContainerBase` to allow per container configuration where required. + - Partition key specification centralized into `CosmosDbArgs`. + - `ITenantId` and `ILogicallyDeleted` support integrated into `CosmosDbContainerBase`, etc. to offer consistent behavior with `EfDb`. + ## v3.20.0 - *Fixed*: Include all constructor parameters when using `AddReferenceDataOrchestrator`. - *Enhancement*: Integrated dynamic `ITenantId` filtering into `EfDb` (controlled with `EfDbArgs`). diff --git a/Common.targets b/Common.targets index 92d61ec5..a3bbb76b 100644 --- a/Common.targets +++ b/Common.targets @@ -1,6 +1,6 @@  - 3.20.0 + 3.21.0 preview Avanade Avanade diff --git a/src/CoreEx.Cosmos/CosmosDb.cs b/src/CoreEx.Cosmos/CosmosDb.cs index fb691ef1..8e9711eb 100644 --- a/src/CoreEx.Cosmos/CosmosDb.cs +++ b/src/CoreEx.Cosmos/CosmosDb.cs @@ -22,7 +22,6 @@ public class CosmosDb(Database database, IMapper mapper, CosmosDbInvoker? invoke private Action? _updateRequestOptionsAction; private Action? _updateQueryRequestOptionsAction; private readonly ConcurrentDictionary> _filters = new(); - private PartitionKey? _partitionKey; /// /// Provides key as combination of model type and container identifier. @@ -43,32 +42,17 @@ private readonly struct Key(Type modelType, string containerId) /// public CosmosDbInvoker Invoker { get; } = invoker ?? (_invoker ??= new CosmosDbInvoker()); - /// - public virtual PartitionKey? PartitionKey => _partitionKey; - /// public CosmosDbArgs DbArgs { get; set; } = new CosmosDbArgs(); - /// - /// Uses (sets) the . - /// - /// The . - /// The instance to support fluent-style method-chaining. - /// As the property can be overridden by an inheritor this may have no affect. - public CosmosDb UsePartitionKey(PartitionKey? partitionKey) - { - _partitionKey = partitionKey; - return this; - } - /// public Container GetCosmosContainer(string containerId) => Database.GetContainer(containerId); /// - public CosmosDbContainer Container(string containerId) where T : class, IEntityKey, new() where TModel : class, IIdentifier, new() => new(this, containerId); + public CosmosDbContainer Container(string containerId, CosmosDbArgs? dbArgs = null) where T : class, IEntityKey, new() where TModel : class, IIdentifier, new() => new(this, containerId, dbArgs); /// - public CosmosDbValueContainer ValueContainer(string containerId) where T : class, IEntityKey, new() where TModel : class, IIdentifier, new() => new(this, containerId); + public CosmosDbValueContainer ValueContainer(string containerId, CosmosDbArgs? dbArgs = null) where T : class, IEntityKey, new() where TModel : class, IIdentifier, new() => new(this, containerId, dbArgs); /// public CosmosDbModelQuery ModelQuery(string containerId, CosmosDbArgs dbArgs, Func, IQueryable>? query) where TModel : class, IIdentifier, new() @@ -143,7 +127,7 @@ public CosmosDb QueryRequestOptions(Action updateQueryReque QueryRequestOptions ICosmosDb.GetQueryRequestOptions(CosmosDbArgs dbArgs) where T : class where TModel : class { var ro = dbArgs.QueryRequestOptions ?? new QueryRequestOptions(); - ro.PartitionKey ??= dbArgs.PartitionKey ?? PartitionKey; + ro.PartitionKey ??= dbArgs.PartitionKey ?? DbArgs.PartitionKey; UpdateQueryRequestOptions(ro); return ro; @@ -153,7 +137,7 @@ QueryRequestOptions ICosmosDb.GetQueryRequestOptions(CosmosDbArgs dbA QueryRequestOptions ICosmosDb.GetQueryRequestOptions(CosmosDbArgs dbArgs) where TModel : class { var ro = dbArgs.QueryRequestOptions ?? new QueryRequestOptions(); - ro.PartitionKey ??= dbArgs.PartitionKey ?? PartitionKey; + ro.PartitionKey ??= dbArgs.PartitionKey ?? DbArgs.PartitionKey; UpdateQueryRequestOptions(ro); return ro; diff --git a/src/CoreEx.Cosmos/CosmosDbArgs.cs b/src/CoreEx.Cosmos/CosmosDbArgs.cs index c2b0365b..baf560b7 100644 --- a/src/CoreEx.Cosmos/CosmosDbArgs.cs +++ b/src/CoreEx.Cosmos/CosmosDbArgs.cs @@ -1,6 +1,7 @@ // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/CoreEx using Microsoft.Azure.Cosmos; +using System; using System.Net; namespace CoreEx.Cosmos @@ -26,8 +27,17 @@ public CosmosDbArgs(CosmosDbArgs template, PartitionKey? partitionKey = null) ItemRequestOptions = template.ItemRequestOptions; QueryRequestOptions = template.QueryRequestOptions; NullOnNotFound = template.NullOnNotFound; + CleanUpResult = template.CleanUpResult; + FilterByTenantId = template.FilterByTenantId; + GetTenantId = template.GetTenantId; } + /// + /// Initializes a new instance of the struct. + /// + /// The . + public CosmosDbArgs(PartitionKey partitionKey) => PartitionKey = partitionKey; + /// /// Initializes a new instance of the struct for Get, Create, Update, and Delete operations with the specified . /// @@ -51,7 +61,7 @@ public CosmosDbArgs(QueryRequestOptions requestOptions, PartitionKey? partitionK } /// - /// Gets or sets the . + /// Gets the . /// public PartitionKey? PartitionKey { get; } = null; @@ -74,5 +84,15 @@ public CosmosDbArgs(QueryRequestOptions requestOptions, PartitionKey? partitionK /// Indicates whether the result should be cleaned up. /// public bool CleanUpResult { get; set; } = false; + + /// + /// Indicates that when the underlying model implements it is to be filtered by the corresponding value. Defaults to true. + /// + public bool FilterByTenantId { get; set; } + + /// + /// Gets or sets the get tenant identifier function; defaults to . + /// + public Func GetTenantId { get; set; } = () => ExecutionContext.HasCurrent ? ExecutionContext.Current.TenantId : null; } } \ No newline at end of file diff --git a/src/CoreEx.Cosmos/CosmosDbContainer.cs b/src/CoreEx.Cosmos/CosmosDbContainer.cs index c3543b41..cc6e0146 100644 --- a/src/CoreEx.Cosmos/CosmosDbContainer.cs +++ b/src/CoreEx.Cosmos/CosmosDbContainer.cs @@ -19,7 +19,8 @@ namespace CoreEx.Cosmos /// The cosmos model . /// The . /// The identifier. - public class CosmosDbContainer(ICosmosDb cosmosDb, string containerId) : CosmosDbContainerBase>(cosmosDb, containerId) where T : class, IEntityKey, new() where TModel : class, IIdentifier, new() + /// The optional . + public class CosmosDbContainer(ICosmosDb cosmosDb, string containerId, CosmosDbArgs? dbArgs = null) : CosmosDbContainerBase>(cosmosDb, containerId, dbArgs) where T : class, IEntityKey, new() where TModel : class, IIdentifier, new() { /// /// Gets the value from the response updating any special properties as required. @@ -45,11 +46,11 @@ internal T GetValue(TModel model) if (val is IETag et && et.ETag != null) et.ETag = ETagGenerator.ParseETag(et.ETag); - return CosmosDb.DbArgs.CleanUpResult ? Cleaner.Clean(val) : val; + return DbArgs.CleanUpResult ? Cleaner.Clean(val) : val; } /// - /// Check the value to determine whether users are authorised using the CosmosDbArgs.AuthorizationFilter. + /// Check the value to determine whether users are authorised using the CosmosDb.AuthorizationFilter. /// private Result CheckAuthorized(TModel model) { @@ -68,7 +69,7 @@ private Result CheckAuthorized(TModel model) /// /// The function to perform additional query execution. /// The . - public CosmosDbQuery Query(Func, IQueryable>? query) => Query(new CosmosDbArgs(CosmosDb.DbArgs), query); + public CosmosDbQuery Query(Func, IQueryable>? query) => Query(new CosmosDbArgs(DbArgs), query); /// /// Gets (creates) a to enable LINQ-style queries. @@ -76,7 +77,7 @@ private Result CheckAuthorized(TModel model) /// The . /// The function to perform additional query execution. /// The . - public CosmosDbQuery Query(PartitionKey? partitionKey = null, Func, IQueryable>? query = null) => Query(new CosmosDbArgs(CosmosDb.DbArgs, partitionKey), query); + public CosmosDbQuery Query(PartitionKey? partitionKey = null, Func, IQueryable>? query = null) => Query(new CosmosDbArgs(DbArgs, partitionKey), query); /// /// Gets (creates) a to enable LINQ-style queries. @@ -91,7 +92,7 @@ private Result CheckAuthorized(TModel model) /// /// The function to perform additional query execution. /// The . - public CosmosDbModelQuery ModelQuery(Func, IQueryable>? query) => ModelQuery(new CosmosDbArgs(CosmosDb.DbArgs), query); + public CosmosDbModelQuery ModelQuery(Func, IQueryable>? query) => ModelQuery(new CosmosDbArgs(DbArgs), query); /// /// Gets (creates) a to enable LINQ-style queries. @@ -99,7 +100,7 @@ private Result CheckAuthorized(TModel model) /// The . /// The function to perform additional query execution. /// The . - public CosmosDbModelQuery ModelQuery(PartitionKey? partitionKey = null, Func, IQueryable>? query = null) => ModelQuery(new CosmosDbArgs(CosmosDb.DbArgs, partitionKey), query); + public CosmosDbModelQuery ModelQuery(PartitionKey? partitionKey = null, Func, IQueryable>? query = null) => ModelQuery(new CosmosDbArgs(DbArgs, partitionKey), query); /// /// Gets (creates) a to enable LINQ-style queries. @@ -114,10 +115,13 @@ private Result CheckAuthorized(TModel model) { try { - var val = await Container.ReadItemAsync(key, args.PartitionKey ?? CosmosDb.PartitionKey ?? PartitionKey.None, CosmosDb.GetItemRequestOptions(args), ct).ConfigureAwait(false); - return Result.Go(CheckAuthorized(val)).ThenAs(() => GetResponseValue(val)); + var resp = await Container.ReadItemAsync(key, args.PartitionKey ?? DbArgs.PartitionKey ?? PartitionKey.None, CosmosDb.GetItemRequestOptions(args), ct).ConfigureAwait(false); + if (resp.Resource == null || (args.FilterByTenantId && resp.Resource is ITenantId tenantId && tenantId.TenantId != DbArgs.GetTenantId()) || (resp.Resource is ILogicallyDeleted ld && ld.IsDeleted.HasValue && ld.IsDeleted.Value)) + return args.NullOnNotFound ? Result.None : Result.NotFoundError(); + + return Result.Go(CheckAuthorized(resp)).ThenAs(() => GetResponseValue(resp)); } - catch (CosmosException dcex) when (args.NullOnNotFound && dcex.StatusCode == System.Net.HttpStatusCode.NotFound) { return Result.None; } + catch (CosmosException dcex) when (args.NullOnNotFound && dcex.StatusCode == System.Net.HttpStatusCode.NotFound) { return args.NullOnNotFound ? Result.None : Result.NotFoundError(); } }, cancellationToken, nameof(GetWithResultAsync)); /// @@ -148,7 +152,7 @@ public override Task> UpdateWithResultAsync(T value, CosmosDbArgs dbAr // Must read existing to update. var resp = await Container.ReadItemAsync(key, pk, ro, ct).ConfigureAwait(false); - if (resp.Resource == null) + if (resp.Resource == null || (args.FilterByTenantId && resp.Resource is ITenantId tenantId && tenantId.TenantId != DbArgs.GetTenantId()) || (resp.Resource is ILogicallyDeleted ld && ld.IsDeleted.HasValue && ld.IsDeleted.Value)) return Result.NotFoundError(); return await Result @@ -178,10 +182,24 @@ public override Task DeleteWithResultAsync(object? id, CosmosDbArgs dbAr { // Must read the existing to validate. var ro = CosmosDb.GetItemRequestOptions(args); - var pk = dbArgs.PartitionKey ?? CosmosDb.PartitionKey ?? PartitionKey.None; + var pk = dbArgs.PartitionKey ?? DbArgs.PartitionKey ?? PartitionKey.None; var resp = await Container.ReadItemAsync(key, pk, ro, ct).ConfigureAwait(false); - if (resp?.Resource == null) - return Result.NotFoundError(); + if (resp.Resource == null || (args.FilterByTenantId && resp.Resource is ITenantId tenantId && tenantId.TenantId != DbArgs.GetTenantId()) || (resp.Resource is ILogicallyDeleted ld && ld.IsDeleted.HasValue && ld.IsDeleted.Value)) + return Result.Success; + + // Delete; either logically or physically. + if (resp.Resource is ILogicallyDeleted ild) + { + ild.IsDeleted = true; + return await Result + .Go(CheckAuthorized(resp.Resource)) + .ThenAsync(async () => + { + ro.SessionToken = resp.Headers?.Session; + await Container.ReplaceItemAsync(resp.Resource, key, pk, ro, ct).ConfigureAwait(false); + return Result.Success; + }); + } return await Result .Go(CheckAuthorized(resp.Resource)) diff --git a/src/CoreEx.Cosmos/CosmosDbContainerBase.cs b/src/CoreEx.Cosmos/CosmosDbContainerBase.cs index cf2d362b..aaed26c1 100644 --- a/src/CoreEx.Cosmos/CosmosDbContainerBase.cs +++ b/src/CoreEx.Cosmos/CosmosDbContainerBase.cs @@ -17,8 +17,10 @@ namespace CoreEx.Cosmos /// The itself. /// The . /// The identifier. - public abstract class CosmosDbContainerBase(ICosmosDb cosmosDb, string containerId) : ICosmosDbContainer where T : class, IEntityKey, new() where TModel : class, IIdentifier, new() where TSelf : CosmosDbContainerBase + /// The optional . + public abstract class CosmosDbContainerBase(ICosmosDb cosmosDb, string containerId, CosmosDbArgs? dbArgs = null) : ICosmosDbContainer where T : class, IEntityKey, new() where TModel : class, IIdentifier, new() where TSelf : CosmosDbContainerBase { + private CosmosDbArgs? _dbArgs = dbArgs; private Func? _partitionKey; /// @@ -28,15 +30,25 @@ namespace CoreEx.Cosmos public Container Container { get; } = cosmosDb.GetCosmosContainer(containerId); /// - /// Gets the from the . + /// Gets or sets the Container-specific . + /// + /// Defaults to on first access. + public CosmosDbArgs DbArgs + { + get => _dbArgs ??= new CosmosDbArgs(CosmosDb.DbArgs); + set => _dbArgs = value; + } + + /// + /// Gets the from the (only Create and Update operations). /// /// The value to infer from. /// The . - /// Will be thrown where the infered is not equal to (where not null). + /// Will be thrown where the infered is not equal to (where not null). public PartitionKey GetPartitionKey(T value) { - var dbpk = CosmosDb.PartitionKey; - var pk = _partitionKey?.Invoke(value) ?? CosmosDb.PartitionKey ?? PartitionKey.None; + var dbpk = DbArgs.PartitionKey; + var pk = _partitionKey?.Invoke(value) ?? DbArgs.PartitionKey ?? PartitionKey.None; if (dbpk is not null && dbpk != PartitionKey.None && dbpk != pk) throw new AuthorizationException(); @@ -44,10 +56,11 @@ public PartitionKey GetPartitionKey(T value) } /// - /// Sets the function to determine the ; used for . + /// Sets the function to determine the ; used for (only Create and Update operations). /// /// The function to determine the . /// The instance to support fluent-style method-chaining. + /// This is used where there is a value and the corresponding needs to be dynamically determined. public TSelf UsePartitionKey(Func partitionKey) { _partitionKey = partitionKey; @@ -91,13 +104,13 @@ public TSelf UsePartitionKey(Func partitionKey) /// The identifier. /// The . /// The entity value where found; otherwise, null (see ). - public Task> GetWithResultAsync(object? id, CancellationToken cancellationToken = default) => GetWithResultAsync(id, new CosmosDbArgs(CosmosDb.DbArgs), cancellationToken); + public Task> GetWithResultAsync(object? id, CancellationToken cancellationToken = default) => GetWithResultAsync(id, new CosmosDbArgs(DbArgs), cancellationToken); /// /// Gets the entity for the specified . /// /// The identifier. - /// The . Defaults to . + /// The . Defaults to . /// The . /// The entity value where found; otherwise, null (see ). public async Task GetAsync(object? id, PartitionKey? partitionKey, CancellationToken cancellationToken = default) => await GetWithResultAsync(id, partitionKey, cancellationToken).ConfigureAwait(false); @@ -106,10 +119,10 @@ public TSelf UsePartitionKey(Func partitionKey) /// Gets the entity for the specified with a . /// /// The identifier. - /// The . Defaults to . + /// The . Defaults to . /// The . /// The entity value where found; otherwise, null (see ). - public Task> GetWithResultAsync(object? id, PartitionKey? partitionKey, CancellationToken cancellationToken = default) => GetWithResultAsync(id, new CosmosDbArgs(CosmosDb.DbArgs, partitionKey), cancellationToken); + public Task> GetWithResultAsync(object? id, PartitionKey? partitionKey, CancellationToken cancellationToken = default) => GetWithResultAsync(id, new CosmosDbArgs(DbArgs, partitionKey), cancellationToken); /// /// Gets the entity for the specified . @@ -143,7 +156,7 @@ public TSelf UsePartitionKey(Func partitionKey) /// The value to create. /// The . /// The created value. - public Task> CreateWithResultAsync(T value, CancellationToken cancellationToken = default) => CreateWithResultAsync(value, new CosmosDbArgs(CosmosDb.DbArgs), cancellationToken); + public Task> CreateWithResultAsync(T value, CancellationToken cancellationToken = default) => CreateWithResultAsync(value, new CosmosDbArgs(DbArgs), cancellationToken); /// /// Creates the entity. @@ -177,7 +190,7 @@ public TSelf UsePartitionKey(Func partitionKey) /// The value to update. /// The . /// The updated value. - public Task> UpdateWithResultAsync(T value, CancellationToken cancellationToken = default) => UpdateWithResultAsync(value, new CosmosDbArgs(CosmosDb.DbArgs), cancellationToken); + public Task> UpdateWithResultAsync(T value, CancellationToken cancellationToken = default) => UpdateWithResultAsync(value, new CosmosDbArgs(DbArgs), cancellationToken); /// /// Updates the entity. @@ -211,13 +224,13 @@ public TSelf UsePartitionKey(Func partitionKey) /// The identifier. /// The . /// The entity value where found; otherwise, null (see ). - public Task DeleteWithResultAsync(object? id, CancellationToken cancellationToken = default) => DeleteWithResultAsync(id, new CosmosDbArgs(CosmosDb.DbArgs), cancellationToken); + public Task DeleteWithResultAsync(object? id, CancellationToken cancellationToken = default) => DeleteWithResultAsync(id, new CosmosDbArgs(DbArgs), cancellationToken); /// /// Deletes the entity for the specified . /// /// The identifier. - /// The . Defaults to . + /// The . Defaults to . /// The . /// The entity value where found; otherwise, null (see ). public async Task DeleteAsync(object? id, PartitionKey? partitionKey, CancellationToken cancellationToken = default) => (await DeleteWithResultAsync(id, partitionKey, cancellationToken).ConfigureAwait(false)).ThrowOnError(); @@ -226,10 +239,10 @@ public TSelf UsePartitionKey(Func partitionKey) /// Deletes the entity for the specified with a . /// /// The identifier. - /// The . Defaults to . + /// The . Defaults to . /// The . /// The entity value where found; otherwise, null (see ). - public Task DeleteWithResultAsync(object? id, PartitionKey? partitionKey, CancellationToken cancellationToken = default) => DeleteWithResultAsync(id, new CosmosDbArgs(CosmosDb.DbArgs, partitionKey), cancellationToken); + public Task DeleteWithResultAsync(object? id, PartitionKey? partitionKey, CancellationToken cancellationToken = default) => DeleteWithResultAsync(id, new CosmosDbArgs(DbArgs, partitionKey), cancellationToken); /// /// Deletes the entity for the specified . diff --git a/src/CoreEx.Cosmos/CosmosDbModelQuery.cs b/src/CoreEx.Cosmos/CosmosDbModelQuery.cs index a79fd8e4..73b72158 100644 --- a/src/CoreEx.Cosmos/CosmosDbModelQuery.cs +++ b/src/CoreEx.Cosmos/CosmosDbModelQuery.cs @@ -36,6 +36,12 @@ private IQueryable AsQueryable(bool allowSynchronousQueryExecution, bool if (filter != null) query = (IQueryable)filter(query); + if (QueryArgs.FilterByTenantId && typeof(ITenantId).IsAssignableFrom(typeof(TModel))) + query = query.Where(x => ((ITenantId)x).TenantId == QueryArgs.GetTenantId()); + + if (typeof(ILogicallyDeleted).IsAssignableFrom(typeof(TModel))) + query = query.Where(x => !((ILogicallyDeleted)x).IsDeleted.IsDefined() || ((ILogicallyDeleted)x).IsDeleted == null || ((ILogicallyDeleted)x).IsDeleted == false); + return query; } diff --git a/src/CoreEx.Cosmos/CosmosDbQuery.cs b/src/CoreEx.Cosmos/CosmosDbQuery.cs index 84393532..86703fe2 100644 --- a/src/CoreEx.Cosmos/CosmosDbQuery.cs +++ b/src/CoreEx.Cosmos/CosmosDbQuery.cs @@ -42,6 +42,12 @@ private IQueryable AsQueryable(bool allowSynchronousQueryExecution, bool if (filter != null) query = (IQueryable)filter(query); + if (QueryArgs.FilterByTenantId && typeof(ITenantId).IsAssignableFrom(typeof(TModel))) + query = query.Where(x => ((ITenantId)x).TenantId == QueryArgs.GetTenantId()); + + if (typeof(ILogicallyDeleted).IsAssignableFrom(typeof(TModel))) + query = query.Where(x => !((ILogicallyDeleted)x).IsDeleted.IsDefined() || ((ILogicallyDeleted)x).IsDeleted == null || ((ILogicallyDeleted)x).IsDeleted == false); + return query; } diff --git a/src/CoreEx.Cosmos/CosmosDbValueContainer.cs b/src/CoreEx.Cosmos/CosmosDbValueContainer.cs index 1adcf8d8..6e7088aa 100644 --- a/src/CoreEx.Cosmos/CosmosDbValueContainer.cs +++ b/src/CoreEx.Cosmos/CosmosDbValueContainer.cs @@ -20,7 +20,8 @@ namespace CoreEx.Cosmos /// Represents a special-purpose CosmosDb that houses an underlying , including name, and flexible , for persistence. /// The . /// The identifier. - public class CosmosDbValueContainer(ICosmosDb cosmosDb, string containerId) : CosmosDbContainerBase>(cosmosDb, containerId) where T : class, IEntityKey, new() where TModel : class, IIdentifier, new() + /// The optional . + public class CosmosDbValueContainer(ICosmosDb cosmosDb, string containerId, CosmosDbArgs? dbArgs = null) : CosmosDbContainerBase>(cosmosDb, containerId, dbArgs) where T : class, IEntityKey, new() where TModel : class, IIdentifier, new() { private readonly string _typeName = typeof(TModel).Name; @@ -46,11 +47,14 @@ internal T GetValue(CosmosDbValue model) { ((ICosmosDbValue)model).PrepareAfter(CosmosDb); var val = CosmosDb.Mapper.Map(model.Value, OperationTypes.Get)!; - return CosmosDb.DbArgs.CleanUpResult ? Cleaner.Clean(val) : val; + if (val is IETag et && et.ETag != null) + et.ETag = ETagGenerator.ParseETag(et.ETag); + + return DbArgs.CleanUpResult ? Cleaner.Clean(val) : val; } /// - /// Check the value to determine whether users are authorised using the CosmosDbArgs.AuthorizationFilter. + /// Check the value to determine whether users are authorised using the CosmosDb.AuthorizationFilter. /// private Result CheckAuthorized(CosmosDbValue model) { @@ -69,7 +73,7 @@ private Result CheckAuthorized(CosmosDbValue model) /// /// The function to perform additional query execution. /// The . - public CosmosDbValueQuery Query(Func>, IQueryable>>? query) => Query(new CosmosDbArgs(CosmosDb.DbArgs), query); + public CosmosDbValueQuery Query(Func>, IQueryable>>? query) => Query(new CosmosDbArgs(DbArgs), query); /// /// Gets (creates) a to enable LINQ-style queries. @@ -77,7 +81,7 @@ private Result CheckAuthorized(CosmosDbValue model) /// The . /// The function to perform additional query execution. /// The . - public CosmosDbValueQuery Query(PartitionKey? partitionKey = null, Func>, IQueryable>>? query = null) => Query(new CosmosDbArgs(CosmosDb.DbArgs, partitionKey), query); + public CosmosDbValueQuery Query(PartitionKey? partitionKey = null, Func>, IQueryable>>? query = null) => Query(new CosmosDbArgs(DbArgs, partitionKey), query); /// /// Gets (creates) a to enable LINQ-style queries. @@ -92,7 +96,7 @@ private Result CheckAuthorized(CosmosDbValue model) /// /// The function to perform additional query execution. /// The . - public CosmosDbValueModelQuery ModelQuery(Func>, IQueryable>>? query) => ModelQuery(new CosmosDbArgs(CosmosDb.DbArgs), query); + public CosmosDbValueModelQuery ModelQuery(Func>, IQueryable>>? query) => ModelQuery(new CosmosDbArgs(DbArgs), query); /// /// Gets (creates) a to enable LINQ-style queries. @@ -100,7 +104,7 @@ private Result CheckAuthorized(CosmosDbValue model) /// The . /// The function to perform additional query execution. /// The . - public CosmosDbValueModelQuery ModelQuery(PartitionKey? partitionKey = null, Func>, IQueryable>>? query = null) => ModelQuery(new CosmosDbArgs(CosmosDb.DbArgs, partitionKey), query); + public CosmosDbValueModelQuery ModelQuery(PartitionKey? partitionKey = null, Func>, IQueryable>>? query = null) => ModelQuery(new CosmosDbArgs(DbArgs, partitionKey), query); /// /// Gets (creates) a to enable LINQ-style queries. @@ -115,7 +119,7 @@ private Result CheckAuthorized(CosmosDbValue model) { try { - var val = await Container.ReadItemAsync>(key, args.PartitionKey ?? CosmosDb.PartitionKey ?? PartitionKey.None, CosmosDb.GetItemRequestOptions(args), ct).ConfigureAwait(false); + var val = await Container.ReadItemAsync>(key, args.PartitionKey ?? DbArgs.PartitionKey ?? PartitionKey.None, CosmosDb.GetItemRequestOptions(args), ct).ConfigureAwait(false); // Check that the TypeName is the same. if (val?.Resource == null || val.Resource.Type != _typeName) @@ -125,7 +129,7 @@ private Result CheckAuthorized(CosmosDbValue model) .Go(CheckAuthorized(val)) .ThenAs(() => GetResponseValue(val)); } - catch (CosmosException dcex) when (args.NullOnNotFound && dcex.StatusCode == System.Net.HttpStatusCode.NotFound) { return Result.None; } + catch (CosmosException dcex) when (args.NullOnNotFound && dcex.StatusCode == System.Net.HttpStatusCode.NotFound) { return args.NullOnNotFound ? Result.None : Result.NotFoundError(); } }, cancellationToken, nameof(GetWithResultAsync)); /// @@ -163,6 +167,9 @@ public override Task> UpdateWithResultAsync(T value, CosmosDbArgs dbAr if (resp?.Resource == null || resp.Resource.Type != _typeName) return Result.NotFoundError(); + if ((args.FilterByTenantId && resp.Resource is ITenantId tenantId && tenantId.TenantId != DbArgs.GetTenantId()) || (resp.Resource is ILogicallyDeleted ld && ld.IsDeleted.HasValue && ld.IsDeleted.Value)) + return Result.NotFoundError(); + return await Result .Go(CheckAuthorized(resp.Resource)) .When(() => v is IETag etag2 && etag2.ETag != null && ETagGenerator.FormatETag(etag2.ETag) != resp.ETag, () => Result.ConcurrencyError()) @@ -190,10 +197,27 @@ public override Task DeleteWithResultAsync(object? id, CosmosDbArgs dbAr { // Must read existing to delete and to make sure we are deleting for the correct Type; don't just trust the key. var ro = CosmosDb.GetItemRequestOptions(args); - var pk = dbArgs.PartitionKey ?? CosmosDb.PartitionKey ?? PartitionKey.None; + var pk = dbArgs.PartitionKey ?? DbArgs.PartitionKey ?? PartitionKey.None; var resp = await Container.ReadItemAsync>(key, pk, ro, ct).ConfigureAwait(false); if (resp?.Resource == null || resp.Resource.Type != _typeName) - return Result.NotFoundError(); + return Result.Success; + + if ((args.FilterByTenantId && resp.Resource is ITenantId tenantId && tenantId.TenantId != DbArgs.GetTenantId()) || (resp.Resource is ILogicallyDeleted ld && ld.IsDeleted.HasValue && ld.IsDeleted.Value)) + return Result.Success; + + // Delete; either logically or physically. + if (resp.Resource is ILogicallyDeleted ild) + { + ild.IsDeleted = true; + return await Result + .Go(CheckAuthorized(resp.Resource)) + .ThenAsync(async () => + { + ro.SessionToken = resp.Headers?.Session; + await Container.ReplaceItemAsync(resp.Resource, key, pk, ro, ct).ConfigureAwait(false); + return Result.Success; + }); + } return await Result .Go(CheckAuthorized(resp.Resource)) @@ -201,7 +225,6 @@ public override Task DeleteWithResultAsync(object? id, CosmosDbArgs dbAr { ro.SessionToken = resp.Headers?.Session; await Container.DeleteItemAsync(key, pk, ro, ct).ConfigureAwait(false); - return Result.Success; }); } diff --git a/src/CoreEx.Cosmos/CosmosDbValueModelQuery.cs b/src/CoreEx.Cosmos/CosmosDbValueModelQuery.cs index c17d27a9..5408cd23 100644 --- a/src/CoreEx.Cosmos/CosmosDbValueModelQuery.cs +++ b/src/CoreEx.Cosmos/CosmosDbValueModelQuery.cs @@ -36,6 +36,12 @@ private IQueryable> AsQueryable(bool allowSynchronousQuery if (filter != null) query = (IQueryable>)filter(query); + if (QueryArgs.FilterByTenantId && typeof(ITenantId).IsAssignableFrom(typeof(TModel))) + query = query.Where(x => ((ITenantId)x.Value).TenantId == QueryArgs.GetTenantId()); + + if (typeof(ILogicallyDeleted).IsAssignableFrom(typeof(TModel))) + query = query.Where(x => !((ILogicallyDeleted)x.Value).IsDeleted.IsDefined() || ((ILogicallyDeleted)x.Value).IsDeleted == null || ((ILogicallyDeleted)x.Value).IsDeleted == false); + return query; } diff --git a/src/CoreEx.Cosmos/CosmosDbValueQuery.cs b/src/CoreEx.Cosmos/CosmosDbValueQuery.cs index 51474221..66a2d123 100644 --- a/src/CoreEx.Cosmos/CosmosDbValueQuery.cs +++ b/src/CoreEx.Cosmos/CosmosDbValueQuery.cs @@ -42,6 +42,12 @@ private IQueryable> AsQueryable(bool allowSynchronousQuery if (filter != null) query = (IQueryable>)filter(query); + if (QueryArgs.FilterByTenantId && typeof(ITenantId).IsAssignableFrom(typeof(TModel))) + query = query.Where(x => ((ITenantId)x.Value).TenantId == QueryArgs.GetTenantId()); + + if (typeof(ILogicallyDeleted).IsAssignableFrom(typeof(TModel))) + query = query.Where(x => !((ILogicallyDeleted)x.Value).IsDeleted.IsDefined() || ((ILogicallyDeleted)x.Value).IsDeleted == null || ((ILogicallyDeleted)x.Value).IsDeleted == false); + return query; } diff --git a/src/CoreEx.Cosmos/CosmosExtensions.cs b/src/CoreEx.Cosmos/CosmosExtensions.cs index 57625b5a..8eba5f8f 100644 --- a/src/CoreEx.Cosmos/CosmosExtensions.cs +++ b/src/CoreEx.Cosmos/CosmosExtensions.cs @@ -1,7 +1,6 @@ // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/CoreEx using Microsoft.Azure.Cosmos; -using System; using System.Threading; using System.Threading.Tasks; diff --git a/src/CoreEx.Cosmos/ICosmosDb.cs b/src/CoreEx.Cosmos/ICosmosDb.cs index 3f8eb9ae..90fc011c 100644 --- a/src/CoreEx.Cosmos/ICosmosDb.cs +++ b/src/CoreEx.Cosmos/ICosmosDb.cs @@ -34,12 +34,6 @@ public interface ICosmosDb /// CosmosDbArgs DbArgs { get; } - /// - /// Gets the default . - /// - /// Where null and the underlying CosmosDb capability requires then will be used. - PartitionKey? PartitionKey { get; } - /// /// Gets the specified . /// @@ -53,8 +47,9 @@ public interface ICosmosDb /// The entity . /// The cosmos model . /// The identifier. + /// The . /// The . - CosmosDbContainer Container(string containerId) where T : class, IEntityKey, new() where TModel : class, IIdentifier, new(); + CosmosDbContainer Container(string containerId, CosmosDbArgs? dbArgs = null) where T : class, IEntityKey, new() where TModel : class, IIdentifier, new(); /// /// Gets (creates) the for the specified . @@ -62,8 +57,9 @@ public interface ICosmosDb /// The entity . /// The cosmos model . /// The identifier. + /// The . /// The . - CosmosDbValueContainer ValueContainer(string containerId) where T : class, IEntityKey, new() where TModel : class, IIdentifier, new(); + CosmosDbValueContainer ValueContainer(string containerId, CosmosDbArgs? dbArgs = null) where T : class, IEntityKey, new() where TModel : class, IIdentifier, new(); /// /// Gets (creates) the for the specified . diff --git a/tests/CoreEx.Cosmos.Test/CosmosDbContainerPartitioningTest.cs b/tests/CoreEx.Cosmos.Test/CosmosDbContainerPartitioningTest.cs index 2fe7b3d8..e7684de5 100644 --- a/tests/CoreEx.Cosmos.Test/CosmosDbContainerPartitioningTest.cs +++ b/tests/CoreEx.Cosmos.Test/CosmosDbContainerPartitioningTest.cs @@ -13,7 +13,7 @@ public async Task SetUp() { await TestSetUp.SetUpAsync("/filter", "/value/filter").ConfigureAwait(false); _db = new CosmosDb(auth: false, partitioning: true); - _db.UsePartitionKey(new PartitionKey("A")); + _db.DbArgs = new CosmosDbArgs(new PartitionKey("A")); _db.Persons1.UsePartitionKey(p => new PartitionKey(p.Filter)); _db.Persons2.UsePartitionKey(p => new PartitionKey(p.Filter)); _db.Persons3.UsePartitionKey(p => new PartitionKey(p.Filter));