Skip to content

Commit

Permalink
πŸ› οΈ Refactoring on preparation for SSR App deployment
Browse files Browse the repository at this point in the history
πŸ“ Adding support for Azure Insights
πŸ“ Adding support for Key Vault
πŸ“ Adding support for Token Credential for Azure Services
πŸ”Ό Updating dependencies
  • Loading branch information
xxnickles committed Feb 1, 2024
1 parent faab56c commit 7c7082f
Show file tree
Hide file tree
Showing 23 changed files with 222 additions and 110 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@

<ItemGroup>
<PackageReference Include="Azure.Data.Tables" Version="12.8.2" />
<PackageReference Include="Azure.Identity" Version="1.10.4" />
<PackageReference Include="Azure.Storage.Blobs" Version="12.19.1" />
<PackageReference Include="Azure.Storage.Queues" Version="12.17.1" />
<PackageReference Include="LanguageExt.Core" Version="4.4.7" />
<PackageReference Include="FuzzySharp" Version="2.0.2" />
<PackageReference Include="MediatR" Version="12.2.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageReference Include="Passwordless" Version="1.0.2" />
<PackageReference Include="PuppeteerSharp" Version="14.0.0" />
<PackageReference Include="SendGrid" Version="9.29.1" />
Expand Down
33 changes: 17 additions & 16 deletions src/AnimeFeedManager.Features/Images/IO/AzureImagesBlobStore.cs
Original file line number Diff line number Diff line change
@@ -1,26 +1,33 @@
ο»Ώusing AnimeFeedManager.Features.Infrastructure.Messaging;
using Azure.Identity;
using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models;
using Microsoft.Extensions.Options;

namespace AnimeFeedManager.Features.Images.IO;

public class AzureImagesBlobStore : IImagesBlobStore, IDisposable
public class AzureImagesBlobStore : IImagesBlobStore
{
private const string Container = "images";
private BlobContainerClient _containerClient;
private readonly IDisposable? _optionsReference;
public AzureImagesBlobStore(IOptionsMonitor<AzureBlobStorageOptions> blobStorageOptions)

public AzureImagesBlobStore(AzureStorageSettings storageOptions)
{
_containerClient = new BlobContainerClient(blobStorageOptions.CurrentValue.StorageConnectionString, Container);
_containerClient = GetClient(storageOptions);
Initialize();
}

_optionsReference = blobStorageOptions.OnChange(options =>
private static BlobContainerClient GetClient(AzureStorageSettings azureSettings)
{
return azureSettings switch
{
_containerClient = new BlobContainerClient(options.StorageConnectionString, Container);
Initialize();
});

ConnectionStringSettings connectionStringOptions => new BlobContainerClient(
connectionStringOptions.StorageConnectionString, Container),
TokenCredentialSettings tokenCredentialOptions => new BlobContainerClient(
new Uri(tokenCredentialOptions.BlobUri, Container), new DefaultAzureCredential()),
_ => throw new ArgumentException(
"Provided Table Storage configuration is not valid. Make sure Configurations for Azure table Storage is correct for either connection string or managed identities",
nameof(TableClientOptions))
};
}

private void Initialize()
Expand All @@ -41,10 +48,4 @@ public async Task<Uri> Upload(string fileName, string path, Stream data)
await blob.UploadAsync(data, new BlobUploadOptions {HttpHeaders = blobHttpHeader});
return blob.Uri;
}


public void Dispose()
{
_optionsReference?.Dispose();
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
ο»Ώusing System.Text.Json;
using AnimeFeedManager.Common.Domain.Errors;
using Azure.Identity;
using Azure.Storage.Queues;
using Microsoft.Extensions.Options;

namespace AnimeFeedManager.Features.Infrastructure.Messaging;

Expand Down Expand Up @@ -30,28 +30,21 @@ Task<Either<DomainError, Unit>> SendDelayedMessage<T>(T message, Box destiny, Mi
CancellationToken cancellationToken = default);
}

