diff --git a/readme.md b/readme.md index dd7ed02..f25e82e 100644 --- a/readme.md +++ b/readme.md @@ -6,6 +6,7 @@ [![License](https://img.shields.io/github/license/devlooped/TableStorage.svg?color=blue)](https://github.com/devlooped/TableStorage/blob/main/license.txt) [![Build](https://github.com/devlooped/TableStorage/workflows/build/badge.svg?branch=main)](https://github.com/devlooped/TableStorage/actions) + Repository pattern with POCO object support for storing to Azure/CosmosDB Table Storage ![Screenshot of basic usage](https://raw.githubusercontent.com/devlooped/TableStorage/main/assets/img/tablestorage.png) @@ -19,7 +20,7 @@ public record Product(string Category, string Id) { public string? Title { get; init; } public double Price { get; init; } - public DateOnly Created { get; init; } + public DateOnly CreatedAt { get; init; } } ``` @@ -38,7 +39,7 @@ var repo = TableRepository.Create(storageAccount, partitionKey: p => p.Category, rowKey: p => p.Id); -var product = new Product("Book", "1234") +var product = new Product("book", "1234") { Title = "Table Storage is Cool", Price = 25.5, @@ -47,8 +48,8 @@ var product = new Product("Book", "1234") // Insert or Update behavior (aka "upsert") await repo.PutAsync(product); -// Enumerate all products in category "Book" -await foreach (var p in repo.EnumerateAsync("Book")) +// Enumerate all products in category "book" +await foreach (var p in repo.EnumerateAsync("book")) Console.WriteLine(p.Price); // Query books priced in the 20-50 range, @@ -59,10 +60,10 @@ await foreach (var info in from prod in repo.CreateQuery() Console.WriteLine($"{info.Title}: {info.Price}"); // Get previously saved product. -Product saved = await repo.GetAsync("Book", "1234"); +Product saved = await repo.GetAsync("book", "1234"); // Delete product -await repo.DeleteAsync("Book", "1234"); +await repo.DeleteAsync("book", "1234"); // Can also delete passing entity await repo.DeleteAsync(saved); @@ -103,7 +104,9 @@ for example. In that case, you could use a `TableRepository` when saving: ```csharp -var repo = TableRepository.Create(storageAccount, "Books", x => x.Author, x => x.ISBN); +var repo = TableRepository.Create(storageAccount, "Books", + partitionKey: x => x.Author, + rowKey: x => x.ISBN); await repo.PutAsync(book); ``` @@ -112,7 +115,9 @@ And later on when listing/filtering books by a particular author, you can use a `TablePartition` so all querying is automatically scoped to that author: ```csharp -var partition = TablePartition.Create(storageAccount, "Books", "Rick Riordan", x => x.ISBN); +var partition = TablePartition.Create(storageAccount, "Books", + partitionKey: "Rick Riordan", + rowKey: x => x.ISBN); // Get Rick Riordan books, only from Disney/Hyperion, with over 1000 pages var query = from book in repo.CreateQuery() @@ -127,51 +132,79 @@ Using table partitions is quite convenient for handling reference data too, for Enumerating all entries in the partition wouldn't be something you'd typically do for your "real" data, but for reference data, it could come in handy. -Stored entities use individual columns for properties, which makes it easy to browse -the data (and query, as shown above!). If you don't need the individual columns, and would -like a document-like storage mechanism instead, you can use the `DocumentRepository.Create` -and `DocumentPartition.Create` factory methods instead. The API is otherwise the same, but -you can see the effect of using one or the other in the following screenshots of the -[Storage Explorer](https://azure.microsoft.com/en-us/features/storage-explorer/) for the same -`Product` entity shown in the first example above: +> NOTE: if access to the `Timestamp` managed by Azure Table Storage for the entity is needed, +> just declare a property with that name with either `DateTimeOffset`, `DateTime` or `string` type +> to read it. -![Screenshot of entity persisted with separate columns for properties](https://raw.githubusercontent.com/devlooped/TableStorage/main/assets/img/entity.png) +Stored entities using `TableRepository` and `TablePartition` use individual columns for +properties, which makes it easy to browse the data (and query, as shown above!). -![Screenshot of entity persisted as a document](https://raw.githubusercontent.com/devlooped/TableStorage/main/assets/img/document.png) +But document-based storage is also available via `DocumentRepository` and `DocumentPartition` if +you don't need the individual columns. + + +## Document Storage + +The `DocumentRepository.Create` and `DocumentPartition.Create` factory methods provide access +to document-based storage, exposing the a similar API as column-based storage. + +Document repositories cause entities to be persisted as a single document column, alongside type and version +information to handle versioning a the app level as needed. -The code that persisted both entities is: +The API is mostly the same as for column-based repositories (document repositories implement +the same underlying `ITableStorage` interface): ```csharp +public record Product(string Category, string Id) +{ + public string? Title { get; init; } + public double Price { get; init; } + public DateOnly CreatedAt { get; init; } +} + +var book = new Product("book", "9781473217386") +{ + Title = "Neuromancer", + Price = 7.32 +}; + +// Column-based storage var repo = TableRepository.Create( CloudStorageAccount.DevelopmentStorageAccount, tableName: "Products", partitionKey: p => p.Category, rowKey: p => p.Id); -await repo.PutAsync(new Product("book", "9781473217386") -{ - Title = "Neuromancer", - Price = 7.32 -}); +await repo.PutAsync(book); +// Document-based storage var docs = DocumentRepository.Create( CloudStorageAccount.DevelopmentStorageAccount, tableName: "Documents", partitionKey: p => p.Category, - rowKey: p => p.Id); + rowKey: p => p.Id + serializer: [SERIALIZER]); -await docs.PutAsync(new Product("book", "9781473217386") -{ - Title = "Neuromancer", - Price = 7.32 -}); +await docs.PutAsync(book); ``` -The `Type` column persisted in the table is the `Type.FullName` of the persited entity, and the -`Version` is the `Major.Minor` of its assembly, which could be used for advanced data migration scenarios. +> If not provided, the serializer defaults to the `System.Text.Json`-based `DocumentSerializer.Default`. + +The resulting differences in storage can be seen in the following screenshots of the +[Azure Storage Explorer](https://azure.microsoft.com/en-us/features/storage-explorer/): + +![Screenshot of entity persisted with separate columns for properties](https://raw.githubusercontent.com/devlooped/TableStorage/main/assets/img/entity.png) + +![Screenshot of entity persisted as a document](https://raw.githubusercontent.com/devlooped/TableStorage/main/assets/img/document.png) + + +The `Type` column persisted in the documents table is the `Type.FullName` of the persisted entity, and the +`Version` is the `[Major].[Minor]` of its assembly, which could be used for advanced data migration scenarios. The major and minor version components are also provided as individual columns for easier querying by various version ranges, using `IDocumentRepository.EnumerateAsync(predicate)`. + + In addition to the default built-in JSON plain-text based serializer (which uses the [System.Text.Json](https://www.nuget.org/packages/system.text.json) package), there are other alternative serializers for the document-based repository, including various binary serializers @@ -189,10 +222,8 @@ var repo = TableRepository.Create(..., > NOTE: when using alternative serializers, entities might need to be annotated with whatever > attributes are required by the underlying libraries. -> NOTE: if access to the `Timestamp` managed by Table Storage for the entity is needed, just declare a property -> with that name with either `DateTimeOffset`, `DateTime` or `string` type. -### Attributes +## Attributes If you want to avoid using strings with the factory methods, you can also annotate the entity type to modify the default values used: @@ -224,7 +255,7 @@ In addition, if you want to omit a particular property from persistence, you can it with `[Browsable(false)]` and it will be skipped when persisting and reading the entity. -### TableEntity Support +## TableEntity Support Since these repository APIs are quite a bit more intuitive than working directly against a `TableClient`, you might want to retrieve/enumerate entities just by their built-in `TableEntity` @@ -264,6 +295,8 @@ Assert.Equal("Neuromancer", entity["Title"]); Assert.Equal(7.32, (double)entity["Price"]); ``` + + ## Installation ``` @@ -296,7 +329,7 @@ The versioning scheme for packages is: - PR builds: *42.42.42-pr*`[NUMBER]` - Branch builds: *42.42.42-*`[BRANCH]`.`[COMMITS]` - + # Sponsors diff --git a/src/TableStorage.Bson.Source/readme.md b/src/TableStorage.Bson.Source/readme.md new file mode 100644 index 0000000..6323eec --- /dev/null +++ b/src/TableStorage.Bson.Source/readme.md @@ -0,0 +1,13 @@ +A source-only BSON binary serializer for use with document-based repositories. + +Usage: + +```csharp + var repo = DocumentRepository.Create(..., serializer: BsonDocumentSerializer.Default); +``` + + + + + + \ No newline at end of file diff --git a/src/TableStorage.Bson/TableStorage.Bson.csproj b/src/TableStorage.Bson/TableStorage.Bson.csproj index 6356fd5..5003fc4 100644 --- a/src/TableStorage.Bson/TableStorage.Bson.csproj +++ b/src/TableStorage.Bson/TableStorage.Bson.csproj @@ -4,12 +4,6 @@ Devlooped.TableStorage.Bson netstandard2.0;net6.0 true - A BSON binary serializer for use with document-based repositories. - -Usage: - - var repo = DocumentRepository.Create<Product>(storageAccount, serializer: BsonDocumentSerializer.Default); - diff --git a/src/TableStorage.Bson/readme.md b/src/TableStorage.Bson/readme.md new file mode 100644 index 0000000..2a90509 --- /dev/null +++ b/src/TableStorage.Bson/readme.md @@ -0,0 +1,13 @@ +A BSON binary serializer for use with document-based repositories. + +Usage: + +```csharp + var repo = DocumentRepository.Create(..., serializer: BsonDocumentSerializer.Default); +``` + + + + + + \ No newline at end of file diff --git a/src/TableStorage.MessagePack.Source/TableStorage.MessagePack.Source.csproj b/src/TableStorage.MessagePack.Source/TableStorage.MessagePack.Source.csproj index 6d34129..858d2f3 100644 --- a/src/TableStorage.MessagePack.Source/TableStorage.MessagePack.Source.csproj +++ b/src/TableStorage.MessagePack.Source/TableStorage.MessagePack.Source.csproj @@ -6,12 +6,6 @@ true false true - A source-only MessagePack binary serializer for use with document-based repositories. - -Usage: - - var repo = DocumentRepository.Create<Product>(storageAccount, serializer: MessagePackDocumentSerializer.Default); - diff --git a/src/TableStorage.MessagePack.Source/readme.md b/src/TableStorage.MessagePack.Source/readme.md new file mode 100644 index 0000000..ae49c68 --- /dev/null +++ b/src/TableStorage.MessagePack.Source/readme.md @@ -0,0 +1,15 @@ +A source-only MessagePack binary serializer for use with document-based repositories. + +Usage: + +```csharp + var repo = DocumentRepository.Create(..., serializer: MessagePackDocumentSerializer.Default); +``` + +> NOTE: MessagePack attributes must be used as usual in order for the serialization to work. + + + + + + \ No newline at end of file diff --git a/src/TableStorage.MessagePack/readme.md b/src/TableStorage.MessagePack/readme.md new file mode 100644 index 0000000..fc8a339 --- /dev/null +++ b/src/TableStorage.MessagePack/readme.md @@ -0,0 +1,15 @@ +A MessagePack binary serializer for use with document-based repositories. + +Usage: + +```csharp + var repo = DocumentRepository.Create(..., serializer: MessagePackDocumentSerializer.Default); +``` + +> NOTE: MessagePack attributes must be used as usual in order for the serialization to work. + + + + + + \ No newline at end of file diff --git a/src/TableStorage.Newtonsoft.Source/readme.md b/src/TableStorage.Newtonsoft.Source/readme.md new file mode 100644 index 0000000..b649894 --- /dev/null +++ b/src/TableStorage.Newtonsoft.Source/readme.md @@ -0,0 +1,13 @@ +A source-only Json.NET serializer for use with document-based repositories. + +Usage: + +```csharp + var repo = DocumentRepository.Create(..., serializer: JsonDocumentSerializer.Default); +``` + + + + + + \ No newline at end of file diff --git a/src/TableStorage.Newtonsoft/TableStorage.Newtonsoft.csproj b/src/TableStorage.Newtonsoft/TableStorage.Newtonsoft.csproj index 4bdbda5..709e0fc 100644 --- a/src/TableStorage.Newtonsoft/TableStorage.Newtonsoft.csproj +++ b/src/TableStorage.Newtonsoft/TableStorage.Newtonsoft.csproj @@ -4,12 +4,6 @@ Devlooped.TableStorage.Newtonsoft netstandard2.0;net6.0 true - A Newtonsoft.Json-based serializer for use with document-based repositories. - -Usage: - - var repo = DocumentRepository.Create<Product>(storageAccount, serializer: JsonDocumentSerializer.Default); - diff --git a/src/TableStorage.Newtonsoft/readme.md b/src/TableStorage.Newtonsoft/readme.md new file mode 100644 index 0000000..49aa774 --- /dev/null +++ b/src/TableStorage.Newtonsoft/readme.md @@ -0,0 +1,13 @@ +A Json.NET serializer for use with document-based repositories. + +Usage: + +```csharp + var repo = DocumentRepository.Create(..., serializer: JsonDocumentSerializer.Default); +``` + + + + + + \ No newline at end of file diff --git a/src/TableStorage.Protobuf.Source/TableStorage.Protobuf.Source.csproj b/src/TableStorage.Protobuf.Source/TableStorage.Protobuf.Source.csproj index fa886ff..37fd4ae 100644 --- a/src/TableStorage.Protobuf.Source/TableStorage.Protobuf.Source.csproj +++ b/src/TableStorage.Protobuf.Source/TableStorage.Protobuf.Source.csproj @@ -6,12 +6,6 @@ true false true - A source-only Protocol Buffers binary serializer for use with document-based repositories. - -Usage: - - var repo = DocumentRepository.Create<Product>(storageAccount, serializer: ProtobufDocumentSerializer.Default); - diff --git a/src/TableStorage.Protobuf.Source/readme.md b/src/TableStorage.Protobuf.Source/readme.md new file mode 100644 index 0000000..cd9394e --- /dev/null +++ b/src/TableStorage.Protobuf.Source/readme.md @@ -0,0 +1,13 @@ +A source-only Protocol Buffers binary serializer for use with document-based repositories. + +Usage: + +```csharp + var repo = DocumentRepository.Create(..., serializer: ProtobufDocumentSerializer.Default); +``` + + + + + + \ No newline at end of file diff --git a/src/TableStorage.Protobuf/TableStorage.Protobuf.csproj b/src/TableStorage.Protobuf/TableStorage.Protobuf.csproj index 45bf387..7a8a517 100644 --- a/src/TableStorage.Protobuf/TableStorage.Protobuf.csproj +++ b/src/TableStorage.Protobuf/TableStorage.Protobuf.csproj @@ -4,12 +4,6 @@ Devlooped.TableStorage.Protobuf netstandard2.0;net6.0 true - A Protocol Buffers binary serializer for use with document-based repositories. - -Usage: - - var repo = DocumentRepository.Create<Product>(storageAccount, serializer: ProtobufDocumentSerializer.Default); - diff --git a/src/TableStorage.Protobuf/readme.md b/src/TableStorage.Protobuf/readme.md new file mode 100644 index 0000000..773b369 --- /dev/null +++ b/src/TableStorage.Protobuf/readme.md @@ -0,0 +1,13 @@ +A Protocol Buffers binary serializer for use with document-based repositories. + +Usage: + +```csharp + var repo = DocumentRepository.Create(..., serializer: ProtobufDocumentSerializer.Default); +``` + + + + + + \ No newline at end of file diff --git a/src/TableStorage.Source/TableStorage.Source.csproj b/src/TableStorage.Source/TableStorage.Source.csproj index e45fc8b..d3e864f 100644 --- a/src/TableStorage.Source/TableStorage.Source.csproj +++ b/src/TableStorage.Source/TableStorage.Source.csproj @@ -6,10 +6,6 @@ true false true - - Repository pattern with POCO object (including C# 9 records!) support for storing to Azure/Cosmos DB Table Storage. - - readme.md @@ -25,7 +21,6 @@ - diff --git a/src/TableStorage.Source/readme.md b/src/TableStorage.Source/readme.md new file mode 100644 index 0000000..d35cda1 --- /dev/null +++ b/src/TableStorage.Source/readme.md @@ -0,0 +1,6 @@ +Source-only version of [TableStorage](https://www.nuget.org/packages/Devlooped.TableStorage). + + + + + \ No newline at end of file diff --git a/src/TableStorage/TableStorage.csproj b/src/TableStorage/TableStorage.csproj index 7d3971e..6173a83 100644 --- a/src/TableStorage/TableStorage.csproj +++ b/src/TableStorage/TableStorage.csproj @@ -4,10 +4,6 @@ Devlooped.TableStorage netstandard2.0;netstandard2.1;net6.0 true - - Repository pattern with POCO object (including C# 9 records!) support for storing to Azure/Cosmos DB Table Storage. - - readme.md @@ -22,7 +18,6 @@ - diff --git a/src/TableStorage/readme.md b/src/TableStorage/readme.md new file mode 100644 index 0000000..0a7fa44 --- /dev/null +++ b/src/TableStorage/readme.md @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/Tests/Sample.cs b/src/Tests/Sample.cs index 6d243b8..c184136 100644 --- a/src/Tests/Sample.cs +++ b/src/Tests/Sample.cs @@ -86,21 +86,11 @@ public enum BookFormat { Paperback, Hardback } public record Book(string ISBN, string Title, string Author, BookFormat Format, int Pages, bool IsPublished = true); - class Product + public record Product(string Category, string Id) { - public Product(string category, string id) - { - Category = category; - Id = id; - } - - public string Category { get; } - - public string Id { get; } - - public string? Title { get; set; } - - public double Price { get; set; } + public string? Title { get; init; } + public double Price { get; init; } + public DateOnly CreatedAt { get; init; } } } }