diff --git a/.github/workflows/ListTables_release.yml b/.github/workflows/ListTables_release.yml new file mode 100644 index 0000000..3be7501 --- /dev/null +++ b/.github/workflows/ListTables_release.yml @@ -0,0 +1,16 @@ +name: ListTables_release + +on: + workflow_dispatch: + +jobs: + main_release: + uses: FrendsPlatform/FrendsTasks/.github/workflows/official_task_pipeline.yml@main + with: + workflow_mode: release + os: all + workdir: Frends.AzureTableStorage.ListTables + secrets: + badge_service_api_key: ${{ secrets.BADGE_SERVICE_API_KEY }} + target_feed_api_key: ${{ secrets.TASKS_FEED_API_KEY }} + source_nuget_feed_url: ${{ secrets.TASKS_FEED_URL }} diff --git a/.github/workflows/ListTables_test_on_main.yml b/.github/workflows/ListTables_test_on_main.yml new file mode 100644 index 0000000..1d68887 --- /dev/null +++ b/.github/workflows/ListTables_test_on_main.yml @@ -0,0 +1,29 @@ +name: ListTables_test_on_main + +on: + push: + branches: + - main + paths: + - 'Frends.AzureTableStorage.ListTables/**' + - '.github/workflows/ListTables*.yml' + workflow_dispatch: + +jobs: + main_test: + uses: FrendsPlatform/FrendsTasks/.github/workflows/official_task_pipeline.yml@main + with: + workflow_mode: main + os: all + workdir: Frends.AzureTableStorage.ListTables + secrets: + badge_service_api_key: ${{ secrets.BADGE_SERVICE_API_KEY }} + env_vars: | + { + "Frends_AzureTableStorage_ConnString": ${{ toJSON(secrets.FRENDS_AZURETABLESTORAGE_CONNSTRING) }}, + "Frends_AzureTableStorage_AccountName": ${{ toJSON(secrets.FRENDS_AZURETABLESTORAGE_ACCOUNTNAME) }}, + "Frends_AzureTableStorage_TenantID": ${{ toJSON(secrets.FRENDS_AZURETABLESTORAGE_TENANTID) }}, + "Frends_AzureTableStorage_ClientID": ${{ toJSON(secrets.FRENDS_AZURETABLESTORAGE_CLIENTID) }}, + "Frends_AzureTableStorage_ClientSecret": ${{ toJSON(secrets.FRENDS_AZURETABLESTORAGE_CLIENTSECRET) }}, + "Frends_AzureTableStorage_SasToken": ${{ toJSON(secrets.FRENDS_AZURETABLESTORAGE_SASTOKEN) }}, + } diff --git a/.github/workflows/ListTables_test_on_push.yml b/.github/workflows/ListTables_test_on_push.yml new file mode 100644 index 0000000..85f6395 --- /dev/null +++ b/.github/workflows/ListTables_test_on_push.yml @@ -0,0 +1,30 @@ +name: ListTables_test_on_push + +on: + push: + branches-ignore: + - main + paths: + - 'Frends.AzureTableStorage.ListTables/**' + - '.github/workflows/ListTables*.yml' + workflow_dispatch: + +jobs: + work_push: + uses: FrendsPlatform/FrendsTasks/.github/workflows/official_task_pipeline.yml@main + with: + workflow_mode: work + os: all + workdir: Frends.AzureTableStorage.ListTables + secrets: + badge_service_api_key: ${{ secrets.BADGE_SERVICE_API_KEY }} + target_feed_api_key: ${{ secrets.TASKS_TEST_FEED_API_KEY }} + env_vars: | + { + "Frends_AzureTableStorage_ConnString": ${{ toJSON(secrets.FRENDS_AZURETABLESTORAGE_CONNSTRING) }}, + "Frends_AzureTableStorage_AccountName": ${{ toJSON(secrets.FRENDS_AZURETABLESTORAGE_ACCOUNTNAME) }}, + "Frends_AzureTableStorage_TenantID": ${{ toJSON(secrets.FRENDS_AZURETABLESTORAGE_TENANTID) }}, + "Frends_AzureTableStorage_ClientID": ${{ toJSON(secrets.FRENDS_AZURETABLESTORAGE_CLIENTID) }}, + "Frends_AzureTableStorage_ClientSecret": ${{ toJSON(secrets.FRENDS_AZURETABLESTORAGE_CLIENTSECRET) }}, + "Frends_AzureTableStorage_SasToken": ${{ toJSON(secrets.FRENDS_AZURETABLESTORAGE_SASTOKEN) }}, + } diff --git a/Frends.AzureTableStorage.ListTables/CHANGELOG.md b/Frends.AzureTableStorage.ListTables/CHANGELOG.md new file mode 100644 index 0000000..b336d2b --- /dev/null +++ b/Frends.AzureTableStorage.ListTables/CHANGELOG.md @@ -0,0 +1,7 @@ +# Changelog + +## [1.0.0] - 2026-09-29 + +### Added + +- Initial implementation diff --git a/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables.Tests/.env.example b/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables.Tests/.env.example new file mode 100644 index 0000000..a1a2cff --- /dev/null +++ b/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables.Tests/.env.example @@ -0,0 +1,14 @@ +# Rename this file to .env to run tests locally + +# Connection String authentication (recommended for local testing) +Frends_AzureTableStorage_ConnString="DefaultEndpointsProtocol=https;AccountName=youraccount;AccountKey=yourkey;EndpointSuffix=core.windows.net" + +# OAuth2 (Service Principal) authentication +Frends_AzureTableStorage_AccountName="youraccount" +Frends_AzureTableStorage_TenantID="12345678-1234-1234-1234-123456789012" +Frends_AzureTableStorage_ClientID="12345678-1234-1234-1234-123456789012" +Frends_AzureTableStorage_ClientSecret="your-client-secret" + +# SAS Token authentication +Frends_AzureTableStorage_AccountName="youraccount" +Frends_AzureTableStorage_SasToken="?sv=2021-06-08&ss=t&srt=sco&sp=rwdlacu&se=2024-12-31T23:59:59Z&st=2024-01-01T00:00:00Z&spr=https&sig=..." diff --git a/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables.Tests/ErrorHandlerTest.cs b/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables.Tests/ErrorHandlerTest.cs new file mode 100644 index 0000000..471b06f --- /dev/null +++ b/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables.Tests/ErrorHandlerTest.cs @@ -0,0 +1,122 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.Threading; +using System.Threading.Tasks; +using Frends.AzureTableStorage.ListTables.Definitions; +using NUnit.Framework; + +namespace Frends.AzureTableStorage.ListTables.Tests; + +[TestFixture] +internal class ErrorHandlerTest : TestBase +{ + private const string CustomErrorMessage = "Custom error occurred during table listing"; + + [Test] + public void Should_Throw_Error_When_ThrowErrorOnFailure_Is_True() + { + var input = DefaultInput(); + var connection = new Connection + { + ConnectionMethod = ConnectionMethod.ConnectionString, + ConnectionString = "Invalid", + }; + var options = DefaultOptions(); + options.ThrowErrorOnFailure = true; + + Func action = async () => { await AzureTableStorage.ListTables(input, connection, options, CancellationToken.None); }; + var ex = Assert.ThrowsAsync(action); + Assert.That(ex, Is.Not.Null); + } + + [Test] + public async Task Should_Return_Failed_Result_When_ThrowErrorOnFailure_Is_False() + { + var input = DefaultInput(); + var connection = new Connection + { + ConnectionMethod = ConnectionMethod.ConnectionString, + ConnectionString = "Invalid", + }; + var options = DefaultOptions(); + options.ThrowErrorOnFailure = false; + + var result = await AzureTableStorage.ListTables(input, connection, options, CancellationToken.None); + + Assert.That(result.Success, Is.False); + Assert.That(result.Error, Is.Not.Null); + Assert.That(result.Error.Message, Is.Not.Empty); + } + + [Test] + public void Should_Use_Custom_ErrorMessageOnFailure() + { + var input = DefaultInput(); + var connection = new Connection + { + ConnectionMethod = ConnectionMethod.ConnectionString, + ConnectionString = "Invalid", + }; + var options = DefaultOptions(); + options.ErrorMessageOnFailure = CustomErrorMessage; + options.ThrowErrorOnFailure = true; + + Func action = async () => { await AzureTableStorage.ListTables(input, connection, options, CancellationToken.None); }; + var ex = Assert.ThrowsAsync(action); + Assert.That(ex, Is.Not.Null); + Assert.That(ex.Message, Does.Contain(CustomErrorMessage)); + } + + [Test] + public async Task Should_Return_Custom_ErrorMessage_When_ThrowErrorOnFailure_Is_False() + { + var input = DefaultInput(); + var connection = new Connection + { + ConnectionMethod = ConnectionMethod.ConnectionString, + ConnectionString = "Invalid", + }; + var options = DefaultOptions(); + options.ErrorMessageOnFailure = CustomErrorMessage; + options.ThrowErrorOnFailure = false; + + var result = await AzureTableStorage.ListTables(input, connection, options, CancellationToken.None); + + Assert.That(result.Success, Is.False); + Assert.That(result.Error, Is.Not.Null); + Assert.That(result.Error.Message, Does.Contain(CustomErrorMessage)); + } + + [Test] + public async Task Should_Always_Throw_OperationCanceledException() + { + Assume.That(ConnectionString, Is.Not.Empty, "Connection string is required for cancellation test."); + + var cts = new CancellationTokenSource(); + cts.Cancel(); + + Func action = async () => + { + await AzureTableStorage.ListTables(DefaultInput(), DefaultConnectionStringConnection(), DefaultOptions(), cts.Token); + }; + + var ex = Assert.ThrowsAsync(action); + Assert.That(ex, Is.Not.Null); + } + + [Test] + public void Should_Throw_ValidationException_When_Input_Is_Null() + { + var options = DefaultOptions(); + + Func action = async () => + { + await AzureTableStorage.ListTables(null, DefaultConnectionStringConnection(), options, CancellationToken.None); + }; + + var ex = Assert.ThrowsAsync(action); + + Assert.That(ex, Is.Not.Null); + Assert.That(ex.Message, Does.Contain("Validated object can't be null")); + } +} diff --git a/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables.Tests/Frends.AzureTableStorage.ListTables.Tests.csproj b/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables.Tests/Frends.AzureTableStorage.ListTables.Tests.csproj new file mode 100644 index 0000000..70d9826 --- /dev/null +++ b/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables.Tests/Frends.AzureTableStorage.ListTables.Tests.csproj @@ -0,0 +1,24 @@ + + + net8.0 + false + disable + + + + + + + + + + + + + + + + + + + diff --git a/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables.Tests/FunctionalTests.cs b/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables.Tests/FunctionalTests.cs new file mode 100644 index 0000000..ab6500d --- /dev/null +++ b/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables.Tests/FunctionalTests.cs @@ -0,0 +1,181 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Azure.Data.Tables; +using Frends.AzureTableStorage.ListTables.Definitions; +using NUnit.Framework; + +namespace Frends.AzureTableStorage.ListTables.Tests; + +[TestFixture] +internal class FunctionalTests : TestBase +{ + private string prefix; + private string firstTableName; + private string secondTableName; + private string thirdTableName; + + [SetUp] + public async Task SetUp() + { + Assume.That(ConnectionString, Is.Not.Empty, "Connection string is required for functional tests."); + + prefix = $"frendstest{DateTime.UtcNow:yyMMddhhmmssfff}"; + firstTableName = $"{prefix}a"; + secondTableName = $"{prefix}b"; + thirdTableName = $"{prefix}c"; + + var serviceClient = new TableServiceClient(ConnectionString); + await serviceClient.CreateTableIfNotExistsAsync(firstTableName); + await serviceClient.CreateTableIfNotExistsAsync(secondTableName); + await serviceClient.CreateTableIfNotExistsAsync(thirdTableName); + } + + [TearDown] + public async Task TearDown() + { + if (string.IsNullOrEmpty(ConnectionString)) return; + + var serviceClient = new TableServiceClient(ConnectionString); + + foreach (var tableName in new[] { firstTableName, secondTableName, thirdTableName }) + { + if (string.IsNullOrEmpty(tableName)) continue; + try + { + await serviceClient.DeleteTableAsync(tableName); + } + catch + { + // ignored + } + } + } + + [Test] + public async Task ListTables_WithConnectionString_ShouldReturnMatchingTables() + { + var input = new Input + { + TableNamePrefix = prefix, + MaxResults = 0, + }; + + var result = await AzureTableStorage.ListTables(input, DefaultConnectionStringConnection(), DefaultOptions(), CancellationToken.None); + + Assert.That(result.Success, Is.True); + Assert.That(result.Count, Is.EqualTo(3)); + Assert.That(result.Tables, Is.Not.Null); + Assert.That(result.Tables.Count, Is.EqualTo(3)); + Assert.That(result.Error, Is.Null); + Assert.That(result.Tables.Exists(table => table.Name == firstTableName), Is.True); + Assert.That(result.Tables.Exists(table => table.Name == secondTableName), Is.True); + Assert.That(result.Tables.Exists(table => table.Name == thirdTableName), Is.True); + } + + [Test] + public async Task ListTables_WithMaxResults_ShouldReturnLimitedNumberOfTables() + { + var input = new Input + { + TableNamePrefix = prefix, + MaxResults = 2, + }; + + var result = await AzureTableStorage.ListTables(input, DefaultConnectionStringConnection(), DefaultOptions(), CancellationToken.None); + + Assert.That(result.Success, Is.True); + Assert.That(result.Count, Is.EqualTo(2)); + Assert.That(result.Tables.Count, Is.EqualTo(2)); + Assert.That(result.Error, Is.Null); + } + + [Test] + public async Task ListTables_WithOAuth2_ShouldReturnMatchingTables() + { + Assume.That(AccountName, Is.Not.Empty, "Storage account name is required for OAuth2 test."); + Assume.That(TenantId, Is.Not.Empty, "TenantId is required for OAuth2 test."); + Assume.That(ClientId, Is.Not.Empty, "ClientId is required for OAuth2 test."); + Assume.That(ClientSecret, Is.Not.Empty, "ClientSecret is required for OAuth2 test."); + + var input = new Input + { + TableNamePrefix = prefix, + MaxResults = 0, + }; + + var result = await AzureTableStorage.ListTables(input, DefaultOAuth2Connection(), DefaultOptions(), CancellationToken.None); + + Assert.That(result.Success, Is.True); + Assert.That(result.Count, Is.EqualTo(3)); + Assert.That(result.Tables, Is.Not.Null); + Assert.That(result.Error, Is.Null); + } + + [Test] + public async Task ListTables_WithSasToken_ShouldReturnMatchingTables() + { + Assume.That(AccountName, Is.Not.Empty, "Storage account name is required for SAS test."); + Assume.That(SasToken, Is.Not.Empty, "SAS token is required for SAS test."); + + var input = new Input + { + TableNamePrefix = prefix, + MaxResults = 0, + }; + + var result = await AzureTableStorage.ListTables(input, DefaultSasTokenConnection(), DefaultOptions(), CancellationToken.None); + + Assert.That(result.Success, Is.True); + Assert.That(result.Count, Is.EqualTo(3)); + Assert.That(result.Tables, Is.Not.Null); + Assert.That(result.Error, Is.Null); + } + + [Test] + public async Task ListTables_WithNonMatchingPrefix_ShouldReturnEmptyList() + { + var input = new Input + { + TableNamePrefix = $"{prefix}xyz", + MaxResults = 0, + }; + + var result = await AzureTableStorage.ListTables(input, DefaultConnectionStringConnection(), DefaultOptions(), CancellationToken.None); + + Assert.That(result.Success, Is.True); + Assert.That(result.Count, Is.EqualTo(0)); + Assert.That(result.Tables, Is.Empty); + Assert.That(result.Error, Is.Null); + } + + [Test] + public async Task ListTables_WithInvalidSasToken_ShouldReturnErrorFromQuery() + { + Assume.That(AccountName, Is.Not.Empty, "Storage account name is required for invalid SAS test."); + + var input = new Input + { + TableNamePrefix = prefix, + MaxResults = 1, + }; + + var connection = new Connection + { + ConnectionMethod = ConnectionMethod.SasToken, + StorageAccountName = AccountName, + SasToken = "sv=2023-11-03&sp=rl&sig=invalidsignature", + }; + + var options = DefaultOptions(); + options.ThrowErrorOnFailure = false; + + var result = await AzureTableStorage.ListTables(input, connection, options, CancellationToken.None); + + Assert.That(result.Success, Is.False); + Assert.That(result.Tables, Is.Null); + Assert.That(result.Count, Is.EqualTo(0)); + Assert.That(result.Error, Is.Not.Null); + Assert.That(result.Error.Message, Is.Not.Empty); + } +} diff --git a/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables.Tests/GlobalSuppressions.cs b/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables.Tests/GlobalSuppressions.cs new file mode 100644 index 0000000..1275afc --- /dev/null +++ b/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables.Tests/GlobalSuppressions.cs @@ -0,0 +1,8 @@ +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("StyleCop.CSharp.SpecialRules", "SA0001::XmlCommentAnalysisDisabled", Justification = "Following Frends documentation guidelines")] +[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1101:PrefixLocalCallsWithThis", Justification = "Following Frends documentation guidelines")] +[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1200:UsingDirectivesMustBePlacedWithinNamespace", Justification = "Following Frends documentation guidelines")] +[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1503:BracesMustNotBeOmitted", Justification = "Following Frends documentation guidelines")] +[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:ElementsMustBeDocumented", Justification = "Documentation checked by custom analyzers")] +[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1633:FileMustHaveHeader", Justification = "Following Frends documentation guidelines")] diff --git a/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables.Tests/TestBase.cs b/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables.Tests/TestBase.cs new file mode 100644 index 0000000..3bd2cf6 --- /dev/null +++ b/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables.Tests/TestBase.cs @@ -0,0 +1,68 @@ +using System; +using dotenv.net; +using Frends.AzureTableStorage.ListTables.Definitions; + +namespace Frends.AzureTableStorage.ListTables.Tests; + +internal abstract class TestBase +{ + internal TestBase() + { + DotEnv.Load(); + ConnectionString = GetEnvVarOrDefault("Frends_AzureTableStorage_ConnString", string.Empty); + AccountName = GetEnvVarOrDefault("Frends_AzureTableStorage_AccountName", string.Empty); + TenantId = GetEnvVarOrDefault("Frends_AzureTableStorage_TenantID", string.Empty); + ClientId = GetEnvVarOrDefault("Frends_AzureTableStorage_ClientID", string.Empty); + ClientSecret = GetEnvVarOrDefault("Frends_AzureTableStorage_ClientSecret", string.Empty); + SasToken = GetEnvVarOrDefault("Frends_AzureTableStorage_SasToken", string.Empty); + } + + protected string ConnectionString { get; set; } + + protected string AccountName { get; set; } + + protected string TenantId { get; set; } + + protected string ClientId { get; set; } + + protected string ClientSecret { get; set; } + + protected string SasToken { get; set; } + + protected static Options DefaultOptions() => new() + { + ThrowErrorOnFailure = true, + ErrorMessageOnFailure = string.Empty, + }; + + protected static Input DefaultInput() => new() + { + TableNamePrefix = string.Empty, + MaxResults = 0, + }; + + protected Connection DefaultConnectionStringConnection() => new() + { + ConnectionMethod = ConnectionMethod.ConnectionString, + ConnectionString = ConnectionString, + }; + + protected Connection DefaultOAuth2Connection() => new() + { + ConnectionMethod = ConnectionMethod.OAuth2, + StorageAccountName = AccountName, + TenantId = TenantId, + ApplicationId = ClientId, + ClientSecret = ClientSecret, + }; + + protected Connection DefaultSasTokenConnection() => new() + { + ConnectionMethod = ConnectionMethod.SasToken, + StorageAccountName = AccountName, + SasToken = SasToken, + }; + + private static string GetEnvVarOrDefault(string name, string defaultValue) => + Environment.GetEnvironmentVariable(name) ?? defaultValue; +} diff --git a/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables.sln b/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables.sln new file mode 100644 index 0000000..4978062 --- /dev/null +++ b/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables.sln @@ -0,0 +1,40 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.32112.339 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Frends.AzureTableStorage.ListTables", "Frends.AzureTableStorage.ListTables\Frends.AzureTableStorage.ListTables.csproj", "{35C305C0-8108-4A98-BB1D-AFE5C926239E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Frends.AzureTableStorage.ListTables.Tests", "Frends.AzureTableStorage.ListTables.Tests\Frends.AzureTableStorage.ListTables.Tests.csproj", "{8CA92187-8E4F-4414-803B-EC899479022E}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{78F7F22E-6E20-4BCE-8362-0C558568B729}" + ProjectSection(SolutionItems) = preProject + CHANGELOG.md = CHANGELOG.md + ..\.github\workflows\ListTables_test_on_main.yml = ..\.github\workflows\ListTables_test_on_main.yml + ..\.github\workflows\ListTables_test_on_push.yml = ..\.github\workflows\ListTables_test_on_push.yml + ..\.github\workflows\ListTables_release.yml = ..\.github\workflows\ListTables_release.yml + README.md = README.md + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {35C305C0-8108-4A98-BB1D-AFE5C926239E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {35C305C0-8108-4A98-BB1D-AFE5C926239E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {35C305C0-8108-4A98-BB1D-AFE5C926239E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {35C305C0-8108-4A98-BB1D-AFE5C926239E}.Release|Any CPU.Build.0 = Release|Any CPU + {8CA92187-8E4F-4414-803B-EC899479022E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8CA92187-8E4F-4414-803B-EC899479022E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8CA92187-8E4F-4414-803B-EC899479022E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8CA92187-8E4F-4414-803B-EC899479022E}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {55BC6629-85C9-48D8-8CA2-B0046AF1AF4B} + EndGlobalSection +EndGlobal diff --git a/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables/Attributes/RequiredIfAttribute.cs b/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables/Attributes/RequiredIfAttribute.cs new file mode 100644 index 0000000..b7ea8df --- /dev/null +++ b/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables/Attributes/RequiredIfAttribute.cs @@ -0,0 +1,33 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.Linq; + +namespace Frends.AzureTableStorage.ListTables.Attributes; + +/// +/// Validates that a property is required if another property has a specific value. +/// If a property is null, empty, or white space only, validation fails. +/// +// TODO : Remove this class if not used +[AttributeUsage(AttributeTargets.Property)] +internal class RequiredIfAttribute(string dependentProperty, params object[] targetValues) : ValidationAttribute +{ + protected override ValidationResult IsValid(object value, ValidationContext validationContext) + { + var field = validationContext.ObjectType.GetProperty(dependentProperty); + + if (field == null) + return new ValidationResult($"Unknown property: {dependentProperty}"); + + var dependentValue = field.GetValue(validationContext.ObjectInstance); + + if (!targetValues.Contains(dependentValue)) return ValidationResult.Success; + + if (value == null || (value is string s && string.IsNullOrWhiteSpace(s))) + { + return new ValidationResult(ErrorMessage ?? $"{validationContext.DisplayName} is required."); + } + + return ValidationResult.Success; + } +} diff --git a/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables/Definitions/Connection.cs b/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables/Definitions/Connection.cs new file mode 100644 index 0000000..c381f5a --- /dev/null +++ b/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables/Definitions/Connection.cs @@ -0,0 +1,130 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using Frends.AzureTableStorage.ListTables.Attributes; + +namespace Frends.AzureTableStorage.ListTables.Definitions; + +/// +/// Connection parameters for Azure Table Storage. +/// +public class Connection +{ + /// + /// Connection method to use for connecting to Azure Table Storage. + /// + /// ConnectionMethod.ConnectionString + [DefaultValue(ConnectionMethod.ConnectionString)] + public ConnectionMethod ConnectionMethod { get; set; } = ConnectionMethod.ConnectionString; + + /// + /// Connection string for the Azure Storage Account. + /// Required when ConnectionMethod is ConnectionString. + /// + /// DefaultEndpointsProtocol=https;AccountName=myaccount;AccountKey=mykey;EndpointSuffix=core.windows.net + [DisplayFormat(DataFormatString = "Text")] + [PasswordPropertyText] + [DefaultValue("")] + [UIHint(nameof(ConnectionMethod), "", ConnectionMethod.ConnectionString)] + [RequiredIf(nameof(ConnectionMethod), ConnectionMethod.ConnectionString)] + public string ConnectionString { get; set; } = string.Empty; + + /// + /// Application (Client) ID of Azure AD Application. + /// Required when ConnectionMethod is OAuth2. + /// + /// 12345678-1234-1234-1234-123456789012 + [DisplayFormat(DataFormatString = "Text")] + [DefaultValue("")] + [UIHint(nameof(ConnectionMethod), "", ConnectionMethod.OAuth2)] + [RequiredIf(nameof(ConnectionMethod), ConnectionMethod.OAuth2)] + public string ApplicationId { get; set; } = string.Empty; + + /// + /// Azure AD Tenant ID. + /// Required when ConnectionMethod is OAuth2. + /// + /// 12345678-1234-1234-1234-123456789012 + [DisplayFormat(DataFormatString = "Text")] + [DefaultValue("")] + [UIHint(nameof(ConnectionMethod), "", ConnectionMethod.OAuth2)] + [RequiredIf(nameof(ConnectionMethod), ConnectionMethod.OAuth2)] + public string TenantId { get; set; } = string.Empty; + + /// + /// Azure AD Application Client Secret. + /// Required when ConnectionMethod is OAuth2. + /// + /// your-client-secret + [DisplayFormat(DataFormatString = "Text")] + [PasswordPropertyText] + [DefaultValue("")] + [UIHint(nameof(ConnectionMethod), "", ConnectionMethod.OAuth2)] + [RequiredIf(nameof(ConnectionMethod), ConnectionMethod.OAuth2)] + public string ClientSecret { get; set; } = string.Empty; + + /// + /// Shared Access Signature (SAS) token for authentication. + /// Required when ConnectionMethod is SasToken. + /// + /// ?sv=2021-06-08&ss=t&srt=sco&sp=rwdlacu&se=2024-12-31T23:59:59Z&st=2024-01-01T00:00:00Z&spr=https&sig=... + [DisplayFormat(DataFormatString = "Text")] + [PasswordPropertyText] + [DefaultValue("")] + [UIHint(nameof(ConnectionMethod), "", ConnectionMethod.SasToken)] + [RequiredIf(nameof(ConnectionMethod), ConnectionMethod.SasToken)] + public string SasToken { get; set; } = string.Empty; + + /// + /// Name of the Azure Storage Account. + /// Required when ConnectionMethod is OAuth2, SasToken, ArcManagedIdentity, or ArcManagedIdentityCrossTenant. + /// + /// mystorageaccount + [DisplayFormat(DataFormatString = "Text")] + [DefaultValue("")] + [UIHint( + nameof(ConnectionMethod), + "", + ConnectionMethod.OAuth2, + ConnectionMethod.SasToken, + ConnectionMethod.ArcManagedIdentity, + ConnectionMethod.ArcManagedIdentityCrossTenant)] + [RequiredIf( + nameof(ConnectionMethod), + ConnectionMethod.OAuth2, + ConnectionMethod.SasToken, + ConnectionMethod.ArcManagedIdentity, + ConnectionMethod.ArcManagedIdentityCrossTenant)] + public string StorageAccountName { get; set; } = string.Empty; + + /// + /// Scopes used when authenticating with Arc Managed Identity Cross Tenant. + /// Required when ConnectionMethod is ArcManagedIdentityCrossTenant. + /// + /// ["api://AzureADTokenExchange/.default"] + [DefaultValue(null)] + [UIHint(nameof(ConnectionMethod), "", ConnectionMethod.ArcManagedIdentityCrossTenant)] + [RequiredIf(nameof(ConnectionMethod), ConnectionMethod.ArcManagedIdentityCrossTenant)] + public string[] Scopes { get; set; } = []; + + /// + /// Target Tenant ID of Azure Tenant. + /// Required when ConnectionMethod is ArcManagedIdentityCrossTenant. + /// + /// 12345678-1234-1234-1234-123456789012 + [DisplayFormat(DataFormatString = "Text")] + [DefaultValue("")] + [UIHint(nameof(ConnectionMethod), "", ConnectionMethod.ArcManagedIdentityCrossTenant)] + [RequiredIf(nameof(ConnectionMethod), ConnectionMethod.ArcManagedIdentityCrossTenant)] + public string TargetTenantId { get; set; } = string.Empty; + + /// + /// Target Client ID of Azure Tenant. + /// Required when ConnectionMethod is ArcManagedIdentityCrossTenant. + /// + /// 12345678-1234-1234-1234-123456789012 + [DisplayFormat(DataFormatString = "Text")] + [DefaultValue("")] + [UIHint(nameof(ConnectionMethod), "", ConnectionMethod.ArcManagedIdentityCrossTenant)] + [RequiredIf(nameof(ConnectionMethod), ConnectionMethod.ArcManagedIdentityCrossTenant)] + public string TargetClientId { get; set; } = string.Empty; +} diff --git a/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables/Definitions/Enums.cs b/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables/Definitions/Enums.cs new file mode 100644 index 0000000..d0f1b15 --- /dev/null +++ b/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables/Definitions/Enums.cs @@ -0,0 +1,32 @@ +namespace Frends.AzureTableStorage.ListTables.Definitions; + +/// +/// Connection method for connecting to Azure Table Storage. +/// +public enum ConnectionMethod +{ + /// + /// Connection string. + /// + ConnectionString = 1, + + /// + /// OAuth2 authentication. + /// + OAuth2 = 2, + + /// + /// Shared Access Signature token. + /// + SasToken = 3, + + /// + /// Azure Arc Managed Identity. + /// + ArcManagedIdentity = 4, + + /// + /// Azure Arc Managed Identity Cross Tenant. + /// + ArcManagedIdentityCrossTenant = 5, +} diff --git a/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables/Definitions/Error.cs b/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables/Definitions/Error.cs new file mode 100644 index 0000000..1e263a8 --- /dev/null +++ b/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables/Definitions/Error.cs @@ -0,0 +1,21 @@ +using System; + +namespace Frends.AzureTableStorage.ListTables.Definitions; + +/// +/// Error that occurred during the task. +/// +public class Error +{ + /// + /// Summary of the error. + /// + /// Authentication issue + public string Message { get; set; } + + /// + /// Additional information about the error. + /// + /// object { Exception AdditionalInfo } + public Exception AdditionalInfo { get; set; } +} diff --git a/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables/Definitions/Input.cs b/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables/Definitions/Input.cs new file mode 100644 index 0000000..653e763 --- /dev/null +++ b/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables/Definitions/Input.cs @@ -0,0 +1,25 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; + +namespace Frends.AzureTableStorage.ListTables.Definitions; + +/// +/// Essential parameters. +/// +public class Input +{ + /// + /// Optional prefix to filter tables by name. Leave empty to list all tables. + /// + /// orders + [DisplayFormat(DataFormatString = "Text")] + [DefaultValue("")] + public string TableNamePrefix { get; set; } + + /// + /// Maximum number of tables to return. 0 means no limit. + /// + /// 100 + [DefaultValue(0)] + public int MaxResults { get; set; } +} diff --git a/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables/Definitions/Options.cs b/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables/Definitions/Options.cs new file mode 100644 index 0000000..730e583 --- /dev/null +++ b/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables/Definitions/Options.cs @@ -0,0 +1,25 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; + +namespace Frends.AzureTableStorage.ListTables.Definitions; + +/// +/// Additional parameters. +/// +public class Options +{ + /// + /// Whether to throw an error on failure. + /// + /// true + [DefaultValue(true)] + public bool ThrowErrorOnFailure { get; set; } = true; + + /// + /// Overrides the error message on failure. + /// + /// Custom error message + [DisplayFormat(DataFormatString = "Text")] + [DefaultValue("")] + public string ErrorMessageOnFailure { get; set; } = string.Empty; +} diff --git a/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables/Definitions/Result.cs b/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables/Definitions/Result.cs new file mode 100644 index 0000000..afa863b --- /dev/null +++ b/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables/Definitions/Result.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; + +namespace Frends.AzureTableStorage.ListTables.Definitions; + +/// +/// Result of the task. +/// +public class Result +{ + /// + /// Indicates if the task completed successfully. + /// + /// true + public bool Success { get; set; } + + /// + /// Tables found. + /// + /// [ { Name = "orders", Uri = "https://myaccount.table.core.windows.net/orders" } ] + public List Tables { get; set; } + + /// + /// Number of tables returned. + /// + /// 5 + public int Count { get; set; } + + /// + /// Error that occurred during task execution. + /// + /// object { string Message, Exception AdditionalInfo } + public Error Error { get; set; } +} diff --git a/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables/Definitions/TableInfo.cs b/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables/Definitions/TableInfo.cs new file mode 100644 index 0000000..4da6849 --- /dev/null +++ b/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables/Definitions/TableInfo.cs @@ -0,0 +1,19 @@ +namespace Frends.AzureTableStorage.ListTables.Definitions; + +/// +/// Represents table metadata, including its name and URI. +/// +public class TableInfo +{ + /// + /// Name of the table. + /// + /// Customers + public string Name { get; set; } + + /// + /// Uri of the table. + /// + /// https://mystorageaccount.table.core.windows.net/Customers + public string Uri { get; set; } +} diff --git a/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables.cs b/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables.cs new file mode 100644 index 0000000..18e3174 --- /dev/null +++ b/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Threading; +using System.Threading.Tasks; +using Frends.AzureTableStorage.ListTables.Definitions; +using Frends.AzureTableStorage.ListTables.Helpers; + +namespace Frends.AzureTableStorage.ListTables; + +/// +/// Task Class for AzureTableStorage operations. +/// +public static class AzureTableStorage +{ + /// + /// List the tables in Azure Table Storage + /// [Documentation](https://tasks.frends.com/tasks/frends-tasks/Frends-AzureTableStorage-ListTables) + /// + /// Essential parameters. + /// Connection parameters. + /// Additional parameters. + /// A cancellation token provided by Frends Platform. + /// object { bool Success, int Count, List<object> Tables { string Name, string Uri }, object Error { string Message, object AdditionalInfo } } + public static async Task ListTables( + [PropertyTab] Input input, + [PropertyTab] Connection connection, + [PropertyTab] Options options, + CancellationToken cancellationToken) + { + try + { + ValidationHandler.Run(input, connection, options); + + var serviceClient = ConnectionHandler.GetTableServiceClient(connection, cancellationToken); + + string filter = null; + if (!string.IsNullOrWhiteSpace(input.TableNamePrefix)) + { + var prefix = input.TableNamePrefix.Replace("'", "''"); + filter = $"TableName ge '{prefix}' and TableName lt '{prefix}{{'"; + } + + var tables = new List(); + + await foreach (var table in serviceClient.QueryAsync(filter, cancellationToken: cancellationToken)) + { + tables.Add(new TableInfo + { + Name = table.Name, + Uri = serviceClient.GetTableClient(table.Name).Uri.ToString(), + }); + + if (input.MaxResults > 0 && tables.Count >= input.MaxResults) + break; + } + + return new Result + { + Success = true, + Tables = tables, + Count = tables.Count, + Error = null, + }; + } + catch (Exception ex) + { + return ex.Handle(options); + } + } +} diff --git a/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables.csproj b/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables.csproj new file mode 100644 index 0000000..83df8d5 --- /dev/null +++ b/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables.csproj @@ -0,0 +1,41 @@ + + + net8.0 + latest + 1.0.0 + Frends + Frends + Frends + Frends + Frends + MIT + true + List the tables in Azure Table Storage + https://frends.com/ + https://github.com/FrendsPlatform/Frends.AzureTableStorage/tree/main/Frends.AzureTableStorage.ListTables + disable + + CS1591, + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + diff --git a/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables/FrendsTaskMetadata.json b/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables/FrendsTaskMetadata.json new file mode 100644 index 0000000..801d13c --- /dev/null +++ b/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables/FrendsTaskMetadata.json @@ -0,0 +1,7 @@ +{ + "Tasks": [ + { + "TaskMethod": "Frends.AzureTableStorage.ListTables.AzureTableStorage.ListTables" + } + ] +} diff --git a/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables/GlobalSuppressions.cs b/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables/GlobalSuppressions.cs new file mode 100644 index 0000000..4b4cc78 --- /dev/null +++ b/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables/GlobalSuppressions.cs @@ -0,0 +1,10 @@ +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1101:PrefixLocalCallsWithThis", Justification = "Following Frends documentation guidelines")] +[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1200:UsingDirectivesMustBePlacedWithinNamespace", Justification = "Following Frends documentation guidelines")] +[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1503:BracesMustNotBeOmitted", Justification = "Following Frends documentation guidelines")] +[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:ElementsMustBeDocumented", Justification = "Documentation checked by custom analyzers")] +[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1623:PropertySummaryDocumentationMustMatchAccessors", Justification = "Following Frends Tasks guidelines")] +[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1629:DocumentationTextMustEndWithAPeriod", Justification = "Following Frends Tasks guidelines")] +[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1633:FileMustHaveHeader", Justification = "Following Frends documentation guidelines")] +[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:FileNameMustMatchTypeName", Justification = "Following Frends Tasks guidelines")] diff --git a/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables/Helpers/ConnectionHandler.cs b/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables/Helpers/ConnectionHandler.cs new file mode 100644 index 0000000..b4ce865 --- /dev/null +++ b/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables/Helpers/ConnectionHandler.cs @@ -0,0 +1,115 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading; +using Azure.Core; +using Azure.Data.Tables; +using Azure.Identity; +using Frends.AzureTableStorage.ListTables.Definitions; + +namespace Frends.AzureTableStorage.ListTables.Helpers; + +/// +/// Connection handler to connect with Azure Table Storage. +/// +public static class ConnectionHandler +{ + /// + /// Get Table Service Client. + /// + /// Connection task parameters + /// cancellation token + /// TableServiceClient object + public static TableServiceClient GetTableServiceClient( + Connection connection, + CancellationToken cancellationToken) + { + try + { + return connection.ConnectionMethod switch + { + ConnectionMethod.ConnectionString => GetTableServiceClientWithConnectionString(connection), + ConnectionMethod.SasToken => GetTableServiceClientWithSasToken(connection), + ConnectionMethod.OAuth2 => GetTableServiceClientWithOAuth2(connection), + ConnectionMethod.ArcManagedIdentity => GetTableServiceClientWithArcManagedIdentity(connection), + ConnectionMethod.ArcManagedIdentityCrossTenant => GetTableServiceClientWithArcManagedIdentityCrossTenant( + connection, + cancellationToken), + _ => throw new NotSupportedException(), + }; + } + catch (Exception ex) + { + throw new ArgumentException($"GetTableServiceClient error: {ex.Message}", ex); + } + } + + private static TableServiceClient GetTableServiceClientWithConnectionString(Connection connection) + { + return new TableServiceClient(connection.ConnectionString); + } + + private static TableServiceClient GetTableServiceClientWithSasToken(Connection connection) + { + return new TableServiceClient(GetUri(connection.StorageAccountName, connection.SasToken)); + } + + private static TableServiceClient GetTableServiceClientWithOAuth2(Connection connection) + { + return new TableServiceClient( + GetUri(connection.StorageAccountName), + new ClientSecretCredential( + connection.TenantId, + connection.ApplicationId, + connection.ClientSecret, + new ClientSecretCredentialOptions())); + } + + [ExcludeFromCodeCoverage(Justification = "We do not have environment prepared to test this connection")] + private static TableServiceClient GetTableServiceClientWithArcManagedIdentity(Connection connection) + { + { + var credentials = new ManagedIdentityCredential(); + + return new TableServiceClient(GetUri(connection.StorageAccountName), credentials); + } + } + + [ExcludeFromCodeCoverage(Justification = "We do not have environment prepared to test this connection")] + private static TableServiceClient GetTableServiceClientWithArcManagedIdentityCrossTenant( + Connection connection, + CancellationToken cancellationToken) + { + { + var credentials = new ManagedIdentityCredential(); + ClientAssertionCredential assertion = new( + connection.TargetTenantId, + connection.TargetClientId, + async _ => + { + var tokenRequestContext = new TokenRequestContext(connection.Scopes); + var accessToken = await credentials + .GetTokenAsync(tokenRequestContext, cancellationToken).ConfigureAwait(false); + + return accessToken.Token; + }); + + return new TableServiceClient(GetUri(connection.StorageAccountName), assertion); + } + } + + private static Uri GetUri(string storageAccountName, string sasToken = null) + { + if (storageAccountName is null + || storageAccountName.Length < 3 + || storageAccountName.Length > 24 + || !storageAccountName.All(c => char.IsLower(c) || char.IsDigit(c))) + throw new ValidationException("StorageAccountName must be 3–24 lowercase letters or digits."); + + var normalizedSasToken = sasToken?.TrimStart('?'); + return sasToken is null + ? new Uri($"https://{storageAccountName}.table.core.windows.net") + : new Uri($"https://{storageAccountName}.table.core.windows.net?{normalizedSasToken}"); + } +} diff --git a/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables/Helpers/ErrorHandler.cs b/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables/Helpers/ErrorHandler.cs new file mode 100644 index 0000000..d0a84e9 --- /dev/null +++ b/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables/Helpers/ErrorHandler.cs @@ -0,0 +1,56 @@ +using System; +using System.Runtime.ExceptionServices; +using Frends.AzureTableStorage.ListTables.Definitions; + +namespace Frends.AzureTableStorage.ListTables.Helpers; + +internal static class ErrorHandler +{ + /// + /// Converts an exception into a failed Result object or rethrows based on task options. + /// + /// The exception to handle. + /// Task options that control whether failures are returned as a Result object or thrown. + /// + /// When true, an OperationCanceledException is rethrown immediately. + /// When false, cancellation is handled like any other failure. + /// + /// A failed Result object when the exception is handled instead of rethrown. + internal static Result Handle(this Exception exception, Options options, bool throwCanceled = true) + { + ThrowIfCanceled(exception, throwCanceled); + if (options.ThrowErrorOnFailure) ThrowBaseException(exception, options.ErrorMessageOnFailure); + + return ReturnResult(exception, options.ErrorMessageOnFailure); + } + + private static void ThrowIfCanceled(Exception exception, bool throwCanceled = true) + { + if (throwCanceled && exception is OperationCanceledException) throw exception; + } + + private static void ThrowBaseException(Exception exception, string customMessage = null) + { + if (string.IsNullOrEmpty(customMessage)) + ExceptionDispatchInfo.Capture(exception).Throw(); + + throw new Exception(customMessage, exception); + } + + private static Result ReturnResult(Exception exception, string customMessage = null) + { + var errorMessage = string.IsNullOrEmpty(customMessage) + ? exception.Message + : $"{customMessage}: {exception.Message}"; + + return new Result + { + Success = false, + Error = new Error + { + Message = errorMessage, + AdditionalInfo = exception, + }, + }; + } +} diff --git a/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables/Helpers/ValidationHandler.cs b/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables/Helpers/ValidationHandler.cs new file mode 100644 index 0000000..d677068 --- /dev/null +++ b/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables/Helpers/ValidationHandler.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; + +namespace Frends.AzureTableStorage.ListTables.Helpers; + +/// +/// Validates objects by their ValidationAttributes. +/// +internal static class ValidationHandler +{ + internal static void Run(params object[] objects) + { + if (objects == null || objects.Length == 0) + throw new ValidationException("Validation failed:\nYou must provide objects to validate"); + var validationMessage = objects.Select(obj => obj.Validate()) + .Aggregate(string.Empty, (current, message) => string.Join("\n", current, message)); + + if (validationMessage.Trim() != string.Empty) + throw new ValidationException($"Validation failed:\n{validationMessage}"); + } + + private static string Validate(this T objectToValidate) + { + if (objectToValidate == null) return "Validated object can't be null!\n"; + var ctx = new ValidationContext(objectToValidate); + List validateResults = []; + Validator.TryValidateObject(objectToValidate, ctx, validateResults, true); + + return validateResults.Aggregate( + string.Empty, + (current, error) => string.Join("\n", current, $"{error.ErrorMessage}")); + } +} diff --git a/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables/migration.json b/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables/migration.json new file mode 100644 index 0000000..0c7a008 --- /dev/null +++ b/Frends.AzureTableStorage.ListTables/Frends.AzureTableStorage.ListTables/migration.json @@ -0,0 +1,12 @@ +[ + { + "Task": "Frends.AzureTableStorage.ListTables", + "Migrations": [ + { + "Version": "1.0.0", + "Description": "", + "Migration": [] + } + ] + } +] diff --git a/Frends.AzureTableStorage.ListTables/README.md b/Frends.AzureTableStorage.ListTables/README.md new file mode 100644 index 0000000..07ac3ee --- /dev/null +++ b/Frends.AzureTableStorage.ListTables/README.md @@ -0,0 +1,34 @@ +# Frends.AzureTableStorage.ListTables + +List the tables in Azure Table Storage + +[![ListTables_build](https://github.com/FrendsPlatform/Frends.AzureTableStorage/actions/workflows/ListTables_test_on_main.yml/badge.svg)](https://github.com/FrendsPlatform/Frends.AzureTableStorage/actions/workflows/ListTables_test_on_main.yml) +![Coverage](https://app-github-custom-badges.azurewebsites.net/Badge?key=FrendsPlatform/Frends.AzureTableStorage/Frends.AzureTableStorage.ListTables|main) +[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT) + +## Installing + +You can install the Task via Frends UI Task View. + +## Building + +### Clone a copy of the repository + +`git clone https://github.com/FrendsPlatform/Frends.AzureTableStorage.git` + +### Build the project + +`dotnet build` + +### Run tests + +Run the tests + +`dotnet test` + +### Create a NuGet package + +`dotnet pack --configuration Release` + +### StyleCop.Analyzers Version +This project uses StyleCop.Analyzers 1.2.0-beta.556, as recommended by the author, to get the latest fixes and improvements not available in the last stable release. diff --git a/README.md b/README.md index 3cee84f..ce9ca4c 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ Frends tasks for Azure Table Storage related operations. - [Frends.AzureTableStorage.CreateTable](Frends.AzureTableStorage.CreateTable/README.md) - [Frends.AzureTableStorage.DeleteTable](Frends.AzureTableStorage.DeleteTable/README.md) +- [Frends.AzureTableStorage.ListTables](Frends.AzureTableStorage.ListTables/README.md) # Contributing