diff --git a/framework/Volo.Abp.sln b/framework/Volo.Abp.sln
index 135afaf4d9..e02d40b1e0 100644
--- a/framework/Volo.Abp.sln
+++ b/framework/Volo.Abp.sln
@@ -301,6 +301,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.BlobStoring.FileSy
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.EntityFrameworkCore.Oracle.Devart", "src\Volo.Abp.EntityFrameworkCore.Oracle.Devart\Volo.Abp.EntityFrameworkCore.Oracle.Devart.csproj", "{75E5C841-5F36-4C44-A532-57CB8E7FFE15}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.BlobStoring.Azure", "src\Volo.Abp.BlobStoring.Azure\Volo.Abp.BlobStoring.Azure.csproj", "{C44242F7-D55D-4867-AAF4-A786E404312E}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.BlobStoring.Azure.Tests", "test\Volo.Abp.BlobStoring.Azure.Tests\Volo.Abp.BlobStoring.Azure.Tests.csproj", "{A80E9A0B-8932-4B5D-83FB-6751708FD484}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -895,6 +899,14 @@ Global
{75E5C841-5F36-4C44-A532-57CB8E7FFE15}.Debug|Any CPU.Build.0 = Debug|Any CPU
{75E5C841-5F36-4C44-A532-57CB8E7FFE15}.Release|Any CPU.ActiveCfg = Release|Any CPU
{75E5C841-5F36-4C44-A532-57CB8E7FFE15}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C44242F7-D55D-4867-AAF4-A786E404312E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C44242F7-D55D-4867-AAF4-A786E404312E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C44242F7-D55D-4867-AAF4-A786E404312E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C44242F7-D55D-4867-AAF4-A786E404312E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A80E9A0B-8932-4B5D-83FB-6751708FD484}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A80E9A0B-8932-4B5D-83FB-6751708FD484}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A80E9A0B-8932-4B5D-83FB-6751708FD484}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A80E9A0B-8932-4B5D-83FB-6751708FD484}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -1047,6 +1059,8 @@ Global
{02B1FBE2-850E-4612-ABC6-DD62BCF2DD6B} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
{68443D4A-1608-4039-B995-7AF4CF82E9F8} = {447C8A77-E5F0-4538-8687-7383196D04EA}
{75E5C841-5F36-4C44-A532-57CB8E7FFE15} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
+ {C44242F7-D55D-4867-AAF4-A786E404312E} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
+ {A80E9A0B-8932-4B5D-83FB-6751708FD484} = {447C8A77-E5F0-4538-8687-7383196D04EA}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {BB97ECF4-9A84-433F-A80B-2A3285BDD1D5}
diff --git a/framework/src/Volo.Abp.BlobStoring.Azure/FodyWeavers.xml b/framework/src/Volo.Abp.BlobStoring.Azure/FodyWeavers.xml
new file mode 100644
index 0000000000..bc5a74a236
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.Azure/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/framework/src/Volo.Abp.BlobStoring.Azure/FodyWeavers.xsd b/framework/src/Volo.Abp.BlobStoring.Azure/FodyWeavers.xsd
new file mode 100644
index 0000000000..3f3946e282
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.Azure/FodyWeavers.xsd
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.
+
+
+
+
+ A comma-separated list of error codes that can be safely ignored in assembly verification.
+
+
+
+
+ 'false' to turn off automatic generation of the XML Schema file.
+
+
+
+
+
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.BlobStoring.Azure/Volo.Abp.BlobStoring.Azure.csproj b/framework/src/Volo.Abp.BlobStoring.Azure/Volo.Abp.BlobStoring.Azure.csproj
new file mode 100644
index 0000000000..1e7c51f4fc
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.Azure/Volo.Abp.BlobStoring.Azure.csproj
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ netstandard2.0
+ Volo.Abp.BlobStoring.Azure
+ Volo.Abp.BlobStoring.Azure
+ $(AssetTargetFallback);portable-net45+win8+wp8+wpa81;
+ false
+ false
+ false
+
+
+
+
+
+
+
+
+
diff --git a/framework/src/Volo.Abp.BlobStoring.Azure/Volo/Abp/BlobStoring/Azure/AbpBlobStoringAzureModule.cs b/framework/src/Volo.Abp.BlobStoring.Azure/Volo/Abp/BlobStoring/Azure/AbpBlobStoringAzureModule.cs
new file mode 100644
index 0000000000..0b386fbec1
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.Azure/Volo/Abp/BlobStoring/Azure/AbpBlobStoringAzureModule.cs
@@ -0,0 +1,10 @@
+using Volo.Abp.Modularity;
+
+namespace Volo.Abp.BlobStoring.Azure
+{
+ [DependsOn(typeof(AbpBlobStoringModule))]
+ public class AbpBlobStoringAzureModule : AbpModule
+ {
+
+ }
+}
diff --git a/framework/src/Volo.Abp.BlobStoring.Azure/Volo/Abp/BlobStoring/Azure/AzureBlobContainerConfigurationExtensions.cs b/framework/src/Volo.Abp.BlobStoring.Azure/Volo/Abp/BlobStoring/Azure/AzureBlobContainerConfigurationExtensions.cs
new file mode 100644
index 0000000000..945c930175
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.Azure/Volo/Abp/BlobStoring/Azure/AzureBlobContainerConfigurationExtensions.cs
@@ -0,0 +1,24 @@
+using System;
+
+namespace Volo.Abp.BlobStoring.Azure
+{
+ public static class AzureBlobContainerConfigurationExtensions
+ {
+ public static AzureBlobProviderConfiguration GetAzureConfiguration(
+ this BlobContainerConfiguration containerConfiguration)
+ {
+ return new AzureBlobProviderConfiguration(containerConfiguration);
+ }
+
+ public static BlobContainerConfiguration UseAzure(
+ this BlobContainerConfiguration containerConfiguration,
+ Action azureConfigureAction)
+ {
+ containerConfiguration.ProviderType = typeof(AzureBlobProvider);
+
+ azureConfigureAction(new AzureBlobProviderConfiguration(containerConfiguration));
+
+ return containerConfiguration;
+ }
+ }
+}
diff --git a/framework/src/Volo.Abp.BlobStoring.Azure/Volo/Abp/BlobStoring/Azure/AzureBlobProvider.cs b/framework/src/Volo.Abp.BlobStoring.Azure/Volo/Abp/BlobStoring/Azure/AzureBlobProvider.cs
new file mode 100644
index 0000000000..1afc3b6a53
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.Azure/Volo/Abp/BlobStoring/Azure/AzureBlobProvider.cs
@@ -0,0 +1,110 @@
+using System;
+using System.IO;
+using System.Threading.Tasks;
+using Azure.Storage.Blobs;
+using Volo.Abp.DependencyInjection;
+
+namespace Volo.Abp.BlobStoring.Azure
+{
+ public class AzureBlobProvider : BlobProviderBase, ITransientDependency
+ {
+ protected IAzureBlobNameCalculator AzureBlobNameCalculator { get; }
+
+ public AzureBlobProvider(IAzureBlobNameCalculator azureBlobNameCalculator)
+ {
+ AzureBlobNameCalculator = azureBlobNameCalculator;
+ }
+
+ public override async Task SaveAsync(BlobProviderSaveArgs args)
+ {
+ var blobName = AzureBlobNameCalculator.Calculate(args);
+ var configuration = args.Configuration.GetAzureConfiguration();
+
+ if (!args.OverrideExisting && await BlobExistsAsync(args, blobName))
+ {
+ throw new BlobAlreadyExistsException($"Saving BLOB '{args.BlobName}' does already exists in the container '{GetContainerName(args)}'! Set {nameof(args.OverrideExisting)} if it should be overwritten.");
+ }
+
+ if (configuration.CreateContainerIfNotExists)
+ {
+ await CreateContainerIfNotExists(args);
+ }
+
+ await GetBlobClient(args, blobName).UploadAsync(args.BlobStream, true);
+ }
+
+ public override async Task DeleteAsync(BlobProviderDeleteArgs args)
+ {
+ var blobName = AzureBlobNameCalculator.Calculate(args);
+
+ if (await BlobExistsAsync(args, blobName))
+ {
+ return await GetBlobClient(args, blobName).DeleteIfExistsAsync();
+ }
+
+ return false;
+ }
+
+ public override async Task ExistsAsync(BlobProviderExistsArgs args)
+ {
+ var blobName = AzureBlobNameCalculator.Calculate(args);
+
+ return await BlobExistsAsync(args, blobName);
+ }
+
+ public override async Task GetOrNullAsync(BlobProviderGetArgs args)
+ {
+ var blobName = AzureBlobNameCalculator.Calculate(args);
+
+ if (!await BlobExistsAsync(args, blobName))
+ {
+ return null;
+ }
+
+ var blobClient = GetBlobClient(args, blobName);
+ var download = await blobClient.DownloadAsync();
+ var memoryStream = new MemoryStream();
+ await download.Value.Content.CopyToAsync(memoryStream);
+ return memoryStream;
+ }
+
+ protected virtual BlobClient GetBlobClient(BlobProviderArgs args, string blobName)
+ {
+ var blobContainerClient = GetBlobContainerClient(args);
+ return blobContainerClient.GetBlobClient(blobName);
+ }
+
+ protected virtual BlobContainerClient GetBlobContainerClient(BlobProviderArgs args)
+ {
+ var configuration = args.Configuration.GetAzureConfiguration();
+ var blobServiceClient = new BlobServiceClient(configuration.ConnectionString);
+ return blobServiceClient.GetBlobContainerClient(GetContainerName(args));
+ }
+
+ protected virtual async Task CreateContainerIfNotExists(BlobProviderArgs args)
+ {
+ var blobContainerClient = GetBlobContainerClient(args);
+ await blobContainerClient.CreateIfNotExistsAsync();
+ }
+
+ private async Task BlobExistsAsync(BlobProviderArgs args, string blobName)
+ {
+ // Make sure Blob Container exists.
+ return await ContainerExistsAsync(GetBlobContainerClient(args)) &&
+ (await GetBlobClient(args, blobName).ExistsAsync()).Value;
+ }
+
+ private static string GetContainerName(BlobProviderArgs args)
+ {
+ var configuration = args.Configuration.GetAzureConfiguration();
+ return configuration.ContainerName.IsNullOrWhiteSpace()
+ ? args.ContainerName
+ : configuration.ContainerName;
+ }
+
+ private static async Task ContainerExistsAsync(BlobContainerClient blobContainerClient)
+ {
+ return (await blobContainerClient.ExistsAsync()).Value;
+ }
+ }
+}
diff --git a/framework/src/Volo.Abp.BlobStoring.Azure/Volo/Abp/BlobStoring/Azure/AzureBlobProviderConfiguration.cs b/framework/src/Volo.Abp.BlobStoring.Azure/Volo/Abp/BlobStoring/Azure/AzureBlobProviderConfiguration.cs
new file mode 100644
index 0000000000..7ab338794b
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.Azure/Volo/Abp/BlobStoring/Azure/AzureBlobProviderConfiguration.cs
@@ -0,0 +1,39 @@
+namespace Volo.Abp.BlobStoring.Azure
+{
+ public class AzureBlobProviderConfiguration
+ {
+ public string ConnectionString
+ {
+ get => _containerConfiguration.GetConfiguration(AzureBlobProviderConfigurationNames.ConnectionString);
+ set => _containerConfiguration.SetConfiguration(AzureBlobProviderConfigurationNames.ConnectionString, Check.NotNullOrWhiteSpace(value, nameof(value)));
+ }
+
+ ///
+ /// This name may only contain lowercase letters, numbers, and hyphens, and must begin with a letter or a number.
+ /// Each hyphen must be preceded and followed by a non-hyphen character.
+ /// The name must also be between 3 and 63 characters long.
+ /// If this parameter is not specified, the ContainerName of the will be used.
+ ///
+ public string ContainerName
+ {
+ get => _containerConfiguration.GetConfiguration(AzureBlobProviderConfigurationNames.ContainerName);
+ set => _containerConfiguration.SetConfiguration(AzureBlobProviderConfigurationNames.ContainerName, Check.NotNullOrWhiteSpace(value, nameof(value)));
+ }
+
+ ///
+ /// Default value: false.
+ ///
+ public bool CreateContainerIfNotExists
+ {
+ get => _containerConfiguration.GetConfigurationOrDefault(AzureBlobProviderConfigurationNames.CreateContainerIfNotExists, false);
+ set => _containerConfiguration.SetConfiguration(AzureBlobProviderConfigurationNames.CreateContainerIfNotExists, value);
+ }
+
+ private readonly BlobContainerConfiguration _containerConfiguration;
+
+ public AzureBlobProviderConfiguration(BlobContainerConfiguration containerConfiguration)
+ {
+ _containerConfiguration = containerConfiguration;
+ }
+ }
+}
diff --git a/framework/src/Volo.Abp.BlobStoring.Azure/Volo/Abp/BlobStoring/Azure/AzureBlobProviderConfigurationNames.cs b/framework/src/Volo.Abp.BlobStoring.Azure/Volo/Abp/BlobStoring/Azure/AzureBlobProviderConfigurationNames.cs
new file mode 100644
index 0000000000..b8fbff19d2
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.Azure/Volo/Abp/BlobStoring/Azure/AzureBlobProviderConfigurationNames.cs
@@ -0,0 +1,9 @@
+namespace Volo.Abp.BlobStoring.Azure
+{
+ public static class AzureBlobProviderConfigurationNames
+ {
+ public const string ConnectionString = "Azure.ConnectionString";
+ public const string ContainerName = "Azure.ContainerName";
+ public const string CreateContainerIfNotExists = "Azure.CreateContainerIfNotExists";
+ }
+}
diff --git a/framework/src/Volo.Abp.BlobStoring.Azure/Volo/Abp/BlobStoring/Azure/DefaultAzureBlobNameCalculator.cs b/framework/src/Volo.Abp.BlobStoring.Azure/Volo/Abp/BlobStoring/Azure/DefaultAzureBlobNameCalculator.cs
new file mode 100644
index 0000000000..0c6cd481c9
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.Azure/Volo/Abp/BlobStoring/Azure/DefaultAzureBlobNameCalculator.cs
@@ -0,0 +1,22 @@
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.MultiTenancy;
+
+namespace Volo.Abp.BlobStoring.Azure
+{
+ public class DefaultAzureBlobNameCalculator : IAzureBlobNameCalculator, ITransientDependency
+ {
+ protected ICurrentTenant CurrentTenant { get; }
+
+ public DefaultAzureBlobNameCalculator(ICurrentTenant currentTenant)
+ {
+ CurrentTenant = currentTenant;
+ }
+
+ public virtual string Calculate(BlobProviderArgs args)
+ {
+ return CurrentTenant.Id == null
+ ? $"host/{args.BlobName}"
+ : $"tenants/{CurrentTenant.Id.Value.ToString("D")}/{args.BlobName}";
+ }
+ }
+}
diff --git a/framework/src/Volo.Abp.BlobStoring.Azure/Volo/Abp/BlobStoring/Azure/IAzureBlobNameCalculator.cs b/framework/src/Volo.Abp.BlobStoring.Azure/Volo/Abp/BlobStoring/Azure/IAzureBlobNameCalculator.cs
new file mode 100644
index 0000000000..5985a9278d
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.Azure/Volo/Abp/BlobStoring/Azure/IAzureBlobNameCalculator.cs
@@ -0,0 +1,7 @@
+namespace Volo.Abp.BlobStoring.Azure
+{
+ public interface IAzureBlobNameCalculator
+ {
+ string Calculate(BlobProviderArgs args);
+ }
+}
diff --git a/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobProviderArgs.cs b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobProviderArgs.cs
index fcc264977a..0170409a16 100644
--- a/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobProviderArgs.cs
+++ b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobProviderArgs.cs
@@ -28,4 +28,4 @@ namespace Volo.Abp.BlobStoring
CancellationToken = cancellationToken;
}
}
-}
\ No newline at end of file
+}
diff --git a/framework/test/Volo.Abp.BlobStoring.Azure.Tests/Volo.Abp.BlobStoring.Azure.Tests.csproj b/framework/test/Volo.Abp.BlobStoring.Azure.Tests/Volo.Abp.BlobStoring.Azure.Tests.csproj
new file mode 100644
index 0000000000..ff97ea97d3
--- /dev/null
+++ b/framework/test/Volo.Abp.BlobStoring.Azure.Tests/Volo.Abp.BlobStoring.Azure.Tests.csproj
@@ -0,0 +1,19 @@
+
+
+
+
+
+ netcoreapp3.1
+
+ 9f0d2c00-80c1-435b-bfab-2c39c8249091
+
+
+
+
+
+
+
+
+
+
+
diff --git a/framework/test/Volo.Abp.BlobStoring.Azure.Tests/Volo/Abp/BlobStoring/Azure/AbpBlobStoringAzureTestBase.cs b/framework/test/Volo.Abp.BlobStoring.Azure.Tests/Volo/Abp/BlobStoring/Azure/AbpBlobStoringAzureTestBase.cs
new file mode 100644
index 0000000000..8941234cbf
--- /dev/null
+++ b/framework/test/Volo.Abp.BlobStoring.Azure.Tests/Volo/Abp/BlobStoring/Azure/AbpBlobStoringAzureTestBase.cs
@@ -0,0 +1,20 @@
+using Volo.Abp.Testing;
+
+namespace Volo.Abp.BlobStoring.Azure
+{
+ public class AbpBlobStoringAzureTestCommonBase : AbpIntegratedTest
+ {
+ protected override void SetAbpApplicationCreationOptions(AbpApplicationCreationOptions options)
+ {
+ options.UseAutofac();
+ }
+ }
+
+ public class AbpBlobStoringAzureTestBase : AbpIntegratedTest
+ {
+ protected override void SetAbpApplicationCreationOptions(AbpApplicationCreationOptions options)
+ {
+ options.UseAutofac();
+ }
+ }
+}
diff --git a/framework/test/Volo.Abp.BlobStoring.Azure.Tests/Volo/Abp/BlobStoring/Azure/AbpBlobStoringAzureTestModule.cs b/framework/test/Volo.Abp.BlobStoring.Azure.Tests/Volo/Abp/BlobStoring/Azure/AbpBlobStoringAzureTestModule.cs
new file mode 100644
index 0000000000..5e66be4e19
--- /dev/null
+++ b/framework/test/Volo.Abp.BlobStoring.Azure.Tests/Volo/Abp/BlobStoring/Azure/AbpBlobStoringAzureTestModule.cs
@@ -0,0 +1,64 @@
+using System;
+using Azure.Storage.Blobs;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Volo.Abp.Modularity;
+
+namespace Volo.Abp.BlobStoring.Azure
+{
+
+ ///
+ /// This module will not try to connect to azure.
+ ///
+ [DependsOn(
+ typeof(AbpBlobStoringAzureModule),
+ typeof(AbpBlobStoringTestModule)
+ )]
+ public class AbpBlobStoringAzureTestCommonModule : AbpModule
+ {
+
+ }
+
+ [DependsOn(
+ typeof(AbpBlobStoringAzureTestCommonModule)
+ )]
+ public class AbpBlobStoringAzureTestModule : AbpModule
+ {
+ private const string UserSecretsId = "9f0d2c00-80c1-435b-bfab-2c39c8249091";
+
+ private string _connectionString;
+
+ private readonly string _randomContainerName = "abp-azure-test-container-" + Guid.NewGuid().ToString("N");
+
+ public override void ConfigureServices(ServiceConfigurationContext context)
+ {
+ context.Services.ReplaceConfiguration(ConfigurationHelper.BuildConfiguration(builderAction: builder =>
+ {
+ builder.AddUserSecrets(UserSecretsId);
+ }));
+
+ var configuration = context.Services.GetConfiguration();
+ _connectionString = configuration["Azure:ConnectionString"];
+
+ Configure(options =>
+ {
+ options.Containers.ConfigureAll((containerName, containerConfiguration) =>
+ {
+ containerConfiguration.UseAzure(azure =>
+ {
+ azure.ConnectionString = _connectionString;
+ azure.ContainerName = _randomContainerName;
+ azure.CreateContainerIfNotExists = true;
+ });
+ });
+ });
+ }
+
+ public override void OnApplicationShutdown(ApplicationShutdownContext context)
+ {
+ var blobServiceClient = new BlobServiceClient(_connectionString);
+ blobServiceClient.GetBlobContainerClient(_randomContainerName).DeleteIfExists();
+ }
+ }
+
+}
diff --git a/framework/test/Volo.Abp.BlobStoring.Azure.Tests/Volo/Abp/BlobStoring/Azure/AzureBlobContainer_Tests.cs b/framework/test/Volo.Abp.BlobStoring.Azure.Tests/Volo/Abp/BlobStoring/Azure/AzureBlobContainer_Tests.cs
new file mode 100644
index 0000000000..751918be85
--- /dev/null
+++ b/framework/test/Volo.Abp.BlobStoring.Azure.Tests/Volo/Abp/BlobStoring/Azure/AzureBlobContainer_Tests.cs
@@ -0,0 +1,16 @@
+using Xunit;
+
+namespace Volo.Abp.BlobStoring.Azure
+{
+ /*
+ //Please set the correct connection string in secrets.json and continue the test.
+
+ public class AzureBlobContainer_Tests : BlobContainer_Tests
+ {
+ public AzureBlobContainer_Tests()
+ {
+
+ }
+ }
+ */
+}
diff --git a/framework/test/Volo.Abp.BlobStoring.Azure.Tests/Volo/Abp/BlobStoring/Azure/AzureBlobNameCalculator_Tests.cs b/framework/test/Volo.Abp.BlobStoring.Azure.Tests/Volo/Abp/BlobStoring/Azure/AzureBlobNameCalculator_Tests.cs
new file mode 100644
index 0000000000..4540d208ae
--- /dev/null
+++ b/framework/test/Volo.Abp.BlobStoring.Azure.Tests/Volo/Abp/BlobStoring/Azure/AzureBlobNameCalculator_Tests.cs
@@ -0,0 +1,57 @@
+using System;
+using Shouldly;
+using Volo.Abp.MultiTenancy;
+using Xunit;
+
+namespace Volo.Abp.BlobStoring.Azure
+{
+ public class AzureBlobNameCalculator_Tests : AbpBlobStoringAzureTestCommonBase
+ {
+ private readonly IAzureBlobNameCalculator _calculator;
+ private readonly ICurrentTenant _currentTenant;
+
+ private const string AzureContainerName = "/";
+ private const string AzureSeparator = "/";
+
+ public AzureBlobNameCalculator_Tests()
+ {
+ _calculator = GetRequiredService();
+ _currentTenant = GetRequiredService();
+ }
+
+ [Fact]
+ public void Default_Settings()
+ {
+ _calculator.Calculate(
+ GetArgs("my-container", "my-blob")
+ ).ShouldBe($"host{AzureSeparator}my-blob");
+ }
+
+ [Fact]
+ public void Default_Settings_With_TenantId()
+ {
+ var tenantId = Guid.NewGuid();
+
+ using (_currentTenant.Change(tenantId))
+ {
+ _calculator.Calculate(
+ GetArgs("my-container", "my-blob")
+ ).ShouldBe($"tenants{AzureSeparator}{tenantId:D}{AzureSeparator}my-blob");
+ }
+ }
+
+ private static BlobProviderArgs GetArgs(
+ string containerName,
+ string blobName)
+ {
+ return new BlobProviderGetArgs(
+ containerName,
+ new BlobContainerConfiguration().UseAzure(x =>
+ {
+ x.ContainerName = containerName;
+ }),
+ blobName
+ );
+ }
+ }
+}