Skip to content

Commit 3756104

Browse files
committed
Allow persisting entities as documents
Introduce a document-based repository that allows persisting the entire entity payload to a single column. Regardless of the serialization strategy used, we persist the type full name and assembly version of the persisted entity, which can be invaluable in data migration scenarios. In addition to the built-in JSON text serialization, we provide three binary serializers too as separate packages: Bson, MessagePack and Protobuf. The last two require annotating the entity as required by the underlying libraries. Fixes #24.
1 parent db525bd commit 3756104

39 files changed

+1284
-16
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,5 @@ _site
2626
.jekyll-metadata
2727
.jekyll-cache
2828
Gemfile.lock
29-
package-lock.json
29+
package-lock.json
30+
__azurite_db_table__.json

TableStorage.sln

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,20 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests", "src\Tests\Tests.cs
1818
EndProject
1919
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TableStorage.Source", "src\TableStorage.Source\TableStorage.Source.csproj", "{B58183AB-38E7-42FA-BE98-5D7B58C06266}"
2020
EndProject
21+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TableStorage.Bson", "src\TableStorage.Bson\TableStorage.Bson.csproj", "{D0C6041C-D796-483A-8470-F78A4C659BD5}"
22+
EndProject
23+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TableStorage.Bson.Source", "src\TableStorage.Bson.Source\TableStorage.Bson.Source.csproj", "{3CDC8EBB-39EB-4195-8A65-2CAEBF6C250B}"
24+
EndProject
25+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Source", "Source", "{7412B98E-AA65-4B6E-AD83-6F9960C1C452}"
26+
EndProject
27+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TableStorage.MessagePack", "src\TableStorage.MessagePack\TableStorage.MessagePack.csproj", "{C5857B21-FDB6-49FB-8FDE-A1450B0C7EC6}"
28+
EndProject
29+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TableStorage.MessagePack.Source", "src\TableStorage.MessagePack.Source\TableStorage.MessagePack.Source.csproj", "{09DC6B3D-2950-4C2A-9D53-CFFCBBBA612C}"
30+
EndProject
31+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TableStorage.Protobuf", "src\TableStorage.Protobuf\TableStorage.Protobuf.csproj", "{3DEA170D-5637-4A01-AA53-B727013FA850}"
32+
EndProject
33+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TableStorage.Protobuf.Source", "src\TableStorage.Protobuf.Source\TableStorage.Protobuf.Source.csproj", "{7801FE93-4A6C-4C49-82B0-F32DEFE3A2E2}"
34+
EndProject
2135
Global
2236
GlobalSection(SolutionConfigurationPlatforms) = preSolution
2337
Debug|Any CPU = Debug|Any CPU
@@ -36,10 +50,40 @@ Global
3650
{B58183AB-38E7-42FA-BE98-5D7B58C06266}.Debug|Any CPU.Build.0 = Debug|Any CPU
3751
{B58183AB-38E7-42FA-BE98-5D7B58C06266}.Release|Any CPU.ActiveCfg = Release|Any CPU
3852
{B58183AB-38E7-42FA-BE98-5D7B58C06266}.Release|Any CPU.Build.0 = Release|Any CPU
53+
{D0C6041C-D796-483A-8470-F78A4C659BD5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
54+
{D0C6041C-D796-483A-8470-F78A4C659BD5}.Debug|Any CPU.Build.0 = Debug|Any CPU
55+
{D0C6041C-D796-483A-8470-F78A4C659BD5}.Release|Any CPU.ActiveCfg = Release|Any CPU
56+
{D0C6041C-D796-483A-8470-F78A4C659BD5}.Release|Any CPU.Build.0 = Release|Any CPU
57+
{3CDC8EBB-39EB-4195-8A65-2CAEBF6C250B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
58+
{3CDC8EBB-39EB-4195-8A65-2CAEBF6C250B}.Debug|Any CPU.Build.0 = Debug|Any CPU
59+
{3CDC8EBB-39EB-4195-8A65-2CAEBF6C250B}.Release|Any CPU.ActiveCfg = Release|Any CPU
60+
{3CDC8EBB-39EB-4195-8A65-2CAEBF6C250B}.Release|Any CPU.Build.0 = Release|Any CPU
61+
{C5857B21-FDB6-49FB-8FDE-A1450B0C7EC6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
62+
{C5857B21-FDB6-49FB-8FDE-A1450B0C7EC6}.Debug|Any CPU.Build.0 = Debug|Any CPU
63+
{C5857B21-FDB6-49FB-8FDE-A1450B0C7EC6}.Release|Any CPU.ActiveCfg = Release|Any CPU
64+
{C5857B21-FDB6-49FB-8FDE-A1450B0C7EC6}.Release|Any CPU.Build.0 = Release|Any CPU
65+
{09DC6B3D-2950-4C2A-9D53-CFFCBBBA612C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
66+
{09DC6B3D-2950-4C2A-9D53-CFFCBBBA612C}.Debug|Any CPU.Build.0 = Debug|Any CPU
67+
{09DC6B3D-2950-4C2A-9D53-CFFCBBBA612C}.Release|Any CPU.ActiveCfg = Release|Any CPU
68+
{09DC6B3D-2950-4C2A-9D53-CFFCBBBA612C}.Release|Any CPU.Build.0 = Release|Any CPU
69+
{3DEA170D-5637-4A01-AA53-B727013FA850}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
70+
{3DEA170D-5637-4A01-AA53-B727013FA850}.Debug|Any CPU.Build.0 = Debug|Any CPU
71+
{3DEA170D-5637-4A01-AA53-B727013FA850}.Release|Any CPU.ActiveCfg = Release|Any CPU
72+
{3DEA170D-5637-4A01-AA53-B727013FA850}.Release|Any CPU.Build.0 = Release|Any CPU
73+
{7801FE93-4A6C-4C49-82B0-F32DEFE3A2E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
74+
{7801FE93-4A6C-4C49-82B0-F32DEFE3A2E2}.Debug|Any CPU.Build.0 = Debug|Any CPU
75+
{7801FE93-4A6C-4C49-82B0-F32DEFE3A2E2}.Release|Any CPU.ActiveCfg = Release|Any CPU
76+
{7801FE93-4A6C-4C49-82B0-F32DEFE3A2E2}.Release|Any CPU.Build.0 = Release|Any CPU
3977
EndGlobalSection
4078
GlobalSection(SolutionProperties) = preSolution
4179
HideSolutionNode = FALSE
4280
EndGlobalSection
81+
GlobalSection(NestedProjects) = preSolution
82+
{B58183AB-38E7-42FA-BE98-5D7B58C06266} = {7412B98E-AA65-4B6E-AD83-6F9960C1C452}
83+
{3CDC8EBB-39EB-4195-8A65-2CAEBF6C250B} = {7412B98E-AA65-4B6E-AD83-6F9960C1C452}
84+
{09DC6B3D-2950-4C2A-9D53-CFFCBBBA612C} = {7412B98E-AA65-4B6E-AD83-6F9960C1C452}
85+
{7801FE93-4A6C-4C49-82B0-F32DEFE3A2E2} = {7412B98E-AA65-4B6E-AD83-6F9960C1C452}
86+
EndGlobalSection
4387
GlobalSection(ExtensibilityGlobals) = postSolution
4488
SolutionGuid = {9E26BFEF-9184-4EA2-8A64-BCD61E247C33}
4589
EndGlobalSection

assets/img/document.png

23.1 KB
Loading

assets/img/entity.png

19.1 KB
Loading

readme.md

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,62 @@ This is quite convenient for handling reference data, for example. Enumerating a
119119
in the partition wouldn't be something you'd typically do for your "real" data, but for
120120
reference data, it could be useful.
121121

122+
Stored entities will use individual columns for properties, which makes it easy to browse
123+
the data. If you don't need the individual columns, and would like a document-like storage
124+
mechanism instead, you can use the `DocumentRepository.Create` and `DocumentPartition.Create`
125+
factory methods instead. The API is otherwise the same, but you can see the effect of using
126+
one or the other in the following screenshots of the [Storage Explorer](https://azure.microsoft.com/en-us/features/storage-explorer/)
127+
for the same `Product` entity shown in the first example above:
128+
129+
![Screenshot of entity persisted with separate columns for properties](assets/img/entity.png)
130+
131+
![Screenshot of entity persisted as a document](assets/img/document.png)
132+
133+
The code that persisted both entities is:
134+
135+
```csharp
136+
var repo = TableRepository.Create<Product>(
137+
CloudStorageAccount.DevelopmentStorageAccount,
138+
tableName: "Products",
139+
partitionKey: p => p.Category,
140+
rowKey: p => p.Id);
141+
142+
await repo.PutAsync(new Product("book", "9781473217386")
143+
{
144+
Title = "Neuromancer",
145+
Price = 7.32
146+
});
147+
148+
var docs = DocumentRepository.Create<Product>(
149+
CloudStorageAccount.DevelopmentStorageAccount,
150+
tableName: "Documents",
151+
partitionKey: p => p.Category,
152+
rowKey: p => p.Id);
153+
154+
await docs.PutAsync(new Product("book", "9781473217386")
155+
{
156+
Title = "Neuromancer",
157+
Price = 7.32
158+
});
159+
```
160+
161+
The `DocumentType` is the `Type.FullName` of the entity type, and the `DocumentVersion` is
162+
the `Major.Minor` of its assembly, which could be used for advanced data migration scenarios.
163+
164+
In addition to the default built-in JSON plain-text based serializer, you can choose from
165+
various binary serializers which will instead persist the document as a byte array:
166+
167+
[![Bson](https://img.shields.io/nuget/v/Devlooped.TableStorage.svg?color=royalblue&label=Bson)](https://www.nuget.org/packages/Devlooped.TableStorage)
168+
[![MessagePack](https://img.shields.io/nuget/v/Devlooped.TableStorage.svg?color=royalblue&label=MessagePack)](https://www.nuget.org/packages/Devlooped.TableStorage)
169+
[![Protobuf](https://img.shields.io/nuget/v/Devlooped.TableStorage.svg?color=royalblue&label=Protobuf)](https://www.nuget.org/packages/Devlooped.TableStorage)
170+
171+
You can pass the serializer to use to the factory method as follows:
172+
173+
```csharp
174+
var repo = TableRepository.Create<Product>(...,
175+
serializer: [BsonDocumentSerializer|MessagePackDocumentSerializer|ProtobufDocumentSerializer].Default);
176+
```
177+
122178
### Attributes
123179

124180
If you want to avoid using strings with the factory methods, you can also annotate the
@@ -128,12 +184,11 @@ entity type to modify the default values used:
128184
* `[PartitionKey]`: annotates the property that should be used as the partition key
129185
* `[RowKey]`: annotates the property that should be used as the row key.
130186

131-
Values passed to the `TableRepository.Create<T>` or `TablePartition.Create<T>` override
132-
declarative attributes.
187+
Values passed to the factory methods override declarative attributes.
133188

134189
### TableEntity Support
135190

136-
Since these repository APIs are quite a bit more intuitive than working against a direct
191+
Since these repository APIs are quite a bit more intuitive than working directly against a
137192
`TableClient`, you might want to retrieve/enumerate entities just by their built-in `ITableEntity`
138193
properties, like `PartitionKey`, `RowKey`, `Timestamp` and `ETag`. For this scenario, we
139194
also support creating `ITableRepository<TableEntity>` and `ITablePartition<TableEntity>`
@@ -161,7 +216,7 @@ await foreach (TableEntity region in repo.EnumerateAsync())
161216
> Install-Package Devlooped.TableStorage
162217
```
163218

164-
There is also a source-only version, if you want to avoid an additional assembly:
219+
There is also a source-only version, if you want to avoid an additional assembly dependency:
165220

166221
```
167222
> Install-Package Devlooped.TableStorage.Source
@@ -179,8 +234,16 @@ namespace Devlooped
179234
public partial interface ITablePartition<T> { }
180235
public partial class TableRepository { }
181236
public partial class TableRepository<T> { }
237+
public partial class AttributedTableRepository<T> { }
238+
public partial class DocumentRepository { }
239+
public partial class DocumentRepository<T> { }
240+
public partial class AttributedDocumentRepository<T> { }
241+
public partial interface IDocumentSerializer { }
242+
public partial interface IBinaryDocumentSerializer { }
243+
public partial interface IStringDocumentSerializer { }
182244
public partial class TablePartition { }
183245
public partial class TablePartition<T> { }
246+
public partial class DocumentPartition { }
184247

185248
// Perhaps make the attributes visible too if you use them?
186249
public partial class TableAttribute { }
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<Project>
2+
3+
<ItemGroup>
4+
<Compile Update="@(Compile -> WithMetadataValue('NuGetPackageId', 'Devlooped.TableStorage.Bson.Source'))">
5+
<Visible>false</Visible>
6+
<Link>Devlooped\TableStorage.Bson\%(Filename)%(Extension)</Link>
7+
</Compile>
8+
</ItemGroup>
9+
10+
</Project>
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<AssemblyName>Devlooped.TableStorage.Bson.Source</AssemblyName>
5+
<TargetFramework>netstandard2.0</TargetFramework>
6+
<IsPackable>true</IsPackable>
7+
<PackBuildOutput>false</PackBuildOutput>
8+
<PackCompile>true</PackCompile>
9+
<Description>A source-only BSON binary serializer for use with document-based repositories.
10+
11+
Usage:
12+
13+
var repo = DocumentRepository.Create&lt;Product&gt;(storageAccount, serializer: BsonDocumentSerializer.Default);
14+
</Description>
15+
</PropertyGroup>
16+
17+
<ItemGroup>
18+
<PackageReference Include="NuGetizer" Version="0.7.0" />
19+
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="all" />
20+
<PackageReference Include="Newtonsoft.Json.Bson" Version="1.0.2" />
21+
</ItemGroup>
22+
23+
<ItemGroup>
24+
<ProjectReference Include="..\TableStorage.Source\TableStorage.Source.csproj" />
25+
</ItemGroup>
26+
27+
<ItemGroup>
28+
<Compile Include="..\TableStorage.Bson\**\*.cs" Exclude="..\TableStorage.Bson\Visibility.cs;..\TableStorage.Bson\obj\**\*.cs;" />
29+
<None Update="Devlooped.TableStorage.Bson.Source.targets" PackFolder="build" />
30+
</ItemGroup>
31+
32+
</Project>
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
//<auto-generated/>
2+
#nullable enable
3+
using System.IO;
4+
using Newtonsoft.Json;
5+
6+
namespace Devlooped
7+
{
8+
/// <summary>
9+
/// Default implementation of <see cref="IBinaryDocumentSerializer"/> which
10+
/// uses Newtonsoft.Json implementation of BSON for serialization.
11+
/// </summary>
12+
partial class BsonDocumentSerializer : IBinaryDocumentSerializer
13+
{
14+
static readonly JsonSerializer serializer = new JsonSerializer();
15+
16+
/// <summary>
17+
/// Default instance of the serializer.
18+
/// </summary>
19+
public static IDocumentSerializer Default { get; } = new BsonDocumentSerializer();
20+
21+
/// <inheritdoc />
22+
public T? Deserialize<T>(byte[] data)
23+
{
24+
if (data.Length == 0)
25+
return default;
26+
27+
using var mem = new MemoryStream(data);
28+
using var reader = new Newtonsoft.Json.Bson.BsonDataReader(mem);
29+
return (T?)serializer.Deserialize<T>(reader);
30+
}
31+
32+
/// <inheritdoc />
33+
public byte[] Serialize<T>(T value)
34+
{
35+
if (value == null)
36+
return new byte[0];
37+
38+
using var mem = new MemoryStream();
39+
using var writer = new Newtonsoft.Json.Bson.BsonDataWriter(mem);
40+
serializer.Serialize(writer, value);
41+
return mem.ToArray();
42+
}
43+
}
44+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<AssemblyName>Devlooped.TableStorage.Bson</AssemblyName>
5+
<TargetFramework>netstandard2.0</TargetFramework>
6+
<IsPackable>true</IsPackable>
7+
<Description>A BSON binary serializer for use with document-based repositories.
8+
9+
Usage:
10+
11+
var repo = DocumentRepository.Create&lt;Product&gt;(storageAccount, serializer: BsonDocumentSerializer.Default);
12+
</Description>
13+
</PropertyGroup>
14+
15+
<ItemGroup>
16+
<PackageReference Include="NuGetizer" Version="0.7.0" />
17+
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="all" />
18+
<PackageReference Include="Newtonsoft.Json.Bson" Version="1.0.2" />
19+
</ItemGroup>
20+
21+
<ItemGroup>
22+
<ProjectReference Include="..\TableStorage\TableStorage.csproj" />
23+
</ItemGroup>
24+
25+
</Project>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
//<auto-generated/>
2+
namespace Devlooped
3+
{
4+
// Sets default visibility when using compiled version, where everything is public
5+
public partial class BsonDocumentSerializer { }
6+
}

0 commit comments

Comments
 (0)