Skip to content

Commit

Permalink
Add support for enhanced filtering for document-based repo
Browse files Browse the repository at this point in the history
We now need a new document-based specific interface since it will have a new method (EnumerateAsync) which is exclusive for documents. So we rename the old interfaces to `ITableStorage` and `ITableStoragePartition`, which now become the base interfaces for both, and introduce the new `IDocumentRepository` and `IDocumentPartition` which basically add the new enumeration with a filtering expression.

The implementation for documents relies on the underlying `TableQuery`, since we are persisting actual `TableEntity` objects and therefore don't need any of the shenanigans from our POCO repos.

Fixes #37
  • Loading branch information
kzu committed Jun 16, 2021
1 parent 74454e6 commit 4825646
Show file tree
Hide file tree
Showing 20 changed files with 273 additions and 62 deletions.
4 changes: 2 additions & 2 deletions src/TableStorage/AttributedDocumentRepository`1.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@
namespace Devlooped
{
/// <summary>
/// A <see cref="IDocumentRepository{T}"/> implementation which relies on the entity type <typeparamref name="T"/>
/// An <see cref="IDocumentRepository{T}{T}"/> implementation which relies on the entity type <typeparamref name="T"/>
/// being annotated with <see cref="PartitionKeyAttribute"/> and <see cref="RowKeyAttribute"/>, and
/// optionally <see cref="TableAttribute"/> (defaults to type name).
/// </summary>
/// <remarks>
/// When attributed entities are used, this is a convenient generic implementation for use with
/// a dependency injection container, such as in ASP.NET Core:
/// <code>
/// services.AddScoped(typeof(ITableRepository&lt;&gt;), typeof(AttributedDocumentRepository&lt;&gt;));
/// services.AddScoped(typeof(IDocumentRepository&lt;&gt;), typeof(AttributedDocumentRepository&lt;&gt;));
/// </code>
/// </remarks>
partial class AttributedDocumentRepository<T> : DocumentRepository<T> where T : class
Expand Down
2 changes: 1 addition & 1 deletion src/TableStorage/AttributedTableRepository`1.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
namespace Devlooped
{
/// <summary>
/// A <see cref="IDocumentRepository{T}"/> implementation which relies on the entity type <typeparamref name="T"/>
/// An <see cref="ITableRepository{T}"/> implementation which relies on the entity type <typeparamref name="T"/>
/// being annotated with <see cref="PartitionKeyAttribute"/> and <see cref="RowKeyAttribute"/>, and
/// optionally <see cref="TableAttribute"/> (defaults to type name).
/// </summary>
Expand Down
14 changes: 7 additions & 7 deletions src/TableStorage/DocumentPartition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
namespace Devlooped
{
/// <summary>
/// Factory methods to create <see cref="IDocumentRepository{T}"/> instances
/// Factory methods to create <see cref="ITableRepository{T}"/> instances
/// that store entities as a serialized document.
/// </summary>
static partial class DocumentPartition
Expand All @@ -22,30 +22,30 @@ static partial class DocumentPartition
public const string DefaultTableName = "Documents";

/// <summary>
/// Creates an <see cref="IDocumentPartition{T}"/> for the given entity type
/// Creates an <see cref="ITablePartition{T}"/> for the given entity type
/// <typeparamref name="T"/>, using <see cref="DefaultTableName"/> as the table name and the
/// <typeparamref name="T"/> <c>Name</c> as the partition key.
/// </summary>
/// <typeparam name="T">The type of entity that the repository will manage.</typeparam>
/// <param name="storageAccount">The storage account to use.</param>
/// <param name="rowKey">Function to retrieve the row key for a given entity.</param>
/// <returns>The new <see cref="IDocumentPartition{T}"/>.</returns>
/// <returns>The new <see cref="ITablePartition{T}"/>.</returns>
public static IDocumentPartition<T> Create<T>(
CloudStorageAccount storageAccount,
Func<T, string> rowKey,
IBinaryDocumentSerializer? serializer = default) where T : class
=> Create<T>(storageAccount, DefaultTableName, typeof(T).Name, rowKey);

/// <summary>
/// Creates an <see cref="IDocumentPartition{T}"/> for the given entity type
/// Creates an <see cref="ITablePartition{T}"/> for the given entity type
/// <typeparamref name="T"/>, using the given table name and the
/// <typeparamref name="T"/> <c>Name</c> as the partition key.
/// </summary>
/// <typeparam name="T">The type of entity that the repository will manage.</typeparam>
/// <param name="storageAccount">The storage account to use.</param>
/// <param name="tableName">Table name to use.</param>
/// <param name="rowKey">Function to retrieve the row key for a given entity.</param>
/// <returns>The new <see cref="IDocumentPartition{T}"/>.</returns>
/// <returns>The new <see cref="ITablePartition{T}"/>.</returns>
public static IDocumentPartition<T> Create<T>(
CloudStorageAccount storageAccount,
string tableName,
Expand All @@ -54,7 +54,7 @@ public static IDocumentPartition<T> Create<T>(
=> Create<T>(storageAccount, tableName, default, rowKey);

/// <summary>
/// Creates an <see cref="IDocumentPartition{T}"/> for the given entity type
/// Creates an <see cref="ITablePartition{T}"/> for the given entity type
/// <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">The type of entity that the repository will manage.</typeparam>
Expand All @@ -65,7 +65,7 @@ public static IDocumentPartition<T> Create<T>(
/// If not provided, the <typeparamref name="T"/> <c>Name</c> will be used.</param>
/// <param name="rowKey">Optional function to retrieve the row key for a given entity.
/// If not provided, the class will need a property annotated with <see cref="RowKeyAttribute"/>.</param>
/// <returns>The new <see cref="IDocumentPartition{T}"/>.</returns>
/// <returns>The new <see cref="ITablePartition{T}"/>.</returns>
public static IDocumentPartition<T> Create<T>(
CloudStorageAccount storageAccount,
string? tableName = default,
Expand Down
7 changes: 6 additions & 1 deletion src/TableStorage/DocumentPartition`1.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.Cosmos.Table;
Expand All @@ -11,7 +12,7 @@ namespace Devlooped
/// <inheritdoc />
partial class DocumentPartition<T> : IDocumentPartition<T> where T : class
{
readonly IDocumentRepository<T> repository;
readonly DocumentRepository<T> repository;

/// <summary>
/// Initializes the repository with the given storage account and optional table name.
Expand Down Expand Up @@ -50,6 +51,10 @@ public Task DeleteAsync(string rowKey, CancellationToken cancellation = default)
public IAsyncEnumerable<T> EnumerateAsync(CancellationToken cancellation = default)
=> repository.EnumerateAsync(PartitionKey, cancellation);

/// <inheritdoc />
public IAsyncEnumerable<T> EnumerateAsync(Expression<Func<IDocumentEntity, bool>> predicate, CancellationToken cancellation = default)
=> repository.EnumerateAsync(e => e.PartitionKey == PartitionKey, cancellation);

/// <inheritdoc />
public Task<T?> GetAsync(string rowKey, CancellationToken cancellation = default)
=> repository.GetAsync(PartitionKey, rowKey, cancellation);
Expand Down
6 changes: 3 additions & 3 deletions src/TableStorage/DocumentRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
namespace Devlooped
{
/// <summary>
/// Factory methods to create <see cref="IDocumentRepository{T}"/> instances
/// Factory methods to create <see cref="ITableRepository{T}"/> instances
/// that store entities as a serialized document.
/// </summary>
static partial class DocumentRepository
{
/// <summary>
/// Creates an <see cref="IDocumentRepository{T}"/> for the given entity type
/// Creates an <see cref="ITableRepository{T}"/> for the given entity type
/// <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">The type of entity that the repository will manage.</typeparam>
Expand All @@ -23,7 +23,7 @@ static partial class DocumentRepository
/// If not provided, the class will need a property annotated with <see cref="PartitionKeyAttribute"/>.</param>
/// <param name="rowKey">Optional function to retrieve the row key for a given entity.
/// If not provided, the class will need a property annotated with <see cref="RowKeyAttribute"/>.</param>
/// <returns>The new <see cref="IDocumentRepository{T}"/>.</returns>
/// <returns>The new <see cref="ITableRepository{T}"/>.</returns>
public static IDocumentRepository<T> Create<T>(
CloudStorageAccount storageAccount,
string? tableName = default,
Expand Down
76 changes: 54 additions & 22 deletions src/TableStorage/DocumentRepository`1.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
Expand All @@ -12,7 +14,9 @@ namespace Devlooped
/// <inheritdoc />
partial class DocumentRepository<T> : IDocumentRepository<T> where T : class
{
static readonly string documentVersion = (typeof(T).Assembly.GetName().Version ?? new Version(1, 0)).ToString(2);
static readonly string documentVersion;
static readonly int documentMajorVersion;
static readonly int documentMinorVersion;

readonly CloudStorageAccount storageAccount;

Expand All @@ -23,10 +27,18 @@ partial class DocumentRepository<T> : IDocumentRepository<T> where T : class
readonly Func<T, string> rowKey;
readonly Task<CloudTable> table;

readonly Func<string?, CancellationToken, IAsyncEnumerable<T>> enumerate;
readonly Func<Expression<Func<IDocumentEntity, bool>>?, CancellationToken, IAsyncEnumerable<T>> enumerate;
readonly Func<string, string, CancellationToken, Task<T?>> get;
readonly Func<T, CancellationToken, Task<T>> put;

static DocumentRepository()
{
var version = (typeof(T).Assembly.GetName().Version ?? new Version(1, 0));
documentVersion = version.ToString(2);
documentMajorVersion = version.Major;
documentMinorVersion = version.Minor;
}

/// <summary>
/// Initializes the table repository.
/// </summary>
Expand Down Expand Up @@ -91,8 +103,12 @@ await table.ExecuteAsync(TableOperation.Delete(
}

/// <inheritdoc />
public IAsyncEnumerable<T> EnumerateAsync(string? partitionKey = default, CancellationToken cancellation = default)
=> enumerate(partitionKey, cancellation);
public IAsyncEnumerable<T> EnumerateAsync(string? partitionKey = default, CancellationToken cancellation = default)
=> enumerate(partitionKey == null ? null : e => e.PartitionKey == partitionKey, cancellation);

/// <inheritdoc />
public IAsyncEnumerable<T> EnumerateAsync(Expression<Func<IDocumentEntity, bool>> predicate, CancellationToken cancellation = default)
=> enumerate(predicate, cancellation);

/// <inheritdoc />
public Task<T?> GetAsync(string partitionKey, string rowKey, CancellationToken cancellation = default)
Expand All @@ -104,12 +120,16 @@ public Task<T> PutAsync(T entity, CancellationToken cancellation = default)

#region Binary

async IAsyncEnumerable<T> EnumerateBinaryAsync(string? partitionKey = default, [EnumeratorCancellation] CancellationToken cancellation = default)
async IAsyncEnumerable<T> EnumerateBinaryAsync(Expression<Func<IDocumentEntity, bool>>? predicate, [EnumeratorCancellation] CancellationToken cancellation = default)
{
var table = await this.table.ConfigureAwait(false);
var query = new TableQuery<BinaryDocumentEntity>();
if (partitionKey != null)
query = query.Where(TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, partitionKey));
var query = table.CreateQuery<BinaryDocumentEntity>();

if (predicate != null)
{
var expression = Expression.Lambda<Func<BinaryDocumentEntity, bool>>(predicate.Body, Expression.Parameter(typeof(BinaryDocumentEntity)));
query = (TableQuery<BinaryDocumentEntity>)((IQueryable<BinaryDocumentEntity>)query).Where(expression);
}

TableContinuationToken? continuation = null;
do
Expand Down Expand Up @@ -158,8 +178,10 @@ async Task<T> PutBinaryAsync(T entity, CancellationToken cancellation = default)
{
ETag = "*",
Document = binarySerializer!.Serialize(entity),
DocumentType = typeof(T).FullName,
DocumentVersion = documentVersion,
Type = typeof(T).FullName,
Version = documentVersion,
MajorVersion = documentMajorVersion,
MinorVersion = documentMinorVersion,
}), cancellation)
.ConfigureAwait(false);

Expand All @@ -174,12 +196,16 @@ async Task<T> PutBinaryAsync(T entity, CancellationToken cancellation = default)

#region String

async IAsyncEnumerable<T> EnumerateStringAsync(string? partitionKey = default, [EnumeratorCancellation] CancellationToken cancellation = default)
async IAsyncEnumerable<T> EnumerateStringAsync(Expression<Func<IDocumentEntity, bool>>? predicate, [EnumeratorCancellation] CancellationToken cancellation = default)
{
var table = await this.table.ConfigureAwait(false);
var query = new TableQuery<DocumentEntity>();
if (partitionKey != null)
query = query.Where(TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, partitionKey));
var query = table.CreateQuery<DocumentEntity>();

if (predicate != null)
{
var expression = Expression.Lambda<Func<DocumentEntity, bool>>(predicate.Body, Expression.Parameter(typeof(DocumentEntity)));
query = (TableQuery<DocumentEntity>)((IQueryable<DocumentEntity>)query).Where(expression);
}

TableContinuationToken? continuation = null;
do
Expand Down Expand Up @@ -228,8 +254,10 @@ async Task<T> PutStringAsync(T entity, CancellationToken cancellation = default)
{
ETag = "*",
Document = stringSerializer!.Serialize(entity),
DocumentType = typeof(T).FullName,
DocumentVersion = documentVersion,
Type = typeof(T).FullName,
Version = documentVersion,
MajorVersion = documentMajorVersion,
MinorVersion = documentMinorVersion,
}), cancellation)
.ConfigureAwait(false);

Expand All @@ -250,22 +278,26 @@ async Task<CloudTable> GetTableAsync(string tableName)
return table;
}

class BinaryDocumentEntity : TableEntity
class BinaryDocumentEntity : TableEntity, IDocumentEntity
{
public BinaryDocumentEntity() { }
public BinaryDocumentEntity(string partitionKey, string rowKey) : base(partitionKey, rowKey) { }
public byte[]? Document { get; set; }
public string? DocumentType { get; set; }
public string? DocumentVersion { get; set; }
public string? Type { get; set; }
public string? Version { get; set; }
public int? MajorVersion { get; set; }
public int? MinorVersion { get; set; }
}

class DocumentEntity : TableEntity
class DocumentEntity : TableEntity, IDocumentEntity
{
public DocumentEntity() { }
public DocumentEntity(string partitionKey, string rowKey) : base(partitionKey, rowKey) { }
public string? Document { get; set; }
public string? DocumentType { get; set; }
public string? DocumentVersion { get; set; }
public string? Type { get; set; }
public string? Version { get; set; }
public int? MajorVersion { get; set; }
public int? MinorVersion { get; set; }
}
}
}
32 changes: 32 additions & 0 deletions src/TableStorage/IDocumentEntity.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//<auto-generated/>
#nullable enable
using Microsoft.Azure.Cosmos.Table;

namespace Devlooped
{
/// <summary>
/// Document metadata for <see cref="IDocumentRepository{T}"/> querying purposes.
/// </summary>
partial interface IDocumentEntity : ITableEntity
{
/// <summary>
/// The type of the document, its <see cref="System.Type.FullName"/>.
/// </summary>
string? Type { get; }

/// <summary>
/// The major.minor version of the assembly the document type belongs to.
/// </summary>
string? Version { get; }

/// <summary>
/// The major component of the <see cref="Version"/>.
/// </summary>
int? MajorVersion { get; }

/// <summary>
/// The minor component of the <see cref="Version"/>.
/// </summary>
int? MinorVersion { get; }
}
}
31 changes: 31 additions & 0 deletions src/TableStorage/IDocumentPartition`1.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//<auto-generated/>
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Threading;

namespace Devlooped
{
/// <summary>
/// A specific partition within an <see cref="IDocumentRepository{T}"/>.
/// </summary>
/// <typeparam name="T">The type of entity being persisted.</typeparam>
partial interface IDocumentPartition<T> : ITableStoragePartition<T> where T : class
{
/// <summary>
/// Queries the document repository for items that match the given <paramref name="predicate"/>.
/// </summary>
/// <example>
/// var books = DocumentPartition.Create&lt;Book&gt;();
/// await foreach (var book in books.EnumerateAsync(x =>
/// x.PartitionKey == "Rick Riordan" &&
/// x.RowKey.CompareTo("Percy Jackson") >= 0 &&
/// x.Version == "1.0"))
/// {
/// Console.WriteLine(book.ISBN);
/// }
/// </example>
public IAsyncEnumerable<T> EnumerateAsync(Expression<Func<IDocumentEntity, bool>> predicate, CancellationToken cancellation = default);
}
}
29 changes: 29 additions & 0 deletions src/TableStorage/IDocumentRepository`1.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//<auto-generated/>
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Threading;

namespace Devlooped
{
/// <summary>
/// A generic repository that stores entities in table storage as serialized
/// documents.
/// </summary>
/// <typeparam name="T">The type of entity being persisted.</typeparam>
partial interface IDocumentRepository<T> : ITableStorage<T> where T : class
{
/// <summary>
/// Queries the document repository for items that match the given <paramref name="predicate"/>.
/// </summary>
/// <example>
/// var books = DocumentRepository.Create&lt;Book&gt;();
/// await foreach (var book in books.EnumerateAsync(x => x.PartitionKey == "Rick Riordan" && x.DocumentType ))
/// {
/// Console.WriteLine(book.ISBN);
/// }
/// </example>
public IAsyncEnumerable<T> EnumerateAsync(Expression<Func<IDocumentEntity, bool>> predicate, CancellationToken cancellation = default);
}
}
Loading

0 comments on commit 4825646

Please sign in to comment.