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

Commit 7a6e2ad

Browse files
iscottb122Iain Scott
authored andcommitted
Added optional DTO mapping for persistence.
1 parent 6bc78b5 commit 7a6e2ad

21 files changed

Lines changed: 1274 additions & 135 deletions

README.md

Lines changed: 213 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ A facade library useful for [Entity](https://github.com/wintoncode/Winton.Domain
99

1010
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.
1111

12-
## Facade Types
12+
## Facade Interfaces
1313

1414
Note that the default implementations are 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.
1515

@@ -21,7 +21,7 @@ An abstraction layer over [Entity](https://github.com/wintoncode/Winton.DomainMo
2121

2222
An abstraction layer over Value Object operations in DocumentDb. Provides strong typed Create, Delete, and Query methods.
2323

24-
## Usage
24+
## Domain Object Persistence
2525

2626
The default implementations of both `IEntityFacade` and `IValueObjectFacade` should be created from their provided factories. These can each be constructed from an [IDocumentClient](https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.documents.idocumentclient). Their Create methods both take a [Database](https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.documents.database) and a [DocumentCollection](https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.documents.documentcollection). Of course, resolving all dependencies from a DI container is preferred, but for clarity they can be manually constructed as
2727

@@ -34,8 +34,9 @@ DocumentCollection documentCollection = new DocumentCollection { Id = "Accountin
3434
IEntityFacadeFactory entityFacadeFactory = new EntityFacadeFactory(documentClient);
3535
IValueObjectFacadeFactory valueObjectFacadeFactory = new ValueObjectFacadeFactory(documentClient);
3636

37-
IEntityFacade<Account, AccountId> entityFacade = entityFacadeFactory.Create<Account, AccountId>(database, documentCollection);
38-
IValueObjectFacade<AccountType> valueObjectFacade = valueObjectFacadeFactory.Create<AccountType>(database, documentCollection);
37+
IEntityFacade<Account, AccountId> accountFacade = entityFacadeFactory.Create<Account, AccountId>(database, documentCollection);
38+
IEntityFacade<Transaction, TransactionId> transactionFacade = entityFacadeFactory.Create<Transaction, TransactionId>(database, documentCollection);
39+
IValueObjectFacade<AccountType> accountTypeFacade = valueObjectFacadeFactory.Create<AccountType>(database, documentCollection);
3940
```
4041

4142
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.
@@ -98,7 +99,6 @@ public struct AccountType : IEquatable<AccountType>
9899
string name,
99100
decimal rate,
100101
...)
101-
: base(id)
102102
{
103103
Name = name;
104104
Rate = rate;
@@ -144,7 +144,7 @@ public interface IAccountTypeRepository
144144
}
145145
```
146146

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

149149
```csharp
150150
internal sealed class AccountRepository : IAccountRepository
@@ -214,4 +214,210 @@ internal sealed class AccountTypeRepository : IAccountTypeRepository
214214
}
215215
```
216216

217-
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.
217+
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.
218+
219+
## Data Transfer Object Persistence
220+
221+
In most applications, it is desirable to completely separate persistence concerns from domain logic. The above examples slightly violate this principle by leaking some Json serialization details into the domain objects. However, this can be easily avoided, at the expense of having to define some Data Transfer Objects (DTOs), and mapping functions. Consider essentially the same domain objects, but with all serialization logic removed.
222+
223+
```csharp
224+
public struct AccountId : IEquatable<AccountId>
225+
{
226+
public static explicit operator string(AccountId id) { ... }
227+
228+
public static explicit operator AccountId(string value) { ... }
229+
230+
...
231+
}
232+
233+
...
234+
235+
public struct TransactionId : IEquatable<TransactionId>
236+
{
237+
public static explicit operator string(TransactionId id) { ... }
238+
239+
public static explicit operator TransactionId(string value) { ... }
240+
241+
...
242+
}
243+
244+
...
245+
246+
public struct AccountType : IEquatable<AccountType>
247+
{
248+
public AccountType(
249+
string name,
250+
decimal rate,
251+
...)
252+
{
253+
Name = name;
254+
Rate = rate;
255+
...
256+
}
257+
258+
public string Name { get; }
259+
260+
public decimal Rate { get; }
261+
262+
...
263+
}
264+
```
265+
266+
The persistence layer DTOs, including serialization logic, for these simple types would then look very similar to the original domain objects.
267+
268+
```csharp
269+
internal struct AccountDto
270+
{
271+
[JsonConstructor]
272+
public AccountDto(
273+
string id,
274+
AccountTypeDto type,
275+
...)
276+
{
277+
Type = type;
278+
...
279+
}
280+
281+
public string Id { get; }
282+
283+
public AccountTypeDto Type { get; }
284+
285+
...
286+
}
287+
288+
internal struct TransactionDto
289+
{
290+
[JsonConstructor]
291+
public TransactionDto(
292+
string id,
293+
string sender,
294+
string recipient,
295+
...)
296+
{
297+
Sender = sender;
298+
Recipient = recipient;
299+
...
300+
}
301+
302+
public string Id { get; }
303+
304+
public string Sender { get; }
305+
306+
public string Recipient { get; }
307+
308+
...
309+
}
310+
311+
internal struct AccountTypeDto
312+
{
313+
[JsonConstructor]
314+
public AccountTypeDto(
315+
string name,
316+
decimal rate,
317+
...)
318+
{
319+
Name = name;
320+
Rate = rate;
321+
...
322+
}
323+
324+
public string Name { get; }
325+
326+
public decimal Rate { get; }
327+
328+
...
329+
}
330+
```
331+
332+
The overloaded implementations of both `IEntityFacade` and `IValueObjectFacade` can be created from the same factories, but by specifying the appropriate DTO and mapping function. They can be manually constructed as
333+
334+
```csharp
335+
IEntityFacade<Account, AccountId, AccountDto> accountFacade = entityFacadeFactory.Create<Account, AccountId, AccountDto>(
336+
database,
337+
documentCollection,
338+
a => new AccountDto((string)a.Id, new AccountTypeDto(a.Type.Name, a.Type.Rate)),
339+
d => new Account((AccountId)d.Id, new AccountType(d.Type.Name, d.Type.Rate)));
340+
IEntityFacade<Transaction, TransactionId, TransactionDto> transactionFacade = entityFacadeFactory.Create<Transaction, TransactionId, TransactionDto>(
341+
database,
342+
documentCollection,
343+
t => new TransactionDto((string)t.Id, (string)t.Sender, (string)t.Recipient),
344+
d => new Transaction((TransactionId)d.Id, (AccountId)d.Sender, (AccountId)d.Recipient));
345+
IValueObjectFacade<AccountType, AccountTypeDto> accountTypeFacade = valueObjectFacadeFactory.Create<AccountType, AccountTypeDto>(
346+
database,
347+
documentCollection,
348+
at => new AccountTypeDto(at.Name, at.Rate),
349+
d => new AccountType(d.Name, d.Rate));
350+
```
351+
352+
The new repository implementations would then use the overloaded facade interfaces.
353+
354+
```csharp
355+
internal sealed class AccountRepository : IAccountRepository
356+
{
357+
private readonly IEntityFacade<Account, AccountId, AccountDto> _entityFacade;
358+
359+
public AccountRepository(IEntityFacade<Account, AccountId, AccountDto> entityFacade)
360+
{
361+
_entityFacade = entityFacade;
362+
}
363+
364+
public async Task<AccountId> Create(Account account)
365+
{
366+
return (await _entityFacade.Create(account)).Id;
367+
}
368+
369+
public async Task<Account> Get(AccountId id)
370+
{
371+
return await _entityFacade.Read(id);
372+
}
373+
374+
...
375+
}
376+
377+
internal sealed class TransactionRepository : ITransactionRepository
378+
{
379+
private readonly IEntityFacade<Transaction, TransactionId, TransactionDto> _entityFacade;
380+
381+
public TransactionRepository(IEntityFacade<Transaction, TransactionId, TransactionDto> entityFacade)
382+
{
383+
_entityFacade = entityFacade;
384+
}
385+
386+
public async Task<TransactionId> Create(Transaction transaction)
387+
{
388+
return (await _entityFacade.Create(transaction)).Id;
389+
}
390+
391+
public IEnumerable<Transaction> GetAllSentBy(AccountId accountId)
392+
{
393+
var sender = (string)accountId;
394+
return _entityFacade.Query(t => t.Sender == sender);
395+
}
396+
397+
...
398+
}
399+
400+
internal sealed class AccountTypeRepository : IAccountTypeRepository
401+
{
402+
private readonly IValueObjectFacade<AccountType, AccountTypeDto> _valueObjectFacade;
403+
404+
public AccountTypeRepository(IValueObjectFacade<AccountType, AccountTypeDto> valueObjectFacade)
405+
{
406+
_valueObjectFacade = valueObjectFacade;
407+
}
408+
409+
public async Task Create(AccountType accountType)
410+
{
411+
await _valueObjectFacade.Create(accountType);
412+
}
413+
414+
public IEnumerable<AccountType> GetAll()
415+
{
416+
return _valueObjectFacade.Query();
417+
}
418+
419+
...
420+
}
421+
```
422+
423+
Of course, this method results in more code overall, due to the need to define the DTOs, and lambdas to perform the mappings. However, these types and conversions are very simple, and full separation of domain and persistence concerns has been achieved. As before, these repositories will store their respective types in a single shared collection, using the **domain object** type names to discriminate between each type.

src/Winton.DomainModelling.DocumentDb/EntityDocument.cs

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,30 @@
66

77
namespace Winton.DomainModelling.DocumentDb
88
{
9-
internal sealed class EntityDocument<TEntity, TEntityId>
9+
internal sealed class EntityDocument<TEntity, TEntityId, TDto>
1010
where TEntity : Entity<TEntityId>
1111
where TEntityId : IEquatable<TEntityId>
1212
{
13-
public EntityDocument(TEntity entity)
13+
public EntityDocument(TEntity entity, TDto dto)
14+
: this(dto, GetDocumentId(entity.Id), GetDocumentType())
1415
{
15-
Entity = entity;
1616
}
1717

18-
public TEntity Entity { get; }
18+
[JsonConstructor]
19+
private EntityDocument(TDto entity, string id, string type)
20+
{
21+
Dto = entity;
22+
Id = id;
23+
Type = type;
24+
}
25+
26+
[JsonProperty(PropertyName = "Entity")]
27+
public TDto Dto { get; }
1928

2029
[JsonProperty(PropertyName = "id")]
21-
public string Id => GetDocumentId(Entity.Id);
30+
public string Id { get; }
2231

23-
public string Type => GetDocumentType();
32+
public string Type { get; }
2433

2534
public static string GetDocumentId(TEntityId id)
2635
{

0 commit comments

Comments
 (0)