diff --git a/.github/workflows/cd.yaml b/.github/workflows/cd.yaml index 229acf3..69d4811 100644 --- a/.github/workflows/cd.yaml +++ b/.github/workflows/cd.yaml @@ -6,6 +6,7 @@ on: - master paths: - Net.Sdk.Web.Extensions/** + - Net.Sdk.Web.Extensions.Azure/** - ".github/workflows/cd.yaml" jobs: @@ -22,6 +23,7 @@ jobs: Configuration: Release Solution_Path: Net.Sdk.Web.Extensions.sln Source_Project_Path: Net.Sdk.Web.Extensions\Net.Sdk.Web.Extensions.csproj + Azure_Project_path: Net.Sdk.Web.Extensions.Azure\Net.Sdk.Web.Extensions.Azure.csproj Actions_Allow_Unsecure_Commands: true steps: @@ -43,11 +45,17 @@ jobs: env: RuntimeIdentifier: win-${{ matrix.targetplatform }} - - name: Build AspNetCore.Extensions.NetStandard project + - name: Build Net.Sdk.Extensions project run: dotnet build $env:Source_Project_Path -c $env:Configuration - - name: Package AspNetCore.Extensions.NetStandard + - name: Package Net.Sdk.Extensions run: dotnet pack -c Release -o . $env:Source_Project_Path + - name: Build Net.Sdk.Extensions.Azure project + run: dotnet build $env:Azure_Project_Path -c $env:Configuration + + - name: Package Net.Sdk.Extensions.Azure + run: dotnet pack -c Release -o . $env:Azure_Project_Path + - name: Publish run: dotnet nuget push *.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate \ No newline at end of file diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e023eca..13c75e3 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -6,6 +6,7 @@ on: - master paths: - Net.Sdk.Web.Extensions/** + - Net.Sdk.Web.Extensions.Azure/** - ".github/workflows/ci.yaml" jobs: @@ -21,6 +22,7 @@ jobs: env: Solution_Path: Net.Sdk.Web.Extensions.sln Source_Project_Path: Net.Sdk.Web.Extensions\Net.Sdk.Web.Extensions.csproj + Azure_Project_path: Net.Sdk.Web.Extensions.Azure\Net.Sdk.Web.Extensions.Azure.csproj Actions_Allow_Unsecure_Commands: true steps: diff --git a/Net.Sdk.Web.Extensions.Azure/InterceptingAsyncPageable.cs b/Net.Sdk.Web.Extensions.Azure/InterceptingAsyncPageable.cs new file mode 100644 index 0000000..d40089c --- /dev/null +++ b/Net.Sdk.Web.Extensions.Azure/InterceptingAsyncPageable.cs @@ -0,0 +1,35 @@ +using Azure; + +namespace Net.Sdk.Web.Extensions.Azure; + +internal sealed class InterceptingAsyncPageable : AsyncPageable where T : notnull +{ + private readonly Action> interceptPage; + private readonly Action interceptSuccess; + private readonly AsyncPageable originalPageable; + + public InterceptingAsyncPageable(AsyncPageable originalPageable, Action> interceptPage, Action interceptSuccess) + { + this.originalPageable = originalPageable; + this.interceptPage = interceptPage; + this.interceptSuccess = interceptSuccess; + } + + public override async IAsyncEnumerable> AsPages(string? continuationToken = null, int? pageSizeHint = null) + { + await foreach (var page in this.originalPageable.AsPages(continuationToken, pageSizeHint)) + { + this.interceptPage(page); + yield return page; + } + } + + public async override IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) + { + await foreach (var item in this.originalPageable) + { + this.interceptSuccess(); + yield return item; + } + } +} diff --git a/Net.Sdk.Web.Extensions.Azure/InterceptingPageable.cs b/Net.Sdk.Web.Extensions.Azure/InterceptingPageable.cs new file mode 100644 index 0000000..13252ea --- /dev/null +++ b/Net.Sdk.Web.Extensions.Azure/InterceptingPageable.cs @@ -0,0 +1,35 @@ +using Azure; + +namespace Net.Sdk.Web.Extensions.Azure; + +internal sealed class InterceptingPageable : Pageable where T : notnull +{ + private readonly Action> interceptPage; + private readonly Action interceptSuccess; + private readonly Pageable originalPageable; + + public InterceptingPageable(Pageable originalPageable, Action> interceptPage, Action interceptSuccess) + { + this.originalPageable = originalPageable; + this.interceptPage = interceptPage; + this.interceptSuccess = interceptSuccess; + } + + public override IEnumerable> AsPages(string? continuationToken = null, int? pageSizeHint = null) + { + foreach (var page in this.originalPageable.AsPages(continuationToken, pageSizeHint)) + { + this.interceptPage(page); + yield return page; + } + } + + public override IEnumerator GetEnumerator() + { + foreach (var item in this.originalPageable) + { + this.interceptSuccess(); + yield return item; + } + } +} diff --git a/Net.Sdk.Web.Extensions.Azure/NamedBlobContainer.cs b/Net.Sdk.Web.Extensions.Azure/NamedBlobContainer.cs new file mode 100644 index 0000000..270024d --- /dev/null +++ b/Net.Sdk.Web.Extensions.Azure/NamedBlobContainer.cs @@ -0,0 +1,38 @@ +using Azure.Core; +using Azure; +using Azure.Storage.Blobs; +using Azure.Storage; +using Net.Sdk.Web.Extensions.Azure.Options; + +namespace Net.Sdk.Web.Extensions.Azure; + +public class NamedBlobContainerClient : BlobContainerClient +{ + public NamedBlobContainerClient(string connectionString, string blobContainerName) : base(connectionString, blobContainerName) + { + } + + public NamedBlobContainerClient(Uri blobContainerUri, BlobClientOptions? options = null) : base(blobContainerUri, options) + { + } + + public NamedBlobContainerClient(string connectionString, string blobContainerName, BlobClientOptions options) : base(connectionString, blobContainerName, options) + { + } + + public NamedBlobContainerClient(Uri blobContainerUri, StorageSharedKeyCredential credential, BlobClientOptions? options = null) : base(blobContainerUri, credential, options) + { + } + + public NamedBlobContainerClient(Uri blobContainerUri, AzureSasCredential credential, BlobClientOptions? options = null) : base(blobContainerUri, credential, options) + { + } + + public NamedBlobContainerClient(Uri blobContainerUri, TokenCredential credential, BlobClientOptions? options = null) : base(blobContainerUri, credential, options) + { + } + + protected NamedBlobContainerClient() + { + } +} diff --git a/Net.Sdk.Web.Extensions.Azure/NamedTableClient.cs b/Net.Sdk.Web.Extensions.Azure/NamedTableClient.cs new file mode 100644 index 0000000..9426e51 --- /dev/null +++ b/Net.Sdk.Web.Extensions.Azure/NamedTableClient.cs @@ -0,0 +1,359 @@ +using Azure.Core; +using Azure; +using System.Diagnostics; +using System.Extensions; +using System.Linq.Expressions; +using Azure.Data.Tables; +using System.Core.Extensions; +using Azure.Data.Tables.Models; + +namespace Net.Sdk.Web.Extensions.Azure; + +public class NamedTableClient : TableClient +{ + private readonly ILogger> logger; + + public NamedTableClient(ILogger> logger) + { + this.logger = logger.ThrowIfNull(); + } + + public NamedTableClient(ILogger> logger, Uri endpoint, TableClientOptions? options = null) : base(endpoint, options) + { + this.logger = logger.ThrowIfNull(); + } + + public NamedTableClient(ILogger> logger, string connectionString, string tableName) : base(connectionString, tableName) + { + this.logger = logger.ThrowIfNull(); + } + + public NamedTableClient(ILogger> logger, Uri endpoint, AzureSasCredential credential, TableClientOptions? options = null) : base(endpoint, credential, options) + { + this.logger = logger.ThrowIfNull(); + } + + public NamedTableClient(ILogger> logger, Uri endpoint, string tableName, TableSharedKeyCredential credential) : base(endpoint, tableName, credential) + { + this.logger = logger.ThrowIfNull(); + } + + public NamedTableClient(ILogger> logger, string connectionString, string tableName, TableClientOptions? options = null) : base(connectionString, tableName, options) + { + this.logger = logger.ThrowIfNull(); + } + + public NamedTableClient(ILogger> logger, Uri endpoint, string tableName, TableSharedKeyCredential credential, TableClientOptions? options = null) : base(endpoint, tableName, credential, options) + { + this.logger = logger.ThrowIfNull(); + } + + public NamedTableClient(ILogger> logger, Uri endpoint, string tableName, TokenCredential tokenCredential, TableClientOptions? options = null) : base(endpoint, tableName, tokenCredential, options) + { + this.logger = logger.ThrowIfNull(); + } + + public override Response Create(CancellationToken cancellationToken = default) + { + return this.LogOperation(() => base.Create(cancellationToken), nameof(this.Create)); + } + + public override Response CreateIfNotExists(CancellationToken cancellationToken = default) + { + return this.LogOperation(() => base.CreateIfNotExists(cancellationToken), nameof(this.CreateIfNotExists)); + } + + public override Response AddEntity(T entity, CancellationToken cancellationToken = default) + { + return this.LogOperation(() => base.AddEntity(entity, cancellationToken), nameof(this.AddEntity)); + } + + public override Response Delete(CancellationToken cancellationToken = default) + { + return this.LogOperation(() => base.Delete(cancellationToken), nameof(this.Delete)); + } + + public override Response DeleteEntity(string partitionKey, string rowKey, ETag ifMatch = default, CancellationToken cancellationToken = default) + { + return this.LogOperation(() => base.DeleteEntity(partitionKey, rowKey, ifMatch, cancellationToken), nameof(this.DeleteEntity)); + } + + public override Response UpdateEntity(T entity, ETag ifMatch, TableUpdateMode mode = TableUpdateMode.Merge, CancellationToken cancellationToken = default) + { + return this.LogOperation(() => base.UpdateEntity(entity, ifMatch, mode, cancellationToken), nameof(this.UpdateEntity)); + } + + public override Response UpsertEntity(T entity, TableUpdateMode mode = TableUpdateMode.Merge, CancellationToken cancellationToken = default) + { + return this.LogOperation(() => base.UpsertEntity(entity, mode, cancellationToken), nameof(this.UpsertEntity)); + } + + public override Response GetEntity(string partitionKey, string rowKey, IEnumerable? select = null, CancellationToken cancellationToken = default) + { + return this.LogOperation(() => base.GetEntity(partitionKey, rowKey, select, cancellationToken), nameof(this.GetEntity)); + } + + public override NullableResponse GetEntityIfExists(string partitionKey, string rowKey, IEnumerable? select = null, CancellationToken cancellationToken = default) + { + return this.LogOperation(() => base.GetEntityIfExists(partitionKey, rowKey, select, cancellationToken), nameof(GetEntityIfExists)); + } + + public override Pageable Query(string? filter = null, int? maxPerPage = null, IEnumerable? select = null, CancellationToken cancellationToken = default) + { + return this.LogOperation(() => base.Query(filter, maxPerPage, select, cancellationToken), nameof(this.Query)); + } + + public override Pageable Query(Expression> filter, int? maxPerPage = null, IEnumerable? select = null, CancellationToken cancellationToken = default) + { + return this.LogOperation(() => base.Query(filter, maxPerPage, select, cancellationToken), nameof(this.Query)); + } + + public override Response> SubmitTransaction(IEnumerable transactionActions, CancellationToken cancellationToken = default) + { + return this.LogOperation(() => base.SubmitTransaction(transactionActions, cancellationToken), nameof(this.SubmitTransaction)); + } + + public override Task> CreateAsync(CancellationToken cancellationToken = default) + { + return this.LogOperation(() => base.CreateAsync(cancellationToken), nameof(this.CreateAsync)); + } + + public override Task> CreateIfNotExistsAsync(CancellationToken cancellationToken = default) + { + return this.LogOperation(() => base.CreateIfNotExistsAsync(cancellationToken), nameof(this.CreateIfNotExistsAsync)); + } + + public override Task AddEntityAsync(T entity, CancellationToken cancellationToken = default) + { + return this.LogOperation(() => base.AddEntityAsync(entity, cancellationToken), nameof(this.AddEntityAsync)); + } + + public override Task DeleteAsync(CancellationToken cancellationToken = default) + { + return this.LogOperation(() => base.DeleteAsync(cancellationToken), nameof(this.DeleteAsync)); + } + + public override Task DeleteEntityAsync(string partitionKey, string rowKey, ETag ifMatch = default, CancellationToken cancellationToken = default) + { + return this.LogOperation(() => base.DeleteEntityAsync(partitionKey, rowKey, ifMatch, cancellationToken), nameof(this.DeleteEntityAsync)); + } + + public override Task UpdateEntityAsync(T entity, ETag ifMatch, TableUpdateMode mode = TableUpdateMode.Merge, CancellationToken cancellationToken = default) + { + return this.LogOperation(() => base.UpdateEntityAsync(entity, ifMatch, mode, cancellationToken), nameof(this.UpdateEntityAsync)); + } + + public override Task UpsertEntityAsync(T entity, TableUpdateMode mode = TableUpdateMode.Merge, CancellationToken cancellationToken = default) + { + return this.LogOperation(() => base.UpsertEntityAsync(entity, mode, cancellationToken), nameof(this.UpsertEntityAsync)); + } + + public override Task> GetEntityAsync(string partitionKey, string rowKey, IEnumerable? select = null, CancellationToken cancellationToken = default) + { + return this.LogOperation(() => base.GetEntityAsync(partitionKey, rowKey, select, cancellationToken), nameof(this.GetEntityAsync)); + } + + public override Task> GetEntityIfExistsAsync(string partitionKey, string rowKey, IEnumerable? select = null, CancellationToken cancellationToken = default) + { + return this.LogOperation(() => base.GetEntityIfExistsAsync(partitionKey, rowKey, select, cancellationToken), nameof(this.GetEntityIfExistsAsync)); + } + + public override AsyncPageable QueryAsync(Expression> filter, int? maxPerPage = null, IEnumerable? select = null, CancellationToken cancellationToken = default) + { + return this.LogOperation(() => base.QueryAsync(filter, maxPerPage, select, cancellationToken), nameof(this.QueryAsync)); + } + + public override AsyncPageable QueryAsync(string? filter = null, int? maxPerPage = null, IEnumerable? select = null, CancellationToken cancellationToken = default) + { + return this.LogOperation(() => base.QueryAsync(filter, maxPerPage, select, cancellationToken), nameof(this.QueryAsync)); + } + + public override Task>> SubmitTransactionAsync(IEnumerable transactionActions, CancellationToken cancellationToken = default) + { + return this.LogOperation(() => base.SubmitTransactionAsync(transactionActions, cancellationToken), nameof(this.SubmitTransactionAsync)); + } + + private Response LogOperation(Func func, string operationName) + { + var scopedLogger = this.logger.CreateScopedLogger(operationName, string.Empty); + var sw = Stopwatch.StartNew(); + scopedLogger.LogInformation(">> {0}", this.Uri); + try + { + var response = func(); + scopedLogger.LogInformation("<< {0} {1}ms", response.Status, sw.ElapsedMilliseconds); + return response; + } + catch (RequestFailedException ex) + { + scopedLogger.LogInformation("<< {0} {1}ms", ex.Status, sw.ElapsedMilliseconds); + throw; + } + } + + private Response LogOperation(Func> func, string operationName) + { + var scopedLogger = this.logger.CreateScopedLogger(operationName, string.Empty); + var sw = Stopwatch.StartNew(); + scopedLogger.LogInformation(">> {0}", this.Uri); + try + { + var response = func(); + scopedLogger.LogInformation("<< {0} {1}ms", response.GetRawResponse().Status, sw.ElapsedMilliseconds); + return response; + } + catch (RequestFailedException ex) + { + scopedLogger.LogInformation("<< {0} {1}ms", ex.Status, sw.ElapsedMilliseconds); + throw; + } + } + + private NullableResponse LogOperation(Func> func, string operationName) + { + var scopedLogger = this.logger.CreateScopedLogger(operationName, string.Empty); + var sw = Stopwatch.StartNew(); + scopedLogger.LogInformation(">> {0}", this.Uri); + try + { + var response = func(); + scopedLogger.LogInformation("<< {0} {1}ms", response.GetRawResponse().Status, sw.ElapsedMilliseconds); + return response; + } + catch (RequestFailedException ex) + { + scopedLogger.LogInformation("<< {0} {1}ms", ex.Status, sw.ElapsedMilliseconds); + throw; + } + } + + private Pageable LogOperation(Func> func, string operationName) + where T : notnull + { + var scopedLogger = this.logger.CreateScopedLogger(operationName, string.Empty); + var sw = Stopwatch.StartNew(); + scopedLogger.LogInformation(">> {0}", this.Uri); + var response = func(); + return new InterceptingPageable(response, page => + { + scopedLogger.LogInformation("<< {0} {1}ms", page.GetRawResponse().Status, sw.ElapsedMilliseconds); + }, () => + { + scopedLogger.LogInformation("<< 200 {1}ms", sw.ElapsedMilliseconds); + }); + } + + private Response> LogOperation(Func>> func, string operationName) + { + var scopedLogger = this.logger.CreateScopedLogger(operationName, string.Empty); + var sw = Stopwatch.StartNew(); + scopedLogger.LogInformation(">> {0}", this.Uri); + try + { + var response = func(); + foreach (var action in response.Value) + { + scopedLogger.LogInformation("<< {0} {1}ms", action.Status, sw.ElapsedMilliseconds); + } + + return response; + } + catch (RequestFailedException ex) + { + scopedLogger.LogInformation("<< {0} {1}ms", ex.Status, sw.ElapsedMilliseconds); + throw; + } + } + + private async Task LogOperation(Func> func, string operationName) + { + var scopedLogger = this.logger.CreateScopedLogger(operationName, string.Empty); + var sw = Stopwatch.StartNew(); + scopedLogger.LogInformation(">> {0}", this.Uri); + try + { + var response = await func(); + scopedLogger.LogInformation("<< {0} {1}ms", response.Status, sw.ElapsedMilliseconds); + return response; + } + catch (RequestFailedException ex) + { + scopedLogger.LogInformation("<< {0} {1}ms", ex.Status, sw.ElapsedMilliseconds); + throw; + } + } + + private async Task> LogOperation(Func>> func, string operationName) + { + var scopedLogger = this.logger.CreateScopedLogger(operationName, string.Empty); + var sw = Stopwatch.StartNew(); + scopedLogger.LogInformation(">> {0}", this.Uri); + try + { + var response = await func(); + scopedLogger.LogInformation("<< {0} {1}ms", response.GetRawResponse().Status, sw.ElapsedMilliseconds); + return response; + } + catch (RequestFailedException ex) + { + scopedLogger.LogInformation("<< {0} {1}ms", ex.Status, sw.ElapsedMilliseconds); + throw; + } + } + + private async Task> LogOperation(Func>> func, string operationName) + { + var scopedLogger = this.logger.CreateScopedLogger(operationName, string.Empty); + var sw = Stopwatch.StartNew(); + scopedLogger.LogInformation(">> {0}", this.Uri); + try + { + var response = await func(); + scopedLogger.LogInformation("<< {0} {1}ms", response.GetRawResponse().Status, sw.ElapsedMilliseconds); + return response; + } + catch (RequestFailedException ex) + { + scopedLogger.LogInformation("<< {0} {1}ms", ex.Status, sw.ElapsedMilliseconds); + throw; + } + } + + private AsyncPageable LogOperation(Func> func, string operationName) + where T : notnull + { + var scopedLogger = this.logger.CreateScopedLogger(operationName, string.Empty); + var sw = Stopwatch.StartNew(); + scopedLogger.LogInformation(">> {0}", this.Uri); + var response = func(); + return new InterceptingAsyncPageable(response, page => + { + scopedLogger.LogInformation("<< {0} {1}ms", page.GetRawResponse().Status, sw.ElapsedMilliseconds); + }, () => + { + scopedLogger.LogInformation("<< 200 {1}ms", sw.ElapsedMilliseconds); + }); + } + + private async Task>> LogOperation(Func>>> func, string operationName) + { + var scopedLogger = this.logger.CreateScopedLogger(operationName, string.Empty); + var sw = Stopwatch.StartNew(); + scopedLogger.LogInformation(">> {0}", this.Uri); + try + { + var response = await func(); + foreach (var action in response.Value) + { + scopedLogger.LogInformation("<< {0} {1}ms", action.Status, sw.ElapsedMilliseconds); + } + + return response; + } + catch (RequestFailedException ex) + { + scopedLogger.LogInformation("<< {0} {1}ms", ex.Status, sw.ElapsedMilliseconds); + throw; + } + } +} \ No newline at end of file diff --git a/Net.Sdk.Web.Extensions.Azure/Net.Sdk.Web.Extensions.Azure.csproj b/Net.Sdk.Web.Extensions.Azure/Net.Sdk.Web.Extensions.Azure.csproj new file mode 100644 index 0000000..d4ca07c --- /dev/null +++ b/Net.Sdk.Web.Extensions.Azure/Net.Sdk.Web.Extensions.Azure.csproj @@ -0,0 +1,33 @@ + + + + net8.0 + enable + enable + Library + true + 0.7 + Alexandru Macocian + LICENSE + true + https://github.com/AlexMacocian/Net.Sdk.Web.Extensions + Azure specific extensions for Microsoft.NET.Sdk.Web + latest + + + + + + True + + + + + + + + + + + + diff --git a/Net.Sdk.Web.Extensions.Azure/Options/AzureCredentialsOptions.cs b/Net.Sdk.Web.Extensions.Azure/Options/AzureCredentialsOptions.cs new file mode 100644 index 0000000..618ee7c --- /dev/null +++ b/Net.Sdk.Web.Extensions.Azure/Options/AzureCredentialsOptions.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace Net.Sdk.Web.Extensions.Azure.Options; + +public sealed class AzureCredentialsOptions : IAzureClientSecretCredentialOptions +{ + [JsonPropertyName(nameof(ClientSecret))] + public string ClientSecret { get; set; } = default!; + + [JsonPropertyName(nameof(ClientId))] + public string ClientId { get; set; } = default!; + + [JsonPropertyName(nameof(TenantId))] + public string TenantId { get; set; } = default!; +} \ No newline at end of file diff --git a/Net.Sdk.Web.Extensions.Azure/Options/IAzureBlobStorageOptions.cs b/Net.Sdk.Web.Extensions.Azure/Options/IAzureBlobStorageOptions.cs new file mode 100644 index 0000000..70c7e0c --- /dev/null +++ b/Net.Sdk.Web.Extensions.Azure/Options/IAzureBlobStorageOptions.cs @@ -0,0 +1,6 @@ +namespace Net.Sdk.Web.Extensions.Azure.Options; + +public interface IAzureBlobStorageOptions +{ + string ContainerName { get; set; } +} diff --git a/Net.Sdk.Web.Extensions.Azure/Options/IAzureClientSecretCredentialOptions.cs b/Net.Sdk.Web.Extensions.Azure/Options/IAzureClientSecretCredentialOptions.cs new file mode 100644 index 0000000..38ac9da --- /dev/null +++ b/Net.Sdk.Web.Extensions.Azure/Options/IAzureClientSecretCredentialOptions.cs @@ -0,0 +1,6 @@ +namespace Net.Sdk.Web.Extensions.Azure.Options; + +public interface IAzureClientSecretCredentialOptions : IAzureCredentialOptions +{ + string ClientSecret { get; set; } +} \ No newline at end of file diff --git a/Net.Sdk.Web.Extensions.Azure/Options/IAzureCredentialOptions.cs b/Net.Sdk.Web.Extensions.Azure/Options/IAzureCredentialOptions.cs new file mode 100644 index 0000000..dfb36ed --- /dev/null +++ b/Net.Sdk.Web.Extensions.Azure/Options/IAzureCredentialOptions.cs @@ -0,0 +1,8 @@ +namespace Net.Sdk.Web.Extensions.Azure.Options; + +public interface IAzureCredentialOptions +{ + string ClientId { get; set; } + + string TenantId { get; set; } +} diff --git a/Net.Sdk.Web.Extensions.Azure/Options/IAzureTableStorageOptions.cs b/Net.Sdk.Web.Extensions.Azure/Options/IAzureTableStorageOptions.cs new file mode 100644 index 0000000..4165cec --- /dev/null +++ b/Net.Sdk.Web.Extensions.Azure/Options/IAzureTableStorageOptions.cs @@ -0,0 +1,6 @@ +namespace Net.Sdk.Web.Extensions.Azure.Options; + +public interface IAzureTableStorageOptions +{ + string TableName { get; set; } +} \ No newline at end of file diff --git a/Net.Sdk.Web.Extensions.Azure/Options/IStorageAccountOptions.cs b/Net.Sdk.Web.Extensions.Azure/Options/IStorageAccountOptions.cs new file mode 100644 index 0000000..00ee1d8 --- /dev/null +++ b/Net.Sdk.Web.Extensions.Azure/Options/IStorageAccountOptions.cs @@ -0,0 +1,6 @@ +namespace Net.Sdk.Web.Extensions.Azure.Options; + +public interface IStorageAccountOptions +{ + string AccountName { get; } +} diff --git a/Net.Sdk.Web.Extensions.Azure/WebApplicationBuilderExtensions.cs b/Net.Sdk.Web.Extensions.Azure/WebApplicationBuilderExtensions.cs new file mode 100644 index 0000000..e66f112 --- /dev/null +++ b/Net.Sdk.Web.Extensions.Azure/WebApplicationBuilderExtensions.cs @@ -0,0 +1,224 @@ +using Azure.Core; +using Azure.Identity; +using Microsoft.Extensions.Options; +using Net.Sdk.Web.Extensions.Azure.Options; +using System.Core.Extensions; +using System.Extensions; + +namespace Net.Sdk.Web.Extensions.Azure; + +public static class WebApplicationBuilderExtensions +{ + public static WebApplicationBuilder ConfigureAzureClientSecretCredentials(this WebApplicationBuilder builder) + { + builder.ThrowIfNull() + .ConfigureExtended() + .Services.AddSingleton(sp => + { + var options = sp.GetRequiredService>().Value; + return new ClientSecretCredential(options.TenantId, options.ClientId, options.ClientSecret); + }); + + return builder; + } + + public static WebApplicationBuilder ConfigureAzureClientSecretCredentials(this WebApplicationBuilder builder) + where TOptions : class, IAzureClientSecretCredentialOptions, new() + { + builder.ThrowIfNull() + .ConfigureExtended() + .Services.AddSingleton(sp => + { + var options = sp.GetRequiredService>().Value; + return new ClientSecretCredential(options.TenantId, options.ClientId, options.ClientSecret); + }); + + return builder; + } + + public static WebApplicationBuilder WithTableClientSingleton(this WebApplicationBuilder builder) + where TTableOptions : class, IAzureTableStorageOptions, new() + where TAccountOptions : class, IStorageAccountOptions, new() + { + builder.Services.ThrowIfNull() + .AddSingleton(sp => + { + var tokenCredential = sp.GetRequiredService(); + var storageOptions = sp.GetRequiredService>(); + var clientOptions = sp.GetRequiredService>(); + var logger = sp.GetRequiredService>>(); + return new NamedTableClient(logger, new Uri($"https://{storageOptions.Value.AccountName}.table.core.windows.net"), clientOptions.Value.TableName, tokenCredential, default); + }); + + return builder; + } + + public static WebApplicationBuilder WithTableClientSingleton(this WebApplicationBuilder builder, string tableName) + where TAccountOptions : class, IStorageAccountOptions, new() + { + tableName.ThrowIfNull(); + builder.Services.ThrowIfNull() + .AddSingleton(sp => + { + var tokenCredential = sp.GetRequiredService(); + var storageOptions = sp.GetRequiredService>(); + var logger = sp.GetRequiredService>>(); + return new NamedTableClient(logger, new Uri($"https://{storageOptions.Value.AccountName}.table.core.windows.net"), tableName, tokenCredential, default); + }); + + return builder; + } + + public static WebApplicationBuilder WithTableClientSingleton(this WebApplicationBuilder builder, string accountName, string tableName) + { + tableName.ThrowIfNull(); + accountName.ThrowIfNull(); + builder.Services.ThrowIfNull() + .AddSingleton(sp => + { + var tokenCredential = sp.GetRequiredService(); + var logger = sp.GetRequiredService>>(); + return new NamedTableClient(logger, new Uri($"https://{accountName}.table.core.windows.net"), tableName, tokenCredential, default); + }); + + return builder; + } + + public static WebApplicationBuilder WithTableClientScoped(this WebApplicationBuilder builder) + where TTableOptions : class, IAzureTableStorageOptions, new() + where TAccountOptions : class, IStorageAccountOptions, new() + { + builder.Services.ThrowIfNull() + .AddScoped(sp => + { + var tokenCredential = sp.GetRequiredService(); + var storageOptions = sp.GetRequiredService>(); + var clientOptions = sp.GetRequiredService>(); + var logger = sp.GetRequiredService>>(); + return new NamedTableClient(logger, new Uri($"https://{storageOptions.Value.AccountName}.table.core.windows.net"), clientOptions.Value.TableName, tokenCredential, default); + }); + + return builder; + } + + public static WebApplicationBuilder WithTableClientScoped(this WebApplicationBuilder builder, string tableName) + where TAccountOptions : class, IStorageAccountOptions, new() + { + tableName.ThrowIfNull(); + builder.Services.ThrowIfNull() + .AddScoped(sp => + { + var tokenCredential = sp.GetRequiredService(); + var storageOptions = sp.GetRequiredService>(); + var logger = sp.GetRequiredService>>(); + return new NamedTableClient(logger, new Uri($"https://{storageOptions.Value.AccountName}.table.core.windows.net"), tableName, tokenCredential, default); + }); + + return builder; + } + + public static WebApplicationBuilder WithTableClientScoped(this WebApplicationBuilder builder, string accountName, string tableName) + { + tableName.ThrowIfNull(); + accountName.ThrowIfNull(); + builder.Services.ThrowIfNull() + .AddScoped(sp => + { + var tokenCredential = sp.GetRequiredService(); + var logger = sp.GetRequiredService>>(); + return new NamedTableClient(logger, new Uri($"https://{accountName}.table.core.windows.net"), tableName, tokenCredential, default); + }); + + return builder; + } + + public static WebApplicationBuilder WithBlobContainerClientSingleton(this WebApplicationBuilder builder) + where TBlobContainerOptions : class, IAzureBlobStorageOptions, new() + where TAccountOptions : class, IStorageAccountOptions, new() + { + builder.ThrowIfNull() + .Services.AddSingleton(sp => + { + var tokenCredential = sp.GetRequiredService(); + var storageOptions = sp.GetRequiredService>(); + var clientOptions = sp.GetRequiredService>(); + return new NamedBlobContainerClient(new Uri($"https://{storageOptions.Value.AccountName}.blob.core.windows.net/{clientOptions.Value.ContainerName}"), tokenCredential, default); + }); + + return builder; + } + + public static WebApplicationBuilder WithBlobContainerClientSingleton(this WebApplicationBuilder builder, string containerName) + where TAccountOptions : class, IStorageAccountOptions, new() + { + containerName.ThrowIfNull(); + builder.ThrowIfNull() + .Services.AddSingleton(sp => + { + var tokenCredential = sp.GetRequiredService(); + var storageOptions = sp.GetRequiredService>(); + return new NamedBlobContainerClient(new Uri($"https://{storageOptions.Value.AccountName}.blob.core.windows.net/{containerName}"), tokenCredential, default); + }); + + return builder; + } + + public static WebApplicationBuilder WithBlobContainerClientSingleton(this WebApplicationBuilder builder, string accountName, string containerName) + { + accountName.ThrowIfNull(); + containerName.ThrowIfNull(); + builder.ThrowIfNull() + .Services.AddSingleton(sp => + { + var tokenCredential = sp.GetRequiredService(); + return new NamedBlobContainerClient(new Uri($"https://{accountName}.blob.core.windows.net/{containerName}"), tokenCredential, default); + }); + + return builder; + } + + public static WebApplicationBuilder WithBlobContainerClientScoped(this WebApplicationBuilder builder) + where TBlobContainerOptions : class, IAzureBlobStorageOptions, new() + where TAccountOptions : class, IStorageAccountOptions, new() + { + builder.ThrowIfNull() + .Services.AddScoped(sp => + { + var tokenCredential = sp.GetRequiredService(); + var storageOptions = sp.GetRequiredService>(); + var clientOptions = sp.GetRequiredService>(); + return new NamedBlobContainerClient(new Uri($"https://{storageOptions.Value.AccountName}.blob.core.windows.net/{clientOptions.Value.ContainerName}"), tokenCredential, default); + }); + + return builder; + } + + public static WebApplicationBuilder WithBlobContainerClientScoped(this WebApplicationBuilder builder, string containerName) + where TAccountOptions : class, IStorageAccountOptions, new() + { + containerName.ThrowIfNull(); + builder.ThrowIfNull() + .Services.AddScoped(sp => + { + var tokenCredential = sp.GetRequiredService(); + var storageOptions = sp.GetRequiredService>(); + return new NamedBlobContainerClient(new Uri($"https://{storageOptions.Value.AccountName}.blob.core.windows.net/{containerName}"), tokenCredential, default); + }); + + return builder; + } + + public static WebApplicationBuilder WithBlobContainerClientScoped(this WebApplicationBuilder builder, string accountName, string containerName) + { + accountName.ThrowIfNull(); + containerName.ThrowIfNull(); + builder.ThrowIfNull() + .Services.AddScoped(sp => + { + var tokenCredential = sp.GetRequiredService(); + return new NamedBlobContainerClient(new Uri($"https://{accountName}.blob.core.windows.net/{containerName}"), tokenCredential, default); + }); + + return builder; + } +} diff --git a/Net.Sdk.Web.Extensions.sln b/Net.Sdk.Web.Extensions.sln index a96077a..f53a5ac 100644 --- a/Net.Sdk.Web.Extensions.sln +++ b/Net.Sdk.Web.Extensions.sln @@ -23,6 +23,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{ .github\workflows\ci.yaml = .github\workflows\ci.yaml EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Net.Sdk.Web.Extensions.Azure", "Net.Sdk.Web.Extensions.Azure\Net.Sdk.Web.Extensions.Azure.csproj", "{F9D782AF-2E99-497A-B5C2-5DF6FB254EE5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -33,6 +35,10 @@ Global {D0570CEC-F740-4332-AC10-8491974D1827}.Debug|Any CPU.Build.0 = Debug|Any CPU {D0570CEC-F740-4332-AC10-8491974D1827}.Release|Any CPU.ActiveCfg = Release|Any CPU {D0570CEC-F740-4332-AC10-8491974D1827}.Release|Any CPU.Build.0 = Release|Any CPU + {F9D782AF-2E99-497A-B5C2-5DF6FB254EE5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F9D782AF-2E99-497A-B5C2-5DF6FB254EE5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F9D782AF-2E99-497A-B5C2-5DF6FB254EE5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F9D782AF-2E99-497A-B5C2-5DF6FB254EE5}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE