diff --git a/src/OrchardCore.Modules/OrchardCore.DataProtection.Azure/BlobOptions.cs b/src/OrchardCore.Modules/OrchardCore.DataProtection.Azure/BlobOptions.cs new file mode 100644 index 00000000000..1db47d2a1e2 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.DataProtection.Azure/BlobOptions.cs @@ -0,0 +1,12 @@ +namespace OrchardCore.DataProtection.Azure; + +public class BlobOptions +{ + public string ConnectionString { get; set; } + + public string ContainerName { get; set; } = "dataprotection"; + + public string BlobName { get; set; } + + public bool CreateContainer { get; set; } = true; +} diff --git a/src/OrchardCore.Modules/OrchardCore.DataProtection.Azure/BlobOptionsSetup.cs b/src/OrchardCore.Modules/OrchardCore.DataProtection.Azure/BlobOptionsSetup.cs new file mode 100644 index 00000000000..a1652835ad0 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.DataProtection.Azure/BlobOptionsSetup.cs @@ -0,0 +1,109 @@ +using System; +using System.Threading.Tasks; +using Azure.Storage.Blobs; +using Azure.Storage.Blobs.Models; +using Fluid; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using OrchardCore.Environment.Shell; +using OrchardCore.Environment.Shell.Configuration; + +namespace OrchardCore.DataProtection.Azure; + +public class BlobOptionsSetup +{ + private readonly FluidParser _fluidParser = new(); + + private readonly IShellConfiguration _configuration; + private readonly ShellOptions _shellOptions; + private readonly ShellSettings _shellSettings; + private readonly ILogger _logger; + + public BlobOptionsSetup( + IShellConfiguration configuration, + IOptions shellOptions, + ShellSettings shellSettings, + ILogger logger) + { + _configuration = configuration; + _shellOptions = shellOptions.Value; + _shellSettings = shellSettings; + _logger = logger; + } + + public async Task ConfigureAsync(BlobOptions options) + { + _configuration.Bind("OrchardCore_DataProtection_Azure", options); + await ConfigureContainerNameAsync(options); + await ConfigureBlobNameAsync(options); + } + + private async Task ConfigureContainerNameAsync(BlobOptions options) + { + try + { + // Use Fluid directly as the service provider has not been built. + var templateOptions = new TemplateOptions(); + templateOptions.MemberAccessStrategy.Register(); + var templateContext = new TemplateContext(templateOptions); + templateContext.SetValue("ShellSettings", _shellSettings); + + var template = _fluidParser.Parse(options.ContainerName); + + // Container name must be lowercase. + var containerName = template.Render(templateContext, NullEncoder.Default).ToLower(); + options.ContainerName = containerName.Replace("\r", string.Empty).Replace("\n", string.Empty); + } + catch (Exception e) + { + _logger.LogCritical(e, "Unable to parse data protection connection string."); + throw; + } + + if (options.CreateContainer) + { + try + { + _logger.LogDebug("Testing data protection container {ContainerName} existence", options.ContainerName); + var blobContainer = new BlobContainerClient(options.ConnectionString, options.ContainerName); + var response = await blobContainer.CreateIfNotExistsAsync(PublicAccessType.None); + _logger.LogDebug("Data protection container {ContainerName} created.", options.ContainerName); + } + catch (Exception e) + { + _logger.LogCritical(e, "Unable to connect to Azure Storage to configure data protection storage. Ensure that an application setting containing a valid Azure Storage connection string is available at `Modules:OrchardCore.DataProtection.Azure:ConnectionString`."); + throw; + } + } + } + + private async Task ConfigureBlobNameAsync(BlobOptions options) + { + if (string.IsNullOrEmpty(options.BlobName)) + { + options.BlobName = $"{_shellOptions.ShellsContainerName}/{_shellSettings.Name}/DataProtectionKeys.xml"; + + return; + } + + try + { + // Use Fluid directly as the service provider has not been built. + var templateOptions = new TemplateOptions(); + var templateContext = new TemplateContext(templateOptions); + templateOptions.MemberAccessStrategy.Register(); + templateContext.SetValue("ShellSettings", _shellSettings); + + var template = _fluidParser.Parse(options.BlobName); + + var blobName = await template.RenderAsync(templateContext, NullEncoder.Default); + options.BlobName = blobName.Replace("\r", string.Empty).Replace("\n", string.Empty); + } + catch (Exception e) + { + _logger.LogCritical(e, "Unable to parse data protection blob name."); + throw; + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.DataProtection.Azure/Startup.cs b/src/OrchardCore.Modules/OrchardCore.DataProtection.Azure/Startup.cs index 957820a2d29..55eae14677c 100644 --- a/src/OrchardCore.Modules/OrchardCore.DataProtection.Azure/Startup.cs +++ b/src/OrchardCore.Modules/OrchardCore.DataProtection.Azure/Startup.cs @@ -1,139 +1,56 @@ -using System; -using System.Threading.Tasks; using Azure.Storage.Blobs; -using Azure.Storage.Blobs.Models; -using Fluid; using Microsoft.AspNetCore.DataProtection; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using OrchardCore.Environment.Shell; using OrchardCore.Environment.Shell.Configuration; using OrchardCore.Modules; -namespace OrchardCore.DataProtection.Azure +namespace OrchardCore.DataProtection.Azure; + +public class Startup : StartupBase { - public class Startup : StartupBase + private readonly IShellConfiguration _configuration; + private readonly ILogger _logger; + + public Startup(IShellConfiguration configuration, ILogger logger) { - private readonly ShellOptions _shellOptions; - private readonly ShellSettings _shellSettings; - private readonly ILogger _logger; + _configuration = configuration; + _logger = logger; + } - // Local instance since it can be discarded once the startup is over. - private readonly FluidParser _fluidParser = new(); + // Assume that this module will override default configuration, so set the Order to a value above the default. + public override int Order => 10; - public Startup( - IOptions shellOptions, - ShellSettings shellSettings, - ILogger logger) - { - _shellOptions = shellOptions.Value; - _shellSettings = shellSettings; - _logger = logger; - } + public override void ConfigureServices(IServiceCollection services) + { + var connectionString = _configuration.GetValue("OrchardCore_DataProtection_Azure:ConnectionString"); - public override void ConfigureServices(IServiceCollection services) + if (!string.IsNullOrWhiteSpace(connectionString)) { - services.Initialize(async sp => - { - var configuration = sp.GetRequiredService(); - - var connectionString = configuration.GetValue("OrchardCore_DataProtection_Azure:ConnectionString"); - - if (!string.IsNullOrWhiteSpace(connectionString)) + services + .AddSingleton(new BlobOptions()) + .AddTransient() + .AddDataProtection() + .PersistKeysToAzureBlobStorage(sp => { - var containerName = await GetBlobContainerNameAsync(configuration, connectionString); - - services.AddDataProtection() - .PersistKeysToAzureBlobStorage(connectionString, containerName, await GetBlobNameAsync(configuration)); - } - else - { - _logger.LogCritical("No connection string was supplied for OrchardCore.DataProtection.Azure. Ensure that an application setting containing a valid Azure Storage connection string is available at `Modules:OrchardCore.DataProtection.Azure:ConnectionString`."); - } - }); - } - - private async Task GetBlobContainerNameAsync(IShellConfiguration configuration, string connectionString) - { - var containerName = configuration.GetValue("OrchardCore_DataProtection_Azure:ContainerName", "dataprotection"); - - // Use Fluid directly as the service provider has not been built. - try - { - var templateOptions = new TemplateOptions(); - templateOptions.MemberAccessStrategy.Register(); - var templateContext = new TemplateContext(templateOptions); - templateContext.SetValue("ShellSettings", _shellSettings); - - var template = _fluidParser.Parse(containerName); + var options = sp.GetRequiredService(); + return new BlobClient( + options.ConnectionString, + options.ContainerName, + options.BlobName); + }); - // container name must be lowercase - containerName = template.Render(templateContext, NullEncoder.Default).ToLower(); - containerName = containerName.Replace("\r", string.Empty).Replace("\n", string.Empty); - } - catch (Exception e) - { - _logger.LogCritical(e, "Unable to parse data protection connection string."); - throw; - } - - var createContainer = configuration.GetValue("OrchardCore_DataProtection_Azure:CreateContainer", true); - if (createContainer) + services.Initialize(async sp => { - try - { - _logger.LogDebug("Testing data protection container {ContainerName} existence", containerName); - var _blobContainer = new BlobContainerClient(connectionString, containerName); - var response = await _blobContainer.CreateIfNotExistsAsync(PublicAccessType.None); - _logger.LogDebug("Data protection container {ContainerName} created.", containerName); - } - catch (Exception) - { - _logger.LogCritical("Unable to connect to Azure Storage to configure data protection storage. Ensure that an application setting containing a valid Azure Storage connection string is available at `Modules:OrchardCore.DataProtection.Azure:ConnectionString`."); - - throw; - } - } - - return containerName; + var options = sp.GetRequiredService(); + var setup = sp.GetRequiredService(); + await setup.ConfigureAsync(options); + }); } - - private async Task GetBlobNameAsync(IShellConfiguration configuration) + else { - var blobName = configuration.GetValue("OrchardCore_DataProtection_Azure:BlobName"); - - if (string.IsNullOrEmpty(blobName)) - { - blobName = $"{_shellOptions.ShellsContainerName}/{_shellSettings.Name}/DataProtectionKeys.xml"; - } - else - { - try - { - // Use Fluid directly as the service provider has not been built. - var templateOptions = new TemplateOptions(); - var templateContext = new TemplateContext(templateOptions); - templateOptions.MemberAccessStrategy.Register(); - templateContext.SetValue("ShellSettings", _shellSettings); - - var template = _fluidParser.Parse(blobName); - - blobName = await template.RenderAsync(templateContext, NullEncoder.Default); - blobName = blobName.Replace("\r", string.Empty).Replace("\n", string.Empty); - } - catch (Exception e) - { - _logger.LogCritical(e, "Unable to parse data protection blob name."); - throw; - } - } - - return blobName; + _logger.LogCritical("No connection string was supplied for OrchardCore.DataProtection.Azure. Ensure that an application setting containing a valid Azure Storage connection string is available at `Modules:OrchardCore.DataProtection.Azure:ConnectionString`."); } - - // Assume that this module will override default configuration, so set the Order to a value above the default. - public override int Order => 10; } }