Repository navigation
Expand file tree
/
Copy pathCosmosDbReferenceData.cs
More file actions
157 lines (143 loc) · 11.4 KB
/
Copy pathCosmosDbReferenceData.cs
File metadata and controls
157 lines (143 loc) · 11.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
namespace CoreEx.Cosmos;
/// <summary>
/// Provides standardized <see cref="RefData.Abstractions.IReferenceData"/> operations for Cosmos DB.
/// </summary>
/// <remarks>This encapsulates common CRUD operations enabling consistent handling of reference data; the Cosmos DB equivalent of <c>EfDbReferenceData</c>.
/// <para>The reference data is assumed to be stored within a container (typically shared by all reference data types, differentiated by <see cref="ITypeDiscriminator"/>) using the default (no explicit)
/// partition key, as the identifier alone is used to get, update and delete the underlying document. Where an ambient <see cref="CosmosDbUnitOfWork"/> transaction is active the mutations are enlisted
/// and only executed when the unit-of-work completes; in which case the resulting <see cref="IETag.ETag"/> is not final until then (see <see cref="IUnitOfWork.SynchronizeETag{T}(CompositeKey, T)"/>).</para></remarks>
public static class CosmosDbReferenceData
{
/// <summary>
/// Creates the specified <paramref name="value"/> in the container.
/// </summary>
/// <typeparam name="TId">The identifier <see cref="Type"/>.</typeparam>
/// <typeparam name="TRef">The <see cref="IReferenceData{TId}"/> <see cref="Type"/>.</typeparam>
/// <typeparam name="TModel">The model <see cref="Type"/>.</typeparam>
/// <typeparam name="TMapper">The <see cref="IBiDirectionMapper{TSource, TDestination}"/> <see cref="Type"/>.</typeparam>
/// <param name="container">The <see cref="CosmosDbContainer{TModel}"/>.</param>
/// <param name="value">The value to create.</param>
/// <param name="mapper">The mapper.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The <see cref="DataResult{T}"/> containing the created value.</returns>
/// <remarks>The identifier is generated using <see cref="Runtime.GenerateIdentifierAsync{TId, TFor}"/> and stored as a <see cref="string"/>. A newly created value is always inactive. A duplicate <c>Code</c> (for the same type discriminator) results in a <see cref="DuplicateException"/>, but only where the container has a unique key policy covering <c>/typeDiscriminator</c> and <c>/code</c>; the container must also use <see cref="CosmosDbContainerOptions.WithReferenceDataOutboxEvent"/> (see <see cref="CosmosDbOptions.Container(string, Action{CosmosDbContainerOptions}?)"/>) so that the co-located outbox events do not themselves violate that unique key.</remarks>
public static async Task<Result<DataResult<TRef>>> CreateAsync<TId, TRef, TModel, TMapper>(CosmosDbContainer<TModel> container, TRef value, TMapper mapper, CancellationToken cancellationToken = default)
where TRef : class, IReferenceData<TId>
where TModel : CosmosDbReferenceDataModelBase, IEntityKey, new()
where TMapper : class, IBiDirectionMapper<TRef, TModel>
{
container.ThrowIfNull();
var model = mapper.To.Map(value)!;
model.Id = (await Runtime.GenerateIdentifierAsync<TId, TRef>().ConfigureAwait(false))?.ToString() ?? throw new InvalidOperationException("A generated identifier must not be null.");
model.IsActive = false;
var result = await container.CreateWithResultAsync(model, cancellationToken).ConfigureAwait(false);
return result.ThenAs(dr => new DataResult<TRef>(mapper.From.Map(dr.Value)!, dr.WasMutated));
}
/// <summary>
/// Updates the specified <paramref name="value"/> in the container.
/// </summary>
/// <typeparam name="TId">The identifier <see cref="Type"/>.</typeparam>
/// <typeparam name="TRef">The <see cref="IReferenceData{TId}"/> <see cref="Type"/>.</typeparam>
/// <typeparam name="TModel">The model <see cref="Type"/>.</typeparam>
/// <typeparam name="TMapper">The <see cref="IBiDirectionMapper{TSource, TDestination}"/> <see cref="Type"/>.</typeparam>
/// <param name="container">The <see cref="CosmosDbContainer{TModel}"/>.</param>
/// <param name="id">The identifier of the value to update.</param>
/// <param name="value">The value to update.</param>
/// <param name="mapper">The mapper.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The <see cref="DataResult{T}"/> containing the updated value.</returns>
/// <remarks>The <see cref="CosmosDbReferenceDataModelBase.Code"/> and active state are retained from the existing value. The optimistic concurrency check is performed using the <see cref="IETag.ETag"/> of the <paramref name="value"/>.</remarks>
public static async Task<Result<DataResult<TRef>>> UpdateAsync<TId, TRef, TModel, TMapper>(CosmosDbContainer<TModel> container, TId id, TRef value, TMapper mapper, CancellationToken cancellationToken = default)
where TRef : class, IReferenceData<TId>
where TModel : CosmosDbReferenceDataModelBase, IEntityKey, new()
where TMapper : class, IBiDirectionMapper<TRef, TModel>
{
container.ThrowIfNull();
var existing = await container.GetWithResultAsync(CompositeKey.Create(id), cancellationToken).ConfigureAwait(false);
if (existing.IsFailure)
return existing.AsResult();
var model = mapper.To.Map(value)!;
model.Id = existing.Value.Id;
model.Code = existing.Value.Code; // Code is immutable and cannot be updated.
model.IsActive = existing.Value.IsActive; // IsActive can only be updated via specific methods.
var result = await container.UpdateWithResultAsync(model, cancellationToken).ConfigureAwait(false);
return result.ThenAs(dr => new DataResult<TRef>(mapper.From.Map(dr.Value)!, dr.WasMutated));
}
/// <summary>
/// Activates the specified <paramref name="id"/> in the container.
/// </summary>
/// <typeparam name="TId">The identifier <see cref="Type"/>.</typeparam>
/// <typeparam name="TRef">The <see cref="IReferenceData{TId}"/> <see cref="Type"/>.</typeparam>
/// <typeparam name="TModel">The model <see cref="Type"/>.</typeparam>
/// <typeparam name="TMapper">The <see cref="IBiDirectionMapper{TSource, TDestination}"/> <see cref="Type"/>.</typeparam>
/// <param name="container">The <see cref="CosmosDbContainer{TModel}"/>.</param>
/// <param name="id">The identifier of the value to activate.</param>
/// <param name="mapper">The mapper.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The <see cref="DataResult{T}"/> containing the activated value.</returns>
public static Task<Result<DataResult<TRef>>> ActivateAsync<TId, TRef, TModel, TMapper>(CosmosDbContainer<TModel> container, TId id, TMapper mapper, CancellationToken cancellationToken = default)
where TRef : class, IReferenceData<TId>
where TModel : CosmosDbReferenceDataModelBase, IEntityKey, new()
where TMapper : class, IBiDirectionMapper<TRef, TModel>
=> SetActiveAsync<TId, TRef, TModel, TMapper>(container, id, true, mapper, cancellationToken);
/// <summary>
/// Deactivates the specified <paramref name="id"/> in the container.
/// </summary>
/// <typeparam name="TId">The identifier <see cref="Type"/>.</typeparam>
/// <typeparam name="TRef">The <see cref="IReferenceData{TId}"/> <see cref="Type"/>.</typeparam>
/// <typeparam name="TModel">The model <see cref="Type"/>.</typeparam>
/// <typeparam name="TMapper">The <see cref="IBiDirectionMapper{TSource, TDestination}"/> <see cref="Type"/>.</typeparam>
/// <param name="container">The <see cref="CosmosDbContainer{TModel}"/>.</param>
/// <param name="id">The identifier of the value to deactivate.</param>
/// <param name="mapper">The mapper.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The <see cref="DataResult{T}"/> containing the deactivated value.</returns>
public static Task<Result<DataResult<TRef>>> DeactivateAsync<TId, TRef, TModel, TMapper>(CosmosDbContainer<TModel> container, TId id, TMapper mapper, CancellationToken cancellationToken = default)
where TRef : class, IReferenceData<TId>
where TModel : CosmosDbReferenceDataModelBase, IEntityKey, new()
where TMapper : class, IBiDirectionMapper<TRef, TModel>
=> SetActiveAsync<TId, TRef, TModel, TMapper>(container, id, false, mapper, cancellationToken);
/// <summary>
/// Sets the active state of the specified <paramref name="id"/>; no update is performed where already in the requested state.
/// </summary>
private static async Task<Result<DataResult<TRef>>> SetActiveAsync<TId, TRef, TModel, TMapper>(CosmosDbContainer<TModel> container, TId id, bool isActive, TMapper mapper, CancellationToken cancellationToken)
where TRef : class, IReferenceData<TId>
where TModel : CosmosDbReferenceDataModelBase, IEntityKey, new()
where TMapper : class, IBiDirectionMapper<TRef, TModel>
{
container.ThrowIfNull();
var existing = await container.GetWithResultAsync(CompositeKey.Create(id), cancellationToken).ConfigureAwait(false);
if (existing.IsFailure)
return existing.AsResult();
if (existing.Value.IsActive == isActive)
return new DataResult<TRef>(mapper.From.Map(existing.Value)!, false);
existing.Value.IsActive = isActive;
var result = await container.UpdateWithResultAsync(existing.Value, cancellationToken).ConfigureAwait(false);
return result.ThenAs(dr => new DataResult<TRef>(mapper.From.Map(dr.Value)!, dr.WasMutated));
}
/// <summary>
/// Deletes the specified <paramref name="id"/> from the container.
/// </summary>
/// <typeparam name="TId">The identifier <see cref="Type"/>.</typeparam>
/// <typeparam name="TRef">The <see cref="IReferenceData{TId}"/> <see cref="Type"/>.</typeparam>
/// <typeparam name="TModel">The model <see cref="Type"/>.</typeparam>
/// <param name="container">The <see cref="CosmosDbContainer{TModel}"/>.</param>
/// <param name="id">The identifier of the value to delete.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A <see cref="DataResult"/></returns>
/// <remarks>A delete is considered idempotent and as such no <see cref="NotFoundException"/> will be thrown. The returning <see cref="DataResult.WasMutated"/> is informational only.</remarks>
public static async Task<Result<DataResult>> DeleteAsync<TId, TRef, TModel>(CosmosDbContainer<TModel> container, TId id, CancellationToken cancellationToken = default)
where TRef : class, IReferenceData<TId>
where TModel : CosmosDbReferenceDataModelBase, IEntityKey, new()
{
container.ThrowIfNull();
var existing = await container.GetWithResultAsync(CompositeKey.Create(id), cancellationToken).ConfigureAwait(false);
if (existing.IsFailure)
return existing.IsNotFoundError ? DataResult.False : existing.AsResult();
if (existing.Value.IsActive)
return Result<DataResult>.Fail("An active reference data value cannot be deleted.", c => c.WithErrorCode("cannot-delete-active"));
// Carry the ETag of the read above so that a concurrent change (e.g. activation) results in a precondition failure rather than deleting an item that is no longer inactive.
var args = container.Args with { ItemRequestOptions = new ItemRequestOptions { IfMatchEtag = existing.Value.ETag } };
return await container.DeleteWithResultAsync(args, CompositeKey.Create(id), cancellationToken).ConfigureAwait(false);
}
}