public class AzureQueueMessages : IDomainPostman, IDisposable
public class AzureQueueMessages : IDomainPostman
{
private AzureBlobStorageOptions _blobStorageOptions;
private AzureStorageSettings _azureSettings;
private readonly JsonSerializerOptions _jsonOptions;
private readonly QueueClientOptions _queueClientOptions;
private readonly IDisposable? _optionsReference;
private readonly QueueClientOptions _queueClientOptions;

public AzureQueueMessages(
IOptionsMonitor<AzureBlobStorageOptions> blobStorageOptions)
public AzureQueueMessages(AzureStorageSettings tableStorageSettings)
{
_blobStorageOptions = blobStorageOptions.CurrentValue;
_azureSettings = tableStorageSettings;
_jsonOptions = new JsonSerializerOptions(new JsonSerializerOptions
{ PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
{PropertyNamingPolicy = JsonNamingPolicy.CamelCase});
_queueClientOptions = new QueueClientOptions
{
MessageEncoding = QueueMessageEncoding.Base64
};
_optionsReference = blobStorageOptions.OnChange(options =>
{
_blobStorageOptions = options;
});

}

public async Task<Either<DomainError, Unit>> SendMessage<T>(T message, Box destiny,
Expand Down Expand Up @@ -85,19 +78,28 @@ public async Task<Either<DomainError, Unit>> SendDelayedMessage<T>(T message, Bo
private async Task SendMessage<T>(T message, string destiny, TimeSpan? delay = default,
CancellationToken cancellationToken = default)
{
var queue = new QueueClient(_blobStorageOptions?.StorageConnectionString ?? string.Empty, destiny,
_queueClientOptions);
var queue = GetClient(destiny);
await queue.CreateIfNotExistsAsync(cancellationToken: cancellationToken);
await queue.SendMessageAsync(AsBinary(message), cancellationToken: cancellationToken, visibilityTimeout: delay);
}

private BinaryData AsBinary<T>(T data)
private QueueClient GetClient(string destiny)
{
return BinaryData.FromObjectAsJson(data, _jsonOptions);
return _azureSettings switch
{
ConnectionStringSettings connectionStringOptions => new QueueClient(
connectionStringOptions.StorageConnectionString, destiny,
_queueClientOptions),
TokenCredentialSettings tokenCredentialOptions => new QueueClient(
tokenCredentialOptions.QueueUri, new DefaultAzureCredential(), _queueClientOptions),
_ => throw new ArgumentException(
"Provided Table Storage configuration is not valid. Make sure Configurations for Azure table Storage is correct for either connection string or managed identities",
nameof(TableClientOptions))
};
}

public void Dispose()
private BinaryData AsBinary<T>(T data)
{
_optionsReference?.Dispose();
return BinaryData.FromObjectAsJson(data, _jsonOptions);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
ο»Ώnamespace AnimeFeedManager.Features.Infrastructure.Messaging;

public abstract record AzureStorageSettings;

public sealed record ConnectionStringSettings(string StorageConnectionString) : AzureStorageSettings;

public sealed record TokenCredentialSettings(QueueUri QueueUri, BlobUri BlobUri) : AzureStorageSettings;

public readonly record struct QueueUri(Uri Uri)
{
public static implicit operator Uri(QueueUri queueUri) => queueUri.Uri;
}

public readonly record struct BlobUri(Uri Uri)
{
public static implicit operator Uri(BlobUri blobUri) => blobUri.Uri;
}
69 changes: 60 additions & 9 deletions src/AnimeFeedManager.Features/Infrastructure/Registration.cs
Original file line number Diff line number Diff line change
@@ -1,22 +1,73 @@
ο»Ώusing AnimeFeedManager.Features.Infrastructure.Messaging;
ο»Ώusing System.Diagnostics.CodeAnalysis;
using AnimeFeedManager.Features.Infrastructure.Messaging;
using Azure.Identity;
using Microsoft.Extensions.Configuration;

namespace AnimeFeedManager.Features.Infrastructure;

public static class InfrastructureRegistration
{
public static IServiceCollection RegisterStorage(this IServiceCollection services, string connectionString)
private const string TableBaseUrl = "https://{0}.table.core.windows.net";
private const string QueueBaseUrl = "https://{0}.queue.core.windows.net";
private const string BlobBaseUrl = "https://{0}.blob.core.windows.net";


public static IServiceCollection RegisterStorage(this IServiceCollection services,
string connectionString)
{
RegisterWithConnectionString(services, connectionString);
services.RegisterCommonServices();
return services;
}

public static IServiceCollection RegisterStorage(this IServiceCollection services,
IConfigurationManager configuration)
{

services.Configure<AzureBlobStorageOptions>(options =>
var storageAccountName = configuration["StorageAccountName"];
if (!string.IsNullOrEmpty(storageAccountName))
{
options.StorageConnectionString = connectionString;
});
RegisterWithAzureIdentity(services, storageAccountName);
}
else
{
RegisterWithConnectionString(services, configuration.GetConnectionString("AzureStorage") ?? string.Empty);
}

services.RegisterCommonServices();

return services;
}

var tableClient = new TableServiceClient(connectionString);
private static void RegisterCommonServices(this IServiceCollection services)
{
services.TryAddSingleton<IDomainPostman, AzureQueueMessages>();
services.TryAddSingleton(typeof(ITableClientFactory<>), typeof(TableClientFactory<>));
services.TryAddSingleton(tableClient);
}

return services;
private static void RegisterWithAzureIdentity(IServiceCollection services, string storageAccountName)
{
if (CreateUri(TableBaseUrl, storageAccountName, out var tableUri) &&
CreateUri(QueueBaseUrl, storageAccountName, out var queueUri) &&
CreateUri(BlobBaseUrl, storageAccountName, out var blobUri))
{
services.TryAddSingleton<AzureStorageSettings>(
new TokenCredentialSettings(new QueueUri(queueUri), new BlobUri(blobUri)));
services.TryAddSingleton(new TableServiceClient(tableUri, new DefaultAzureCredential()));
}
else
{
throw new ArgumentException("Azure Storage resources are malformed.");
}
}

private static void RegisterWithConnectionString(IServiceCollection services, string connectionString)
{
services.TryAddSingleton<AzureStorageSettings>(new ConnectionStringSettings(connectionString));
services.TryAddSingleton(new TableServiceClient(connectionString));
}

private static bool CreateUri(string baseUrl, string storageAccountName, [NotNullWhen(true)] out Uri? tableUri)
{
return Uri.TryCreate($"{baseUrl}{storageAccountName}", UriKind.Absolute, out tableUri);
}
}
1 change: 0 additions & 1 deletion src/AnimeFeedManager.Features/Movies/Registration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using AnimeFeedManager.Features.Movies.Scrapping;
using AnimeFeedManager.Features.Movies.Scrapping.IO;
using AnimeFeedManager.Features.Movies.Subscriptions.IO;
using AnimeFeedManager.Features.Tv.Subscriptions.IO;

namespace AnimeFeedManager.Features.Movies;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
ο»Ώusing System.Collections.Frozen;
using System.Text.Json;
ο»Ώusing System.Text.Json;
using AnimeFeedManager.Common.Domain.Errors;

namespace AnimeFeedManager.Features.Seasons.IO;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.1.0" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.SignalRService" Version="1.12.0" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Storage" Version="6.2.0" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Storage.Queues" Version="5.2.0" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.SignalRService" Version="1.13.0" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Storage" Version="6.3.0" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Storage.Queues" Version="5.3.0" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Timer" Version="4.3.0" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="1.16.4" OutputItemType="Analyzer" />
<PackageReference Include="Microsoft.Azure.Functions.Worker" Version="1.20.1" />
<PackageReference Include="Microsoft.Azure.Functions.Worker" Version="1.21.0" />
<PackageReference Include="SendGrid" Version="9.29.1" />
</ItemGroup>
<ItemGroup>
Expand Down
3 changes: 3 additions & 0 deletions src/AnimeFeedManager.Web/AnimeFeedManager.Web.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.3.0" />
<PackageReference Include="Azure.Identity" Version="1.10.4" />
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.22.0" />
<PackageReference Include="Passwordless" Version="1.0.2" />
</ItemGroup>

Expand Down
53 changes: 49 additions & 4 deletions src/AnimeFeedManager.Web/Bootstrapping/Registration.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
ο»Ώusing AnimeFeedManager.Features.Images;
ο»Ώusing AnimeFeedManager.Common.Domain.Types;
using AnimeFeedManager.Features.Images;
using AnimeFeedManager.Features.Infrastructure;
using AnimeFeedManager.Features.Maintenance;
using AnimeFeedManager.Features.Migration;
Expand All @@ -9,18 +10,63 @@
using AnimeFeedManager.Features.State;
using AnimeFeedManager.Features.Tv;
using AnimeFeedManager.Features.Users;
using AnimeFeedManager.Web.Features.Security;
using Azure.Identity;
using MediatR.NotificationPublishers;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server;
using Passwordless.Net;

namespace AnimeFeedManager.Web.Bootstrapping;

internal static class Registration
{
internal static IServiceCollection RegisterAppDependencies(this IServiceCollection services, IConfigurationManager configuration)
internal static void TryAddVault(this IConfigurationManager configuration)
{
if (Uri.TryCreate(configuration["VaultUri"], UriKind.Absolute, out var vaultUri))
{
configuration.AddAzureKeyVault(vaultUri, new DefaultAzureCredential());
}
}

internal static IServiceCollection RegisterSecurityServices(this IServiceCollection services,
IConfigurationManager configuration)
{
services.AddCascadingAuthenticationState();
services.AddScoped<AuthenticationStateProvider, ServerAuthenticationStateProvider>();
services.AddAuthorization();
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = "/login";
options.LogoutPath = "/logout";
options.ExpireTimeSpan = TimeSpan.FromMinutes(30);
options.Cookie.MaxAge = options.ExpireTimeSpan;
options.SlidingExpiration = true;
});


services.AddAuthorizationBuilder()
.AddPolicy(Policies.AdminRequired, policy => policy.RequireRole(RoleNames.Admin));

// bind section Passwordless to the object PassworlessOptions
services.Configure<PasswordlessOptions>(configuration.GetSection("Passwordless"));
// Add Passworless
services.AddPasswordlessSdk(options => { configuration.GetRequiredSection("Passwordless").Bind(options); });

services.AddScoped<IUserProvider, UserProvider>();

return services;
}

internal static IServiceCollection RegisterAppDependencies(this IServiceCollection services,
IConfigurationManager configuration)
{
// MediatR
services.RegisterMediatR();
// Storage
services.RegisterStorage(configuration.GetConnectionString("AzureStorage") ?? string.Empty);
services.RegisterStorage(configuration);
// App
services.RegisterSeasonsServices();
services.RegisterImageServices();
Expand All @@ -47,5 +93,4 @@ private static IServiceCollection RegisterMediatR(this IServiceCollection servic

return services;
}

}
Loading

0 comments on commit 7c7082f

Please sign in to comment.