-
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for querying with LINQ and expressions
This commit introduces support for LINQ querying a table repository. Unlike the document-based one, the table repository persists entities in individual columns that can therefore be queried. The document-based repository is now a separte common base interface for both queryable and non-queryable repos. The main usage is very similar to the built-in API, where you invoke `CreateQuery` and then perform LINQ queries over it. There are obvious restrictions on what works and doesn't, and it's documented at https://docs.microsoft.com/en-us/rest/api/storageservices/query-operators-supported-for-the-table-service. The built-in implementation for querying is deeply intertwined with the ITableEntity interface, to the point that it's effectively unsable for our purposes. No amount of trial/hacking could get over the fact that TableQuery<T> requires the T to implement ITableEntity. Short of generating dynamic proxies (a seriously complex approach), we simply cannot use that. Luckily, it's all OData in the end, so if you build the right URIs (including headers and signatures), you don't actually need it. That includes properly parsing the continuation token headers. So we take a dependency on OData for the URI building capabilities, but otherwise execute the queries ourselves using plain HttpClient. The same deserialization support configured for the other operations on the repo is used, so the behavior is indistinguishable from EnumerateAsync. An extension method `EnumerateAsync(predicate)` is also provided now, which might provide a simpler API surface for simple Where-style filtering. Finally, the "native" way of consuming these queries is `IAsyncEnumerable<T>`, but the LINQ querying works over `IQueryable<T>` so there is a need to bridge the two. There is some discussion in the dotnet org about this, since EF Core provides similar capabilities, but nothing exists in the platform yet. So we provide a simple `GetAsyncEnumerator` extension method for `IQueryable<T>` to bridge that gap, for use with built-in `await foreach` over the query. For tests, we just `cat` the async enumerable extensions for convenience, from https://github.com/devlooped/catbag/blob/main/System/Collections/Generic/IAsyncEnumerableExtensions.cs. Fixes #33.
- Loading branch information
Showing
30 changed files
with
871 additions
and
92 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
//<auto-generated/> | ||
#nullable enable | ||
using System; | ||
using System.Globalization; | ||
using System.Linq; | ||
using System.Net.Http; | ||
using System.Net.Http.Headers; | ||
using System.Runtime.InteropServices; | ||
using System.Security.Cryptography; | ||
using System.Text; | ||
using Microsoft.Azure.Cosmos.Table; | ||
|
||
namespace Devlooped | ||
{ | ||
static class Http | ||
{ | ||
public static HttpClient Client { get; } | ||
|
||
static Http() | ||
{ | ||
Client = new HttpClient(); | ||
Client.DefaultRequestHeaders.Add("Accept", "application/json; odata=nometadata"); | ||
Client.DefaultRequestHeaders.Add("Accept-Charset", "UTF-8"); | ||
Client.DefaultRequestHeaders.Add("User-Agent", $"Azure-Cosmos-Table/1.0.8 ({RuntimeInformation.FrameworkDescription}; {Environment.OSVersion.Platform} {Environment.OSVersion.Version})"); | ||
Client.DefaultRequestHeaders.Add("DataServiceVersion", "3.0;NetFx"); | ||
Client.DefaultRequestHeaders.Add("MaxDataServiceVersion", "3.0;NetFx"); | ||
Client.DefaultRequestHeaders.Add("x-ms-version", "2017-07-29"); | ||
} | ||
|
||
public static HttpRequestMessage AddAuthorizationHeader(this HttpRequestMessage request, CloudStorageAccount account) | ||
{ | ||
if (!request.Headers.TryGetValues("x-ms-date", out var values) || | ||
values.FirstOrDefault() is not string date || | ||
string.IsNullOrEmpty(date)) | ||
{ | ||
date = DateTime.UtcNow.ToString("R", CultureInfo.InvariantCulture); | ||
request.Headers.Add("x-ms-date", date); | ||
} | ||
|
||
var resource = request.RequestUri.GetComponents(UriComponents.Path, UriFormat.Unescaped); | ||
var toSign = string.Format("{0}\n/{1}/{2}", | ||
request.Headers.GetValues("x-ms-date").First(), | ||
account.Credentials.AccountName, | ||
resource.TrimStart('/')); | ||
|
||
var hasher = new HMACSHA256(Convert.FromBase64String(account.Credentials.Key)); | ||
var signature = hasher.ComputeHash(Encoding.UTF8.GetBytes(toSign)); | ||
var authentication = new AuthenticationHeaderValue("SharedKeyLite", account.Credentials.AccountName + ":" + Convert.ToBase64String(signature)); | ||
|
||
request.Headers.Authorization = authentication; | ||
|
||
return request; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
//<auto-generated/> | ||
#nullable enable | ||
using System; | ||
using System.Collections.Generic; | ||
using System.ComponentModel; | ||
using System.Linq; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
|
||
namespace Devlooped | ||
{ | ||
/// <summary> | ||
/// Extension method <see cref="GetAsyncEnumerator{TSource}(IQueryable{TSource}, CancellationToken)"/> to | ||
/// allow native <c>await foreach</c> support for <see cref="IQueryable{T}"/> queries created from | ||
/// the <see cref="ITableRepository{T}.CreateQuery"/> or <see cref="ITablePartition{T}.CreateQuery"/>, which | ||
/// implement <see cref="IAsyncEnumerable{T}"/>. | ||
/// </summary> | ||
[EditorBrowsable(EditorBrowsableState.Never)] | ||
static partial class IQueryableExtensions | ||
{ | ||
/// <summary> | ||
/// Gets the <see cref="IAsyncEnumerator{T}"/> for an <see cref="IQueryable{T}"/> that | ||
/// implements <see cref="IAsyncEnumerable{T}"/>, for use with <c>await foreach</c>. | ||
/// </summary> | ||
/// <exception cref="ArgumentException">The <paramref name="source"/> does not implement | ||
/// <see cref="IAsyncEnumerable{T}"/>.</exception> | ||
public static IAsyncEnumerator<TSource> GetAsyncEnumerator<TSource>(this IQueryable<TSource> source, CancellationToken cancellation = default) | ||
{ | ||
if (source is not IAsyncEnumerable<TSource> enumerable) | ||
throw new ArgumentException("The source it not an async enumerable.", nameof(source)); | ||
|
||
return enumerable.GetAsyncEnumerator(cancellation); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
//<auto-generated/> | ||
#nullable enable | ||
using System.Linq; | ||
|
||
namespace Devlooped | ||
{ | ||
/// <summary> | ||
/// A specific partition within an <see cref="ITableRepository{T}"/>, which allows querying | ||
/// by the entity properties, since they are stored in individual columns. | ||
/// </summary> | ||
/// <typeparam name="T">The type of entity being persisted.</typeparam> | ||
partial interface ITablePartition<T> : IDocumentPartition<T> where T : class | ||
{ | ||
/// <summary> | ||
/// Creates a query for use with LINQ expressions. See | ||
/// <see ref="https://docs.microsoft.com/en-us/rest/api/storageservices/query-operators-supported-for-the-table-service">supported operators</see>. | ||
/// </summary> | ||
/// <remarks> | ||
/// The query is scoped to the current partition already. | ||
/// </remarks> | ||
/// <example> | ||
/// var books = TablePartition.Create<Book>("Bestsellers"); | ||
/// await foreach (var book in books.CreateQuery().Where(x => x.IsPublished)) | ||
/// { | ||
/// Console.WriteLine(book.ISBN); | ||
/// } | ||
/// </example> | ||
/// <example> | ||
/// var books = TablePartition.Create<Book>("Bestsellers"); | ||
/// await foreach (var published in from book in books.CreateQuery() | ||
/// where book.IsPublished && book.Pages > 1000 | ||
/// select book) | ||
/// { | ||
/// Console.WriteLine(published.ISBN); | ||
/// } | ||
/// </example> | ||
IQueryable<T> CreateQuery(); | ||
} | ||
} |
Oops, something went wrong.