From d809f1a6d2662a399fa9f690402ac38f91c839ef Mon Sep 17 00:00:00 2001 From: Dave Melendez Date: Tue, 26 Apr 2022 21:45:00 -0500 Subject: [PATCH] ElCamino.Azure.Data.Tables refactor --- ElCamino.AspNetCore.Identity.AzureTable.sln | 8 + README.md | 24 +-- ...mino.AspNetCore.Identity.AzureTable.csproj | 3 +- .../Helpers/AzureSdkHelper.cs | 37 ---- .../Helpers/Odata/EdmType.cs | 19 -- .../Helpers/Odata/QueryComparisons.cs | 39 ---- .../Helpers/Odata/TableOperators.cs | 24 --- .../IdentityCloudContext.cs | 2 +- .../BatchOperationHelper.cs | 172 +++++++++--------- src/ElCamino.Azure.Data.Tables/EdmType.cs | 36 ++++ .../ElCamino.Azure.Data.Tables.csproj | 51 ++++++ .../EntityMapExtensions.cs} | 10 +- .../IAsyncEnumerableExtensions.cs | 29 ++- .../QueryComparisons.cs | 56 ++++++ .../TableOperators.cs | 37 ++++ .../TableQuery.cs | 24 ++- .../projectNugetPic.png | Bin 0 -> 10619 bytes 17 files changed, 325 insertions(+), 246 deletions(-) delete mode 100644 src/ElCamino.AspNetCore.Identity.AzureTable/Helpers/AzureSdkHelper.cs delete mode 100644 src/ElCamino.AspNetCore.Identity.AzureTable/Helpers/Odata/EdmType.cs delete mode 100644 src/ElCamino.AspNetCore.Identity.AzureTable/Helpers/Odata/QueryComparisons.cs delete mode 100644 src/ElCamino.AspNetCore.Identity.AzureTable/Helpers/Odata/TableOperators.cs rename src/{ElCamino.AspNetCore.Identity.AzureTable/Helpers => ElCamino.Azure.Data.Tables}/BatchOperationHelper.cs (92%) create mode 100644 src/ElCamino.Azure.Data.Tables/EdmType.cs create mode 100644 src/ElCamino.Azure.Data.Tables/ElCamino.Azure.Data.Tables.csproj rename src/{ElCamino.AspNetCore.Identity.AzureTable/Helpers/EntityMapHelper.cs => ElCamino.Azure.Data.Tables/EntityMapExtensions.cs} (85%) rename src/{ElCamino.AspNetCore.Identity.AzureTable/Helpers => ElCamino.Azure.Data.Tables}/IAsyncEnumerableExtensions.cs (73%) create mode 100644 src/ElCamino.Azure.Data.Tables/QueryComparisons.cs create mode 100644 src/ElCamino.Azure.Data.Tables/TableOperators.cs rename src/{ElCamino.AspNetCore.Identity.AzureTable/Helpers/Odata => ElCamino.Azure.Data.Tables}/TableQuery.cs (90%) create mode 100644 src/ElCamino.Azure.Data.Tables/projectNugetPic.png diff --git a/ElCamino.AspNetCore.Identity.AzureTable.sln b/ElCamino.AspNetCore.Identity.AzureTable.sln index 86ed090..8953596 100644 --- a/ElCamino.AspNetCore.Identity.AzureTable.sln +++ b/ElCamino.AspNetCore.Identity.AzureTable.sln @@ -21,6 +21,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ElCamino.AspNetCore.Identit EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ElCamino.AspNetCore.Identity.AzureTable.Model", "src\ElCamino.AspNetCore.Identity.AzureTable.Model\ElCamino.AspNetCore.Identity.AzureTable.Model.csproj", "{C90FB0E3-5BE2-41E7-9DA3-9083C7700F91}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ElCamino.Azure.Data.Tables", "src\ElCamino.Azure.Data.Tables\ElCamino.Azure.Data.Tables.csproj", "{DAF57676-8534-4A62-BC7B-317753F2A275}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -58,6 +60,12 @@ Global {C90FB0E3-5BE2-41E7-9DA3-9083C7700F91}.Release|Any CPU.Build.0 = Release|Any CPU {C90FB0E3-5BE2-41E7-9DA3-9083C7700F91}.Signed|Any CPU.ActiveCfg = Debug|Any CPU {C90FB0E3-5BE2-41E7-9DA3-9083C7700F91}.Signed|Any CPU.Build.0 = Debug|Any CPU + {DAF57676-8534-4A62-BC7B-317753F2A275}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DAF57676-8534-4A62-BC7B-317753F2A275}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DAF57676-8534-4A62-BC7B-317753F2A275}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DAF57676-8534-4A62-BC7B-317753F2A275}.Release|Any CPU.Build.0 = Release|Any CPU + {DAF57676-8534-4A62-BC7B-317753F2A275}.Signed|Any CPU.ActiveCfg = Debug|Any CPU + {DAF57676-8534-4A62-BC7B-317753F2A275}.Signed|Any CPU.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/README.md b/README.md index 66def15..288d2d0 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,9 @@ -identityazuretable +ElCamino.Azure.Data.Tables ================== -This project provides a high performance cloud solution for ASP.NET Identity Core using Azure Table storage replacing the Entity Framework / MSSQL provider. +Azure Table Storage odata query building and operators from the older Azure Storage SDKs and some other async, mapping, and batch helpers and/or extensions. [![Build Status](https://dev.azure.com/elcamino/Azure%20OpenSource/_apis/build/status/IdentityAzureTableCore?branchName=master)](https://dev.azure.com/elcamino/Azure%20OpenSource/_build/latest?definitionId=4&branchName=master) -[![NuGet Badge](https://buildstats.info/nuget/ElCamino.AspNetCore.Identity.AzureTable)](https://www.nuget.org/packages/ElCamino.AspNetCore.Identity.AzureTable/) -[![NuGet Badge](https://buildstats.info/nuget/ElCamino.AspNet.Identity.AzureTable)](https://www.nuget.org/packages/ElCamino.AspNet.Identity.AzureTable/) +[![NuGet Badge](https://buildstats.info/nuget/ElCamino.Azure.Data.Tables)](https://www.nuget.org/packages/ElCamino.Azure.Data.Tables/) Project site at https://dlmelendez.github.io/identityazuretable/. - -Identity Core latest template -``` -dotnet new --install ElCamino.AspNetCore.Identity.AzureTable.Templates - -#MVC Template -dotnet new mvc-id-azure-tables - -#Razor Pages Template -dotnet new rzp-id-azure-tables -``` - -Identity Core 3.x (uses PageModel - latest) - Use ElCamino.AspNetCore.Identity.AzureTable, sample mvc app: https://github.com/dlmelendez/identityazuretable/tree/master/sample/samplemvccore4 - -Identity Core 2.x (uses PageModel - latest) - Use ElCamino.AspNetCore.Identity.AzureTable, sample mvc app: https://github.com/dlmelendez/identityazuretable/tree/master/sample/samplemvccore3 - -Identity Core 2.x (uses MVC - older) - Use ElCamino.AspNetCore.Identity.AzureTable, sample mvc app: https://github.com/dlmelendez/identityazuretable/tree/master/sample/samplemvccore2 diff --git a/src/ElCamino.AspNetCore.Identity.AzureTable/ElCamino.AspNetCore.Identity.AzureTable.csproj b/src/ElCamino.AspNetCore.Identity.AzureTable/ElCamino.AspNetCore.Identity.AzureTable.csproj index 94b2213..cfe84ec 100644 --- a/src/ElCamino.AspNetCore.Identity.AzureTable/ElCamino.AspNetCore.Identity.AzureTable.csproj +++ b/src/ElCamino.AspNetCore.Identity.AzureTable/ElCamino.AspNetCore.Identity.AzureTable.csproj @@ -6,7 +6,7 @@ Azure Table Storage Provider for ASP.NET Identity Core David Melendez netstandard2.0;net6.0 - 9.0 + 10.0 ElCamino.AspNetCore.Identity.AzureTable ../../tools/Key.snk true @@ -57,6 +57,7 @@ + diff --git a/src/ElCamino.AspNetCore.Identity.AzureTable/Helpers/AzureSdkHelper.cs b/src/ElCamino.AspNetCore.Identity.AzureTable/Helpers/AzureSdkHelper.cs deleted file mode 100644 index cf68dc1..0000000 --- a/src/ElCamino.AspNetCore.Identity.AzureTable/Helpers/AzureSdkHelper.cs +++ /dev/null @@ -1,37 +0,0 @@ -// MIT License Copyright 2020 (c) David Melendez. All rights reserved. See License.txt in the project root for license information. - -using ElCamino.AspNetCore.Identity.AzureTable.Helpers; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Threading.Tasks; - -namespace Azure.Data.Tables -{ - public static class AzureSdkHelper - { - //Azure SDK changes, you are still killing me. - public static async IAsyncEnumerable ExecuteQueryAsync(this TableClient ct, TableQuery tq) - where T : class, ITableEntity, new() - { -#if DEBUG - int iCounter = 0; -#endif - - AsyncPageable segment = ct.QueryAsync(tq.FilterString, tq.TakeCount, tq.SelectColumns); - await foreach (T result in segment.ConfigureAwait(false)) - { -#if DEBUG - iCounter++; -#endif - yield return result; - } - -#if DEBUG - Debug.WriteLine("ExecuteQueryAsync: (Count): {0}", iCounter); - Debug.WriteLine("ExecuteQueryAsync (Query): " + tq.FilterString); -#endif - } - - } -} diff --git a/src/ElCamino.AspNetCore.Identity.AzureTable/Helpers/Odata/EdmType.cs b/src/ElCamino.AspNetCore.Identity.AzureTable/Helpers/Odata/EdmType.cs deleted file mode 100644 index 1a0acbe..0000000 --- a/src/ElCamino.AspNetCore.Identity.AzureTable/Helpers/Odata/EdmType.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace ElCamino.AspNetCore.Identity.AzureTable.Helpers -{ - public enum EdmType - { - Binary, - Boolean, - DateTime, - Double, - Guid, - Int32, - Int64, - String - - } -} diff --git a/src/ElCamino.AspNetCore.Identity.AzureTable/Helpers/Odata/QueryComparisons.cs b/src/ElCamino.AspNetCore.Identity.AzureTable/Helpers/Odata/QueryComparisons.cs deleted file mode 100644 index 0b83e20..0000000 --- a/src/ElCamino.AspNetCore.Identity.AzureTable/Helpers/Odata/QueryComparisons.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace ElCamino.AspNetCore.Identity.AzureTable.Helpers -{ - public static class QueryComparisons - { - /// - /// Represents the Equal operator. - /// - public const string Equal = "eq"; - - /// - /// Represents the Not Equal operator. - /// - public const string NotEqual = "ne"; - - /// - /// Represents the Greater Than operator. - /// - public const string GreaterThan = "gt"; - - /// - /// Represents the Greater Than or Equal operator. - /// - public const string GreaterThanOrEqual = "ge"; - - /// - /// Represents the Less Than operator. - /// - public const string LessThan = "lt"; - - /// - /// Represents the Less Than or Equal operator. - /// - public const string LessThanOrEqual = "le"; - } -} diff --git a/src/ElCamino.AspNetCore.Identity.AzureTable/Helpers/Odata/TableOperators.cs b/src/ElCamino.AspNetCore.Identity.AzureTable/Helpers/Odata/TableOperators.cs deleted file mode 100644 index d6c30e2..0000000 --- a/src/ElCamino.AspNetCore.Identity.AzureTable/Helpers/Odata/TableOperators.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace ElCamino.AspNetCore.Identity.AzureTable.Helpers -{ - /// - /// From https://github.com/Azure/azure-storage-net/blob/v9.3.2/Lib/Common/Table/TableOperators.cs - /// - public static class TableOperators - { - public const string And = "and"; - - /// - /// Represents the Not operator. - /// - public const string Not = "not"; - - /// - /// Represents the Or operator. - /// - public const string Or = "or"; - } -} diff --git a/src/ElCamino.AspNetCore.Identity.AzureTable/IdentityCloudContext.cs b/src/ElCamino.AspNetCore.Identity.AzureTable/IdentityCloudContext.cs index 15838b3..d8a8ba2 100644 --- a/src/ElCamino.AspNetCore.Identity.AzureTable/IdentityCloudContext.cs +++ b/src/ElCamino.AspNetCore.Identity.AzureTable/IdentityCloudContext.cs @@ -1,7 +1,7 @@ // MIT License Copyright 2020 (c) David Melendez. All rights reserved. See License.txt in the project root for license information. using System; -using ElCamino.AspNetCore.Identity.AzureTable.Model; using Azure.Data.Tables; +using ElCamino.AspNetCore.Identity.AzureTable.Model; namespace ElCamino.AspNetCore.Identity.AzureTable { diff --git a/src/ElCamino.AspNetCore.Identity.AzureTable/Helpers/BatchOperationHelper.cs b/src/ElCamino.Azure.Data.Tables/BatchOperationHelper.cs similarity index 92% rename from src/ElCamino.AspNetCore.Identity.AzureTable/Helpers/BatchOperationHelper.cs rename to src/ElCamino.Azure.Data.Tables/BatchOperationHelper.cs index 49ff5e7..36bdd96 100644 --- a/src/ElCamino.AspNetCore.Identity.AzureTable/Helpers/BatchOperationHelper.cs +++ b/src/ElCamino.Azure.Data.Tables/BatchOperationHelper.cs @@ -1,89 +1,83 @@ -// MIT License Copyright 2020 (c) David Melendez. All rights reserved. See License.txt in the project root for license information. - -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Azure; -using Azure.Data.Tables; - -namespace ElCamino.AspNetCore.Identity.AzureTable.Helpers -{ - /// - /// Used to instantiate multiple TableBatchOperations when the - /// TableOperation maximum is reached on a single TableBatchOperation - /// - public class BatchOperationHelper - { - - private readonly Dictionary> _batches = new(); - - private readonly TableClient _table; - public BatchOperationHelper(TableClient table) - { - _table = table; - } - - public virtual void AddEntities(IEnumerable entities) where T : class, ITableEntity, new() - { - foreach(T entity in entities) - { - AddEntity(entity); - } - } - public virtual void AddEntity(T entity) where T : class, ITableEntity, new() - { - GetCurrent(entity.PartitionKey).Add(new TableTransactionAction(TableTransactionActionType.Add, entity)); - } - public virtual void DeleteEntity(string partitionKey, string rowKey, ETag ifMatch = default) - { - GetCurrent(partitionKey).Add(new TableTransactionAction(TableTransactionActionType.Delete, new TableEntity(partitionKey, rowKey),ifMatch)); - } - - public virtual async Task> SubmitBatchAsync(CancellationToken cancellationToken = default) - { - ConcurrentBag bag = new ConcurrentBag(); - List batches = new List(_batches.Count); - foreach(KeyValuePair> kv in _batches) - { - batches.Add(_table.SubmitTransactionAsync(kv.Value, cancellationToken) - .ContinueWith((result) => - { - foreach (var r in result.Result.Value) - { - bag.Add(r); - } - }, cancellationToken)); - } - await Task.WhenAll(batches).ConfigureAwait(false); - Clear(); - return bag; - } - - public virtual void UpdateEntity(T entity, ETag ifMatch, TableUpdateMode mode = TableUpdateMode.Merge) where T : class, ITableEntity, new() - { - GetCurrent(entity.PartitionKey).Add(new TableTransactionAction(mode == TableUpdateMode.Merge? TableTransactionActionType.UpdateMerge : TableTransactionActionType.UpdateReplace, entity, ifMatch)); - } - - public virtual void UpsertEntity(T entity, TableUpdateMode mode = TableUpdateMode.Merge) where T : class, ITableEntity, new() - { - GetCurrent(entity.PartitionKey).Add(new TableTransactionAction(mode == TableUpdateMode.Merge ? TableTransactionActionType.UpsertMerge : TableTransactionActionType.UpsertReplace, entity)); - } - - public void Clear() - { - _batches.Clear(); - } - - private List GetCurrent(string partitionKey) - { - if(!_batches.ContainsKey(partitionKey)) - { - _batches.Add(partitionKey, new List()); - } - - return _batches[partitionKey]; - } - } -} +// MIT License Copyright 2020 (c) David Melendez. All rights reserved. See License.txt in the project root for license information. + +using System.Collections.Concurrent; + +namespace Azure.Data.Tables +{ + /// + /// Used to instantiate multiple TableBatchOperations when the + /// TableOperation maximum is reached on a single TableBatchOperation + /// + public class BatchOperationHelper + { + private readonly Dictionary> _batches = new(); + + private readonly TableClient _table; + + public BatchOperationHelper(TableClient table) + { + _table = table; + } + + public virtual void AddEntities(IEnumerable entities) where T : class, ITableEntity, new() + { + foreach(T entity in entities) + { + AddEntity(entity); + } + } + public virtual void AddEntity(T entity) where T : class, ITableEntity, new() + { + GetCurrent(entity.PartitionKey).Add(new TableTransactionAction(TableTransactionActionType.Add, entity)); + } + public virtual void DeleteEntity(string partitionKey, string rowKey, ETag ifMatch = default) + { + GetCurrent(partitionKey).Add(new TableTransactionAction(TableTransactionActionType.Delete, new TableEntity(partitionKey, rowKey),ifMatch)); + } + + public virtual async Task> SubmitBatchAsync(CancellationToken cancellationToken = default) + { + ConcurrentBag bag = new ConcurrentBag(); + List batches = new List(_batches.Count); + foreach(KeyValuePair> kv in _batches) + { + batches.Add(_table.SubmitTransactionAsync(kv.Value, cancellationToken) + .ContinueWith((result) => + { + foreach (var r in result.Result.Value) + { + bag.Add(r); + } + }, cancellationToken)); + } + await Task.WhenAll(batches).ConfigureAwait(false); + Clear(); + return bag; + } + + public virtual void UpdateEntity(T entity, ETag ifMatch, TableUpdateMode mode = TableUpdateMode.Merge) where T : class, ITableEntity, new() + { + GetCurrent(entity.PartitionKey).Add(new TableTransactionAction(mode == TableUpdateMode.Merge? TableTransactionActionType.UpdateMerge : TableTransactionActionType.UpdateReplace, entity, ifMatch)); + } + + public virtual void UpsertEntity(T entity, TableUpdateMode mode = TableUpdateMode.Merge) where T : class, ITableEntity, new() + { + GetCurrent(entity.PartitionKey).Add(new TableTransactionAction(mode == TableUpdateMode.Merge ? TableTransactionActionType.UpsertMerge : TableTransactionActionType.UpsertReplace, entity)); + } + + public void Clear() + { + _batches.Clear(); + } + + private List GetCurrent(string partitionKey) + { + if(!_batches.ContainsKey(partitionKey)) + { + _batches.Add(partitionKey, new List()); + } + + return _batches[partitionKey]; + } + } +} diff --git a/src/ElCamino.Azure.Data.Tables/EdmType.cs b/src/ElCamino.Azure.Data.Tables/EdmType.cs new file mode 100644 index 0000000..9204e94 --- /dev/null +++ b/src/ElCamino.Azure.Data.Tables/EdmType.cs @@ -0,0 +1,36 @@ +// ----------------------------------------------------------------------------------------- +// +// Copyright 2013 Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// ----------------------------------------------------------------------------------------- + + +namespace Azure.Data.Tables +{ + /// + /// From https://github.com/Azure/azure-storage-net/blob/v9.3.2/Lib/Common/Table/EdmType.cs + /// + public enum EdmType + { + Binary, + Boolean, + DateTime, + Double, + Guid, + Int32, + Int64, + String + + } +} diff --git a/src/ElCamino.Azure.Data.Tables/ElCamino.Azure.Data.Tables.csproj b/src/ElCamino.Azure.Data.Tables/ElCamino.Azure.Data.Tables.csproj new file mode 100644 index 0000000..df8ca97 --- /dev/null +++ b/src/ElCamino.Azure.Data.Tables/ElCamino.Azure.Data.Tables.csproj @@ -0,0 +1,51 @@ + + + + Azure Table Storage odata query building and operators from the older Azure Storage SDKs and some other async, mapping, and batch helpers and/or extensions. + Copyright © 2022 David Melendez, MIT License + Azure Table Storage Extensions + David Melendez + netstandard2.0;net6.0 + 10.0 + ElCamino.Azure.Data.Tables + ../../tools/Key.snk + true + true + ElCamino.Azure.Data.Tables + ASP.NET;Azure;Table Storage;Azure.Data.Tables + Check https://github.com/dlmelendez/identityazuretable/releases for the latest release information. + projectNugetPic.png + + true + git + https://github.com/dlmelendez/identityazuretable.git + True + 6.1 + https://dlmelendez.github.io/identityazuretable + + + MIT + true + snupkg + README.md + en-US + enable + disable + + + + + \ + True + + + + + + + + + + + + diff --git a/src/ElCamino.AspNetCore.Identity.AzureTable/Helpers/EntityMapHelper.cs b/src/ElCamino.Azure.Data.Tables/EntityMapExtensions.cs similarity index 85% rename from src/ElCamino.AspNetCore.Identity.AzureTable/Helpers/EntityMapHelper.cs rename to src/ElCamino.Azure.Data.Tables/EntityMapExtensions.cs index f8a3115..8b14f5a 100644 --- a/src/ElCamino.AspNetCore.Identity.AzureTable/Helpers/EntityMapHelper.cs +++ b/src/ElCamino.Azure.Data.Tables/EntityMapExtensions.cs @@ -1,18 +1,12 @@ // MIT License Copyright 2020 (c) David Melendez. All rights reserved. See License.txt in the project root for license information. -using Azure.Data.Tables; -using System; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; using System.Reflection; using System.Runtime.Serialization; -using System.Text; -using System.Threading.Tasks; -namespace ElCamino.AspNetCore.Identity.AzureTable.Helpers +namespace Azure.Data.Tables { - public static class EntityMapHelper + public static class EntityMapExtensions { private static readonly ConcurrentDictionary TypeProperties = new(); diff --git a/src/ElCamino.AspNetCore.Identity.AzureTable/Helpers/IAsyncEnumerableExtensions.cs b/src/ElCamino.Azure.Data.Tables/IAsyncEnumerableExtensions.cs similarity index 73% rename from src/ElCamino.AspNetCore.Identity.AzureTable/Helpers/IAsyncEnumerableExtensions.cs rename to src/ElCamino.Azure.Data.Tables/IAsyncEnumerableExtensions.cs index b86702f..62a1510 100644 --- a/src/ElCamino.AspNetCore.Identity.AzureTable/Helpers/IAsyncEnumerableExtensions.cs +++ b/src/ElCamino.Azure.Data.Tables/IAsyncEnumerableExtensions.cs @@ -1,11 +1,8 @@ // MIT License Copyright 2020 (c) David Melendez. All rights reserved. See License.txt in the project root for license information. -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; +using System.Diagnostics; -namespace ElCamino.AspNetCore.Identity.AzureTable +namespace Azure.Data.Tables { public static class IAsyncEnumerableExtensions { @@ -53,5 +50,27 @@ public static async Task AnyAsync( await using var enumerator = asyncEnumerable.GetAsyncEnumerator(cancellationToken); return await enumerator.MoveNextAsync().ConfigureAwait(false); } + + public static async IAsyncEnumerable ExecuteQueryAsync(this TableClient ct, TableQuery tq) + where T : class, ITableEntity, new() + { +#if DEBUG + int iCounter = 0; +#endif + + AsyncPageable segment = ct.QueryAsync(tq.FilterString, tq.TakeCount, tq.SelectColumns); + await foreach (T result in segment.ConfigureAwait(false)) + { +#if DEBUG + iCounter++; +#endif + yield return result; + } + +#if DEBUG + Debug.WriteLine("ExecuteQueryAsync: (Count): {0}", iCounter); + Debug.WriteLine("ExecuteQueryAsync (Query): " + tq.FilterString); +#endif + } } } diff --git a/src/ElCamino.Azure.Data.Tables/QueryComparisons.cs b/src/ElCamino.Azure.Data.Tables/QueryComparisons.cs new file mode 100644 index 0000000..14acf48 --- /dev/null +++ b/src/ElCamino.Azure.Data.Tables/QueryComparisons.cs @@ -0,0 +1,56 @@ +// ----------------------------------------------------------------------------------------- +// +// Copyright 2013 Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// ----------------------------------------------------------------------------------------- + + +namespace Azure.Data.Tables +{ + /// + /// From https://github.com/Azure/azure-storage-net/blob/v9.3.2/Lib/Common/Table/QueryComparisons.cs + /// + public static class QueryComparisons + { + /// + /// Represents the Equal operator. + /// + public const string Equal = "eq"; + + /// + /// Represents the Not Equal operator. + /// + public const string NotEqual = "ne"; + + /// + /// Represents the Greater Than operator. + /// + public const string GreaterThan = "gt"; + + /// + /// Represents the Greater Than or Equal operator. + /// + public const string GreaterThanOrEqual = "ge"; + + /// + /// Represents the Less Than operator. + /// + public const string LessThan = "lt"; + + /// + /// Represents the Less Than or Equal operator. + /// + public const string LessThanOrEqual = "le"; + } +} diff --git a/src/ElCamino.Azure.Data.Tables/TableOperators.cs b/src/ElCamino.Azure.Data.Tables/TableOperators.cs new file mode 100644 index 0000000..34d52f4 --- /dev/null +++ b/src/ElCamino.Azure.Data.Tables/TableOperators.cs @@ -0,0 +1,37 @@ +// ----------------------------------------------------------------------------------------- +// +// Copyright 2013 Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// ----------------------------------------------------------------------------------------- + +namespace Azure.Data.Tables +{ + /// + /// From https://github.com/Azure/azure-storage-net/blob/v9.3.2/Lib/Common/Table/TableOperators.cs + /// + public static class TableOperators + { + public const string And = "and"; + + /// + /// Represents the Not operator. + /// + public const string Not = "not"; + + /// + /// Represents the Or operator. + /// + public const string Or = "or"; + } +} diff --git a/src/ElCamino.AspNetCore.Identity.AzureTable/Helpers/Odata/TableQuery.cs b/src/ElCamino.Azure.Data.Tables/TableQuery.cs similarity index 90% rename from src/ElCamino.AspNetCore.Identity.AzureTable/Helpers/Odata/TableQuery.cs rename to src/ElCamino.Azure.Data.Tables/TableQuery.cs index 183eb61..3d779fc 100644 --- a/src/ElCamino.AspNetCore.Identity.AzureTable/Helpers/Odata/TableQuery.cs +++ b/src/ElCamino.Azure.Data.Tables/TableQuery.cs @@ -1,11 +1,31 @@ -using System; +// ----------------------------------------------------------------------------------------- +// +// Copyright 2013 Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// ----------------------------------------------------------------------------------------- + +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Text; -namespace ElCamino.AspNetCore.Identity.AzureTable.Helpers +namespace Azure.Data.Tables { + /// + /// From https://github.com/Azure/azure-storage-net/blob/v9.3.2/Lib/Common/Table/TableQuery.cs + /// public class TableQuery { public int? TakeCount { get; set; } diff --git a/src/ElCamino.Azure.Data.Tables/projectNugetPic.png b/src/ElCamino.Azure.Data.Tables/projectNugetPic.png new file mode 100644 index 0000000000000000000000000000000000000000..223ba4abd68f73bd92a520876e6eb7a2fe0e55ed GIT binary patch literal 10619 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 literal 0 HcmV?d00001