Skip to content

Commit

Permalink
Add support for filtering by enum property types
Browse files Browse the repository at this point in the history
"Fix" this support in CosmosDB table client, which does not support this type of query :).

Fixes #35
  • Loading branch information
kzu committed Jun 16, 2021
1 parent 24a669e commit 6b46101
Show file tree
Hide file tree
Showing 3 changed files with 40 additions and 37 deletions.
5 changes: 5 additions & 0 deletions src/TableStorage/DocumentSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ partial class DocumentSerializer : IStringDocumentSerializer
AllowTrailingCommas = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault,
WriteIndented = true,
Converters =
{
// Enums should persist/parse with their string values instead
new JsonStringEnumConverter(allowIntegerValues: false)
}
};

/// <summary>
Expand Down
10 changes: 9 additions & 1 deletion src/TableStorage/TableRepositoryQuery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,18 @@ public async IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellati
var query = (DataServiceQuery)new DataServiceContext(account.TableStorageUri.PrimaryUri).CreateQuery<T>(tableName)
.Provider.CreateQuery(expression);

// OData will translate the enum value in a filter to TYPENAME.'ENUMVALUE'.
// The type name can contain the + sign if it's a nested type, too. So we
// need to remove the type name plus the dot and just leave the string
// value as part of the filter string.
var rawqs = Regex.Replace(
query.RequestUri.GetComponents(UriComponents.Query, UriFormat.Unescaped),
"(\\W)[\\w\\+\\.]+('\\w+')", "$1$2");

// We need to count & break manually because $top is interpreted as the max records per page
// if the set matches more items. This is clearly unintuitive and *not* what one typically
// wants when using LINQ queries! See Note on https://docs.microsoft.com/en-us/rest/api/storageservices/querying-tables-and-entities#supported-query-options
var qs = HttpUtility.ParseQueryString(query.RequestUri.GetComponents(UriComponents.Query, UriFormat.Unescaped));
var qs = HttpUtility.ParseQueryString(rawqs);
if (!long.TryParse(qs["$top"], out var top))
top = -1;

Expand Down
62 changes: 26 additions & 36 deletions src/Tests/QueryTests.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,9 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Net;
using System.Net.Http;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Azure.Cosmos.Table;
using Microsoft.OData.Client;
using Microsoft.OData.UriParser;
using Xunit;

namespace Devlooped
Expand All @@ -27,7 +18,7 @@ public async Task CanTake()
await LoadBooksAsync(repo);

var query = from book in repo.CreateQuery()
where book.Format == "Hardback" && book.IsPublished
where book.Format == BookFormat.Hardback && book.IsPublished
select new { book.ISBN, book.Title };

var result = await query.Take(2).AsAsyncEnumerable().ToListAsync();
Expand Down Expand Up @@ -58,7 +49,7 @@ public async Task CanProject()
var hasResults = false;

await foreach (var info in from book in repo.CreateQuery()
where book.Author == "Rick Riordan" && book.Format == "Hardback" && book.IsPublished
where book.Author == "Rick Riordan" && book.Format == BookFormat.Hardback && book.IsPublished
select new { book.ISBN, book.Title })
{
hasResults = true;
Expand All @@ -83,7 +74,7 @@ public async Task CanProjectPartition()
var hasResults = false;

await foreach (var info in from book in repo.CreateQuery()
where book.Format == "Hardback" && book.IsPublished
where book.Format == BookFormat.Hardback && book.IsPublished
select new { book.ISBN, book.Title })
{
hasResults = true;
Expand All @@ -94,42 +85,41 @@ public async Task CanProjectPartition()
Assert.True(hasResults);
}

[Fact]
public async Task EnumFailsInTableClient()
{
var account = CloudStorageAccount.DevelopmentStorageAccount;
var table = account.CreateCloudTableClient().GetTableReference(nameof(EnumFailsInTableClient));

await table.CreateIfNotExistsAsync();

Assert.Throws<StorageException>(() =>
(from book in table.CreateQuery<BookEntity>()
where book.Format == BookFormat.Hardback && book.IsPublished
select new { book.Title })
.ToList());
}


async Task LoadBooksAsync(ITableRepository<Book> books)
{
foreach (var book in File.ReadAllLines("Books.csv").Skip(1)
.Select(line => line.Split(','))
.Select(values => new Book(values[1], values[2], values[3], values[4], int.Parse(values[5]), bool.Parse(values[6]))))
.Select(values => new Book(values[1], values[2], values[3], Enum.Parse<BookFormat>(values[4]), int.Parse(values[5]), bool.Parse(values[6]))))
{
await books.PutAsync(book);
}
}

public record Book(string ISBN, string Title, string Author, string Format, int? Pages = null, bool IsPublished = true);
public enum BookFormat { Paperback, Hardback }

public class RequestStatusEntity : TableEntity
{
public RequestStatusEntity()
{
}

public RequestStatusEntity(string ID) : base(nameof(RequestStatus), ID)
{
}

public string? ID
{
get => RowKey;
set => RowKey = value;
}

public int Status { get; set; }
public string? Reason { get; set; }
}
public record Book(string ISBN, string Title, string Author, BookFormat Format, int? Pages = null, bool IsPublished = true);

public record RequestStatus(string ID)
public class BookEntity : TableEntity
{
public int Status { get; set; }
public string? Reason { get; set; }
public bool IsPublished { get; set; }
public string? Title { get; set; }
public BookFormat Format { get; set; }
}
}
}

0 comments on commit 6b46101

Please sign in to comment.