Skip to content
This repository was archived by the owner on Jan 27, 2022. It is now read-only.

Commit c5dac7d

Browse files
authored
Merge pull request #4 from wintoncode/value-objects
Added support for persisting value objects in shared collections.
2 parents d288599 + 0268500 commit c5dac7d

10 files changed

Lines changed: 625 additions & 19 deletions

File tree

README.md

Lines changed: 76 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,36 +5,46 @@
55
[![NuGet version](https://img.shields.io/nuget/v/Winton.DomainModelling.DocumentDb.svg)](https://www.nuget.org/packages/Winton.DomainModelling.DocumentDb)
66
[![NuGet version](https://img.shields.io/nuget/vpre/Winton.DomainModelling.DocumentDb.svg)](https://www.nuget.org/packages/Winton.DomainModelling.DocumentDb)
77

8-
A facade library useful for [Entity](https://github.com/wintoncode/Winton.DomainModelling.Abstractions#entity) operations in DocumentDb (SQL API).
8+
A facade library useful for [Entity](https://github.com/wintoncode/Winton.DomainModelling.Abstractions#entity) and Value Object operations in DocumentDb (SQL API).
9+
10+
This implementations allow multiple types to be transparently stored in one collection using 'wrapper' documents with type discriminators and namespaced IDs (for entities). It can be tempting for those from a traditional SQL background to provision a separate collection per type. However, this is often unnecessarily expensive, especially if much of the reserved throughput for a given collection is unused. Taking advantage of the "schemaless" nature of a document store, such as DocumentDb, can both reduce cost and simplify infrastructural complexity. This implementation provides an easy way to work with a single collection within a bounded context (within which persisted type names are unique) while outwardly still achieving the desired level of strong typing. There really is a schema, but the database doesn't need to know about it.
911

1012
## Types
1113

1214
### IEntityFacade
1315

14-
An abstraction layer over [Entity](https://github.com/wintoncode/Winton.DomainModelling.Abstractions#entity) CRUD operations in DocumentDb. Provides strong typed Create, Read (including Query), **Upsert**, and Delete methods.
16+
An abstraction layer over [Entity](https://github.com/wintoncode/Winton.DomainModelling.Abstractions#entity) CRUD operations in DocumentDb. Provides strong typed Create, Read, **Upsert**, Delete, and Query methods.
1517

1618
### EntityFacade
1719

1820
The default implementation of `IEntityFacade`. The Create method supports automatic ID generation for string-serializable ID types, otherwise IDs must be set before creating.
1921

20-
This implementation allows multiple entity types to be transparently stored in one collection (using a 'wrapper' document with a type discriminator and namespaced ID). It can be tempting for those from a traditional SQL background to provision a separate collection per entity type. However, this is often unnecessarily expensive, especially if much of the reserved throughput for a given collection is unused. Taking advantage of the "schemaless" nature of a document store, such as DocumentDb, can both reduce cost and simplify infrastructural complexity. This implementation provides an easy way to work with a single collection within a bounded context (within which entity type names are unique) while outwardly still achieving the desired level of strong typing. There really is a schema, but the database doesn't need to know about it.
22+
Note that this implementation is currently **incompatible with partitioned collections**. This restriction could potentially be lifted in a future version, at the expense of implementation complexity (and probably a leakier abstraction). However, for applications requiring large collections, where partitioning is actually needed, the conveniences provided by this facade are unlikely to be suitable anyway.
23+
24+
### IValueObjectFacade
25+
26+
An abstraction layer over Value Object operations in DocumentDb. Provides strong typed Create, Delete, and Query methods.
27+
28+
### ValueObjectFacade
29+
30+
The default implementation of `IValueObjectFacade`.
2131

2232
Note that this implementation is currently **incompatible with partitioned collections**. This restriction could potentially be lifted in a future version, at the expense of implementation complexity (and probably a leakier abstraction). However, for applications requiring large collections, where partitioning is actually needed, the conveniences provided by this facade are unlikely to be suitable anyway.
2333

2434
## Usage
2535

26-
The constructor for `EntityFacade` takes a [Database](https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.documents.database), a [DocumentCollection](https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.documents.documentcollection), and an [IDocumentClient](https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.documents.idocumentclient). Of course, resolving all dependencies from a DI container is preferred, but for clarity an `EntityFacade` can be manually constructed as
36+
The constructors for both `EntityFacade` and `ValueObjectFacade` take a [Database](https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.documents.database), a [DocumentCollection](https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.documents.documentcollection), and an [IDocumentClient](https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.documents.idocumentclient). Of course, resolving all dependencies from a DI container is preferred, but for clarity they can be manually constructed as
2737

2838
```csharp
2939
Database database = new Database { Id = "AllTheData" };
3040
DocumentCollection documentCollection = new DocumentCollection { Id = "AccountingData" };
31-
3241
IDocumentClient documentClient = new DocumentClient(...);
3342

3443
IEntityFacade entityFacade = new EntityFacade(database, documentCollection, documentClient);
44+
IValueObjectFacade valueObjectFacade = new ValueObjectFacade(database, documentCollection, documentClient);
3545
```
3646

37-
Consider some application with an "Accounting" domain containing 2 entity types, `Account` and `Transaction`, each with a [strong typed](https://tech.winton.com/2017/06/strong-typing-a-pattern-for-more-robust-code/) ID. Note that both ID types use [SingleValueConverter](https://github.com/wintoncode/Winton.Extensions.Serialization.Json#singlevalueconverter), so they are string-serializable.
47+
Consider some application with an "Accounting" domain containing 2 entity types, `Account` and `Transaction`, each with a [strong typed](https://tech.winton.com/2017/06/strong-typing-a-pattern-for-more-robust-code/) ID. Note that both ID types use [SingleValueConverter](https://github.com/wintoncode/Winton.Extensions.Serialization.Json#singlevalueconverter), so they are string-serializable. The "Accounting" domain also contains a value object type, `AccountType`, used as reference data.
3848

3949
```csharp
4050
[JsonConverter(typeof(SingleValueConverter))]
@@ -47,15 +57,15 @@ public sealed class Account : Entity<AccountId>
4757
{
4858
public Account(
4959
AccountId id,
50-
AccountName name,
60+
AccountType type,
5161
...)
5262
: base(id)
5363
{
54-
Name = name;
64+
Type = type;
5565
...
5666
}
5767

58-
public AccountName Name { get; }
68+
public AccountType Type { get; }
5969

6070
...
6171
}
@@ -86,6 +96,27 @@ public sealed class Transaction : Entity<TransactionId>
8696

8797
...
8898
}
99+
100+
public struct AccountType : IEquatable<AccountType>
101+
{
102+
[JsonConstructor]
103+
public AccountType(
104+
string name,
105+
decimal rate,
106+
...)
107+
: base(id)
108+
{
109+
Name = name;
110+
Rate = rate;
111+
...
112+
}
113+
114+
public string Name { get; }
115+
116+
public decimal Rate { get; }
117+
118+
...
119+
}
89120
```
90121

91122
These types could each have their own repository interfaces, defined within the "Accounting" domain.
@@ -108,9 +139,18 @@ public interface ITransactionRepository
108139

109140
...
110141
}
142+
143+
public interface IAccountTypeRepository
144+
{
145+
Task Create(AccountType accountType);
146+
147+
IEnumerable<AccountType> GetAll();
148+
149+
...
150+
}
111151
```
112152

113-
The respective implementations of these repositories, potentially defined in a separate "Persistence" layer, would simply be thin wrappers around the `IEntityFacade`.
153+
The respective implementations of these repositories, potentially defined in a separate "Persistence" layer, would simply be thin wrappers around the `IEntityFacade` or `IValueObjectFacade`.
114154

115155
```csharp
116156
internal sealed class AccountRepository : IAccountRepository
@@ -152,11 +192,35 @@ internal sealed class TransactionRepository : ITransactionRepository
152192
public IEnumerable<Transaction> GetAllSentBy(AccountId accountId)
153193
{
154194
return _entityFacade.Query<Transaction, TransactionId>()
155-
.Where(t => t.Sender == accountId);
195+
.Where(t => t.Sender == accountId)
196+
.AsEnumerable();
197+
}
198+
199+
...
200+
}
201+
202+
internal sealed class AccountTypeRepository : IAccountTypeRepository
203+
{
204+
private readonly IValueObjectFacade _valueObjectFacade;
205+
206+
public AccountTypeRepository(IValueObjectFacade valueObjectFacade)
207+
{
208+
_valueObjectFacade = valueObjectFacade;
209+
}
210+
211+
public async Task Create(AccountType accountType)
212+
{
213+
await _valueObjectFacade.Create<AccountType>(accountType);
214+
}
215+
216+
public IEnumerable<AccountType> GetAll()
217+
{
218+
return _valueObjectFacade.Query<AccountType>()
219+
.AsEnumerable();
156220
}
157221

158222
...
159223
}
160224
```
161225

162-
These repositories will store their respective entities in a single shared collection, using the entity type names to namespace the IDs and discriminate between each type. Therefore, these names should be considered part of the document schema, and would therefore require a data migration if they were changed as part of a domain refactoring.
226+
These repositories will store their respective types in a single shared collection, using the type names to discriminate between each type and namespace the IDs (for entities). Therefore, these names should be considered part of the document schema, and would require a data migration if they were changed as part of a domain refactoring.

src/Winton.DomainModelling.DocumentDb/EntityFacade.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ namespace Winton.DomainModelling.DocumentDb
1212
{
1313
/// <inheritdoc />
1414
/// <summary>
15-
/// An abstraction layer over <see cref="Entity{TEntityId}" /> CRUD operations in DocumentDb. Allows multiple entity
16-
/// types to be transparently stored in one collection using a 'wrapper' document type with a type discriminator and
17-
/// namespaced ID.
15+
/// An abstraction layer over <see cref="Entity{TEntityId}" /> CRUD operations in DocumentDb. Allows multiple types to
16+
/// be transparently stored in one collection using a 'wrapper' document type with a type discriminator and namespaced
17+
/// ID.
1818
/// </summary>
1919
public sealed class EntityFacade : IEntityFacade
2020
{
@@ -32,7 +32,7 @@ public EntityFacade(Database database, DocumentCollection documentCollection, ID
3232
{
3333
if (documentCollection.PartitionKey.Paths.Any())
3434
{
35-
throw new NotSupportedException("Partitioned collections not supported.");
35+
throw new NotSupportedException("Partitioned collections are not supported.");
3636
}
3737

3838
_database = database;
@@ -136,7 +136,7 @@ public async Task<TEntity> Upsert<TEntity, TEntityId>(TEntity entity)
136136
{
137137
if (Equals(entity.Id, default(TEntityId)))
138138
{
139-
throw new NotSupportedException("Upserting with default ID not supported.");
139+
throw new NotSupportedException("Upserting with default ID is not supported.");
140140
}
141141

142142
var document = new EntityDocument<TEntity, TEntityId>(entity);
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright (c) Winton. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
3+
4+
using System;
5+
using System.Linq;
6+
using System.Threading.Tasks;
7+
8+
namespace Winton.DomainModelling.DocumentDb
9+
{
10+
/// <summary>
11+
/// An abstraction layer over Value Object operations in DocumentDb.
12+
/// </summary>
13+
public interface IValueObjectFacade
14+
{
15+
/// <summary>
16+
/// Create a Value Object of a specified type.
17+
/// </summary>
18+
/// <typeparam name="TValueObject">The type of the Value Object.</typeparam>
19+
/// <param name="valueObject">The Value Object to persist.</param>
20+
/// <returns>A Task.</returns>
21+
Task Create<TValueObject>(TValueObject valueObject)
22+
where TValueObject : struct, IEquatable<TValueObject>;
23+
24+
/// <summary>
25+
/// Delete a Value Object of a specified type.
26+
/// </summary>
27+
/// <typeparam name="TValueObject">The type of the Value Object.</typeparam>
28+
/// <param name="valueObject">The Value Object to delete.</param>
29+
/// <returns>A Task.</returns>
30+
Task Delete<TValueObject>(TValueObject valueObject)
31+
where TValueObject : struct, IEquatable<TValueObject>;
32+
33+
/// <summary>
34+
/// Query Value Objects of a specified type.
35+
/// </summary>
36+
/// <typeparam name="TValueObject">The type of the Value Objects.</typeparam>
37+
/// <returns>An <see cref="IQueryable{TValueObject}" />.</returns>
38+
IQueryable<TValueObject> Query<TValueObject>()
39+
where TValueObject : struct, IEquatable<TValueObject>;
40+
}
41+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright (c) Winton. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
3+
4+
using System;
5+
using Newtonsoft.Json;
6+
7+
namespace Winton.DomainModelling.DocumentDb
8+
{
9+
internal sealed class ValueObjectDocument<TValueObject>
10+
where TValueObject : struct, IEquatable<TValueObject>
11+
{
12+
[JsonConstructor]
13+
private ValueObjectDocument(TValueObject valueObject, string id)
14+
{
15+
ValueObject = valueObject;
16+
Id = id;
17+
}
18+
19+
[JsonProperty(PropertyName = "id")]
20+
public string Id { get; }
21+
22+
public string Type => GetDocumentType();
23+
24+
public TValueObject ValueObject { get; }
25+
26+
public static ValueObjectDocument<TValueObject> Create(TValueObject valueObject)
27+
{
28+
return new ValueObjectDocument<TValueObject>(valueObject, null);
29+
}
30+
31+
public static string GetDocumentType()
32+
{
33+
return typeof(TValueObject).Name;
34+
}
35+
}
36+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
// Copyright (c) Winton. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
3+
4+
using System;
5+
using System.Linq;
6+
using System.Threading.Tasks;
7+
using Microsoft.Azure.Documents;
8+
using Microsoft.Azure.Documents.Client;
9+
10+
namespace Winton.DomainModelling.DocumentDb
11+
{
12+
/// <inheritdoc />
13+
/// <summary>
14+
/// An abstraction layer over Value Object operations in DocumentDb. Allows multiple types to be transparently stored
15+
/// in one collection using a 'wrapper' document type with a type discriminator.
16+
/// </summary>
17+
public sealed class ValueObjectFacade : IValueObjectFacade
18+
{
19+
private readonly Database _database;
20+
private readonly IDocumentClient _documentClient;
21+
private readonly DocumentCollection _documentCollection;
22+
23+
/// <summary>
24+
/// Initializes a new instance of the <see cref="ValueObjectFacade" /> class.
25+
/// </summary>
26+
/// <param name="database">The DocumentDb database.</param>
27+
/// <param name="documentCollection">The DocumentDb collection. Partitioned collections are not supported.</param>
28+
/// <param name="documentClient">A document client implementation.</param>
29+
public ValueObjectFacade(
30+
Database database,
31+
DocumentCollection documentCollection,
32+
IDocumentClient documentClient)
33+
{
34+
if (documentCollection.PartitionKey.Paths.Any())
35+
{
36+
throw new NotSupportedException("Partitioned collections are not supported.");
37+
}
38+
39+
_database = database;
40+
_documentCollection = documentCollection;
41+
_documentClient = documentClient;
42+
}
43+
44+
/// <inheritdoc />
45+
/// <summary>
46+
/// Create a Value Object of a specified type.
47+
/// </summary>
48+
/// <typeparam name="TValueObject">The type of the Value Object.</typeparam>
49+
/// <param name="valueObject">The Value Object to persist.</param>
50+
/// <returns>A Task.</returns>
51+
public async Task Create<TValueObject>(TValueObject valueObject)
52+
where TValueObject : struct, IEquatable<TValueObject>
53+
{
54+
ValueObjectDocument<TValueObject> document = Get(valueObject);
55+
56+
if (document == null)
57+
{
58+
document = ValueObjectDocument<TValueObject>.Create(valueObject);
59+
60+
await _documentClient.CreateDocumentAsync(GetUri(), document);
61+
}
62+
}
63+
64+
/// <inheritdoc />
65+
/// <summary>
66+
/// Delete a Value Object of a specified type.
67+
/// </summary>
68+
/// <typeparam name="TValueObject">The type of the Value Object.</typeparam>
69+
/// <param name="valueObject">The Value Object to delete.</param>
70+
/// <returns>A Task.</returns>
71+
public async Task Delete<TValueObject>(TValueObject valueObject)
72+
where TValueObject : struct, IEquatable<TValueObject>
73+
{
74+
ValueObjectDocument<TValueObject> document = Get(valueObject);
75+
76+
if (document != null)
77+
{
78+
await _documentClient.DeleteDocumentAsync(GetUri(document.Id));
79+
}
80+
}
81+
82+
/// <inheritdoc />
83+
/// <summary>
84+
/// Query Value Objects of a specified type.
85+
/// </summary>
86+
/// <typeparam name="TValueObject">The type of the Value Objects.</typeparam>
87+
/// <returns>An <see cref="T:System.Linq.IQueryable`1" />.</returns>
88+
public IQueryable<TValueObject> Query<TValueObject>()
89+
where TValueObject : struct, IEquatable<TValueObject>
90+
{
91+
return CreateValueObjectDocumentQuery<TValueObject>().Select(x => x.ValueObject);
92+
}
93+
94+
private IQueryable<ValueObjectDocument<TValueObject>> CreateValueObjectDocumentQuery<TValueObject>()
95+
where TValueObject : struct, IEquatable<TValueObject>
96+
{
97+
string valueObjectType = ValueObjectDocument<TValueObject>.GetDocumentType();
98+
99+
return _documentClient.CreateDocumentQuery<ValueObjectDocument<TValueObject>>(GetUri())
100+
.Where(x => x.Type == valueObjectType);
101+
}
102+
103+
private ValueObjectDocument<TValueObject> Get<TValueObject>(TValueObject valueObject)
104+
where TValueObject : struct, IEquatable<TValueObject>
105+
{
106+
return CreateValueObjectDocumentQuery<TValueObject>()
107+
.AsEnumerable()
108+
.SingleOrDefault(x => x.ValueObject.Equals(valueObject));
109+
}
110+
111+
private Uri GetUri()
112+
{
113+
return UriFactory.CreateDocumentCollectionUri(_database.Id, _documentCollection.Id);
114+
}
115+
116+
private Uri GetUri(string id)
117+
{
118+
return UriFactory.CreateDocumentUri(_database.Id, _documentCollection.Id, id);
119+
}
120+
}
121+
}

0 commit comments

Comments
 (0)