|
| 1 | +# Winton.DomainModelling.DocumentDb |
| 2 | + |
| 3 | +[](https://ci.appveyor.com/project/wintoncode/winton-domainmodelling-documentdb/branch/master) |
| 4 | +[](https://travis-ci.org/wintoncode/Winton.DomainModelling.DocumentDb) |
| 5 | +[](https://www.nuget.org/packages/Winton.DomainModelling.DocumentDb) |
| 6 | +[](https://www.nuget.org/packages/Winton.DomainModelling.DocumentDb) |
| 7 | + |
| 8 | +A facade library useful for [Entity](https://github.com/wintoncode/Winton.DomainModelling.Abstractions#entity) operations in DocumentDb (SQL API). |
| 9 | + |
| 10 | +## Types |
| 11 | + |
| 12 | +### IEntityFacade |
| 13 | + |
| 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. |
| 15 | + |
| 16 | +### EntityFacade |
| 17 | + |
| 18 | +The default implementation of `IEntityFacade`. The Create method supports automatic ID generation for string-serializable ID types, otherwise IDs must be set before creating. |
| 19 | + |
| 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. |
| 21 | + |
| 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 | +## Usage |
| 25 | + |
| 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 |
| 27 | + |
| 28 | +```csharp |
| 29 | +Database database = new Database { Id = "AllTheData" }; |
| 30 | +DocumentCollection documentCollection = new DocumentCollection { Id = "AccountingData" }; |
| 31 | + |
| 32 | +IDocumentClient documentClient = new DocumentClient(...); |
| 33 | + |
| 34 | +IEntityFacade entityFacade = new EntityFacade(database, documentCollection, documentClient); |
| 35 | +``` |
| 36 | + |
| 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. |
| 38 | + |
| 39 | +```csharp |
| 40 | +[JsonConverter(typeof(SingleValueConverter))] |
| 41 | +public struct AccountId : IEquatable<AccountId> |
| 42 | +{ |
| 43 | + ... |
| 44 | +} |
| 45 | + |
| 46 | +public sealed class Account : Entity<AccountId> |
| 47 | +{ |
| 48 | + public Account( |
| 49 | + AccountId id, |
| 50 | + AccountName name, |
| 51 | + ...) |
| 52 | + : base(id) |
| 53 | + { |
| 54 | + Name = name; |
| 55 | + ... |
| 56 | + } |
| 57 | + |
| 58 | + public AccountName Name { get; } |
| 59 | + |
| 60 | + ... |
| 61 | +} |
| 62 | + |
| 63 | +[JsonConverter(typeof(SingleValueConverter))] |
| 64 | +public struct TransactionId : IEquatable<TransactionId> |
| 65 | +{ |
| 66 | + ... |
| 67 | +} |
| 68 | + |
| 69 | +public sealed class Transaction : Entity<TransactionId> |
| 70 | +{ |
| 71 | + public Transaction( |
| 72 | + TransactionId id, |
| 73 | + AccountId sender, |
| 74 | + AccountId recipient, |
| 75 | + ...) |
| 76 | + : base(id) |
| 77 | + { |
| 78 | + Sender = sender; |
| 79 | + Recipient = recipient; |
| 80 | + ... |
| 81 | + } |
| 82 | + |
| 83 | + public AccountId Sender { get; } |
| 84 | + |
| 85 | + public AccountId Recipient { get; } |
| 86 | + |
| 87 | + ... |
| 88 | +} |
| 89 | +``` |
| 90 | + |
| 91 | +These types could each have their own repository interfaces, defined within the "Accounting" domain. |
| 92 | + |
| 93 | +```csharp |
| 94 | +public interface IAccountRepository |
| 95 | +{ |
| 96 | + Task<AccountId> Create(Account account); |
| 97 | + |
| 98 | + Task<Account> Get(AccountId id); |
| 99 | + |
| 100 | + ... |
| 101 | +} |
| 102 | + |
| 103 | +public interface ITransactionRepository |
| 104 | +{ |
| 105 | + Task<TransactionId> Create(Transaction transaction); |
| 106 | + |
| 107 | + IEnumerable<Transaction> GetAllSentBy(AccountId accountId); |
| 108 | + |
| 109 | + ... |
| 110 | +} |
| 111 | +``` |
| 112 | + |
| 113 | +The respective implementations of these repositories, potentially defined in a separate "Persistence" layer, would simply be thin wrappers around the `IEntityFacade`. |
| 114 | + |
| 115 | +```csharp |
| 116 | +internal sealed class AccountRepository : IAccountRepository |
| 117 | +{ |
| 118 | + private readonly IEntityFacade _entityFacade; |
| 119 | + |
| 120 | + public AccountRepository(IEntityFacade entityFacade) |
| 121 | + { |
| 122 | + _entityFacade = entityFacade; |
| 123 | + } |
| 124 | + |
| 125 | + public async Task<AccountId> Create(Account account) |
| 126 | + { |
| 127 | + return (await _entityFacade.Create<Account, AccountId>(account)).Id; |
| 128 | + } |
| 129 | + |
| 130 | + public async Task<Account> Get(AccountId id) |
| 131 | + { |
| 132 | + return await _entityFacade.Read<Account, AccountId>(id); |
| 133 | + } |
| 134 | + |
| 135 | + ... |
| 136 | +} |
| 137 | + |
| 138 | +internal sealed class TransactionRepository : ITransactionRepository |
| 139 | +{ |
| 140 | + private readonly IEntityFacade _entityFacade; |
| 141 | + |
| 142 | + public TransactionRepository(IEntityFacade entityFacade) |
| 143 | + { |
| 144 | + _entityFacade = entityFacade; |
| 145 | + } |
| 146 | + |
| 147 | + public async Task<TransactionId> Create(Transaction transaction) |
| 148 | + { |
| 149 | + return (await _entityFacade.Create<Transaction, TransactionId>(transaction)).Id; |
| 150 | + } |
| 151 | + |
| 152 | + public IEnumerable<Transaction> GetAllSentBy(AccountId accountId) |
| 153 | + { |
| 154 | + return _entityFacade.Query<Transaction, TransactionId>() |
| 155 | + .Where(t => t.Sender == accountId); |
| 156 | + } |
| 157 | + |
| 158 | + ... |
| 159 | +} |
| 160 | +``` |
| 161 | + |
| 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 immutable once entities have been persisted. |
0 commit comments