diff --git a/framework/Volo.Abp.sln b/framework/Volo.Abp.sln
index 8e8dac2ce5..165e62334c 100644
--- a/framework/Volo.Abp.sln
+++ b/framework/Volo.Abp.sln
@@ -291,6 +291,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.AspNetCore.SignalR
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Tests", "test\Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Tests\Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Tests.csproj", "{79323211-E658-493E-9863-035AA4C3F913}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.BlobStoring", "src\Volo.Abp.BlobStoring\Volo.Abp.BlobStoring.csproj", "{A0CFBDD6-A3CB-438C-83F1-5025F12E2D42}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.BlobStoring.Tests", "test\Volo.Abp.BlobStoring.Tests\Volo.Abp.BlobStoring.Tests.csproj", "{D53A17BB-4E23-451D-AD9B-E1F6AC3F7958}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.BlobStoring.FileSystem", "src\Volo.Abp.BlobStoring.FileSystem\Volo.Abp.BlobStoring.FileSystem.csproj", "{02B1FBE2-850E-4612-ABC6-DD62BCF2DD6B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.BlobStoring.FileSystem.Tests", "test\Volo.Abp.BlobStoring.FileSystem.Tests\Volo.Abp.BlobStoring.FileSystem.Tests.csproj", "{68443D4A-1608-4039-B995-7AF4CF82E9F8}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -865,6 +873,22 @@ Global
{79323211-E658-493E-9863-035AA4C3F913}.Debug|Any CPU.Build.0 = Debug|Any CPU
{79323211-E658-493E-9863-035AA4C3F913}.Release|Any CPU.ActiveCfg = Release|Any CPU
{79323211-E658-493E-9863-035AA4C3F913}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A0CFBDD6-A3CB-438C-83F1-5025F12E2D42}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A0CFBDD6-A3CB-438C-83F1-5025F12E2D42}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A0CFBDD6-A3CB-438C-83F1-5025F12E2D42}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A0CFBDD6-A3CB-438C-83F1-5025F12E2D42}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D53A17BB-4E23-451D-AD9B-E1F6AC3F7958}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D53A17BB-4E23-451D-AD9B-E1F6AC3F7958}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D53A17BB-4E23-451D-AD9B-E1F6AC3F7958}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D53A17BB-4E23-451D-AD9B-E1F6AC3F7958}.Release|Any CPU.Build.0 = Release|Any CPU
+ {02B1FBE2-850E-4612-ABC6-DD62BCF2DD6B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {02B1FBE2-850E-4612-ABC6-DD62BCF2DD6B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {02B1FBE2-850E-4612-ABC6-DD62BCF2DD6B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {02B1FBE2-850E-4612-ABC6-DD62BCF2DD6B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {68443D4A-1608-4039-B995-7AF4CF82E9F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {68443D4A-1608-4039-B995-7AF4CF82E9F8}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {68443D4A-1608-4039-B995-7AF4CF82E9F8}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {68443D4A-1608-4039-B995-7AF4CF82E9F8}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -1012,6 +1036,10 @@ Global
{B64FCE08-E9D2-4984-BF12-FE199F257416} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
{8B758716-DCC9-4223-8421-5588D1597487} = {447C8A77-E5F0-4538-8687-7383196D04EA}
{79323211-E658-493E-9863-035AA4C3F913} = {447C8A77-E5F0-4538-8687-7383196D04EA}
+ {A0CFBDD6-A3CB-438C-83F1-5025F12E2D42} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
+ {D53A17BB-4E23-451D-AD9B-E1F6AC3F7958} = {447C8A77-E5F0-4538-8687-7383196D04EA}
+ {02B1FBE2-850E-4612-ABC6-DD62BCF2DD6B} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
+ {68443D4A-1608-4039-B995-7AF4CF82E9F8} = {447C8A77-E5F0-4538-8687-7383196D04EA}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {BB97ECF4-9A84-433F-A80B-2A3285BDD1D5}
diff --git a/framework/src/Volo.Abp.BlobStoring.FileSystem/FodyWeavers.xml b/framework/src/Volo.Abp.BlobStoring.FileSystem/FodyWeavers.xml
new file mode 100644
index 0000000000..be0de3a908
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.FileSystem/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.BlobStoring.FileSystem/FodyWeavers.xsd b/framework/src/Volo.Abp.BlobStoring.FileSystem/FodyWeavers.xsd
new file mode 100644
index 0000000000..3f3946e282
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.FileSystem/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.FileSystem/Volo.Abp.BlobStoring.FileSystem.csproj b/framework/src/Volo.Abp.BlobStoring.FileSystem/Volo.Abp.BlobStoring.FileSystem.csproj
new file mode 100644
index 0000000000..03fcbbd374
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.FileSystem/Volo.Abp.BlobStoring.FileSystem.csproj
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+ netstandard2.0
+ Volo.Abp.BlobStoring.FileSystem
+ Volo.Abp.BlobStoring.FileSystem
+ $(AssetTargetFallback);portable-net45+win8+wp8+wpa81;
+ false
+ false
+ false
+
+
+
+
+
+
+
+
diff --git a/framework/src/Volo.Abp.BlobStoring.FileSystem/Volo/Abp/BlobStoring/FileSystem/AbpBlobStoringFileSystemModule.cs b/framework/src/Volo.Abp.BlobStoring.FileSystem/Volo/Abp/BlobStoring/FileSystem/AbpBlobStoringFileSystemModule.cs
new file mode 100644
index 0000000000..249bbfaa78
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.FileSystem/Volo/Abp/BlobStoring/FileSystem/AbpBlobStoringFileSystemModule.cs
@@ -0,0 +1,13 @@
+using System;
+using Volo.Abp.Modularity;
+
+namespace Volo.Abp.BlobStoring.FileSystem
+{
+ [DependsOn(
+ typeof(AbpBlobStoringModule)
+ )]
+ public class AbpBlobStoringFileSystemModule : AbpModule
+ {
+
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.BlobStoring.FileSystem/Volo/Abp/BlobStoring/FileSystem/DefaultBlogFilePathCalculator.cs b/framework/src/Volo.Abp.BlobStoring.FileSystem/Volo/Abp/BlobStoring/FileSystem/DefaultBlogFilePathCalculator.cs
new file mode 100644
index 0000000000..62c2585375
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.FileSystem/Volo/Abp/BlobStoring/FileSystem/DefaultBlogFilePathCalculator.cs
@@ -0,0 +1,32 @@
+using System.IO;
+using Volo.Abp.DependencyInjection;
+
+namespace Volo.Abp.BlobStoring.FileSystem
+{
+ public class DefaultBlogFilePathCalculator : IBlogFilePathCalculator, ITransientDependency
+ {
+ public virtual string Calculate(BlobProviderArgs args)
+ {
+ var fileSystemConfiguration = args.Configuration.GetFileSystemConfiguration();
+ var blobPath = fileSystemConfiguration.BasePath;
+
+ if (args.TenantId == null)
+ {
+ blobPath = Path.Combine(blobPath, "host");
+ }
+ else
+ {
+ blobPath = Path.Combine(blobPath, "tenants", args.TenantId.Value.ToString("D"));
+ }
+
+ if (fileSystemConfiguration.AppendContainerNameToBasePath)
+ {
+ blobPath = Path.Combine(blobPath, args.ContainerName);
+ }
+
+ blobPath = Path.Combine(blobPath, args.BlobName);
+
+ return blobPath;
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.BlobStoring.FileSystem/Volo/Abp/BlobStoring/FileSystem/FileSystemBlobContainerConfigurationExtensions.cs b/framework/src/Volo.Abp.BlobStoring.FileSystem/Volo/Abp/BlobStoring/FileSystem/FileSystemBlobContainerConfigurationExtensions.cs
new file mode 100644
index 0000000000..24357554eb
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.FileSystem/Volo/Abp/BlobStoring/FileSystem/FileSystemBlobContainerConfigurationExtensions.cs
@@ -0,0 +1,24 @@
+using System;
+
+namespace Volo.Abp.BlobStoring.FileSystem
+{
+ public static class FileSystemBlobContainerConfigurationExtensions
+ {
+ public static FileSystemBlobProviderConfiguration GetFileSystemConfiguration(
+ this BlobContainerConfiguration containerConfiguration)
+ {
+ return new FileSystemBlobProviderConfiguration(containerConfiguration);
+ }
+
+ public static BlobContainerConfiguration UseFileSystem(
+ this BlobContainerConfiguration containerConfiguration,
+ Action fileSystemConfigureAction)
+ {
+ containerConfiguration.ProviderType = typeof(FileSystemBlobProvider);
+
+ fileSystemConfigureAction(new FileSystemBlobProviderConfiguration(containerConfiguration));
+
+ return containerConfiguration;
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.BlobStoring.FileSystem/Volo/Abp/BlobStoring/FileSystem/FileSystemBlobProvider.cs b/framework/src/Volo.Abp.BlobStoring.FileSystem/Volo/Abp/BlobStoring/FileSystem/FileSystemBlobProvider.cs
new file mode 100644
index 0000000000..b927bdbf47
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.FileSystem/Volo/Abp/BlobStoring/FileSystem/FileSystemBlobProvider.cs
@@ -0,0 +1,69 @@
+using System.IO;
+using System.Threading.Tasks;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.IO;
+
+namespace Volo.Abp.BlobStoring.FileSystem
+{
+ //TODO: What if the file is being used on create, delete or read?
+
+ public class FileSystemBlobProvider : BlobProviderBase, ITransientDependency
+ {
+ protected IBlogFilePathCalculator FilePathCalculator { get; }
+
+ public FileSystemBlobProvider(IBlogFilePathCalculator filePathCalculator)
+ {
+ FilePathCalculator = filePathCalculator;
+ }
+
+ public override async Task SaveAsync(BlobProviderSaveArgs args)
+ {
+ var filePath = FilePathCalculator.Calculate(args);
+
+ DirectoryHelper.CreateIfNotExists(Path.GetDirectoryName(filePath));
+
+ var fileMode = args.OverrideExisting
+ ? FileMode.Create
+ : FileMode.CreateNew;
+
+ using (var fileStream = File.Open(filePath, fileMode, FileAccess.Write))
+ {
+ //TODO: Truely implement this (like this? http://writeasync.net/?p=2621 or https://www.infoworld.com/article/2995387/how-to-perform-asynchronous-file-operations-in-c.html)
+
+ await args.BlobStream.CopyToAsync(
+ fileStream,
+ 81920, //this is already the default value, but needed to set to be able to pass the cancellationToken
+ args.CancellationToken
+ );
+
+ await fileStream.FlushAsync();
+ }
+ }
+
+ public override Task DeleteAsync(BlobProviderDeleteArgs args)
+ {
+ var filePath = FilePathCalculator.Calculate(args);
+
+ return Task.FromResult(FileHelper.DeleteIfExists(filePath));
+ }
+
+ public override Task ExistsAsync(BlobProviderExistsArgs args)
+ {
+ var filePath = FilePathCalculator.Calculate(args);
+
+ return Task.FromResult(File.Exists(filePath));
+ }
+
+ public override Task GetOrNullAsync(BlobProviderGetArgs args)
+ {
+ var filePath = FilePathCalculator.Calculate(args);
+
+ if (!File.Exists(filePath))
+ {
+ return Task.FromResult(null);
+ }
+
+ return Task.FromResult(File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.Read));
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.BlobStoring.FileSystem/Volo/Abp/BlobStoring/FileSystem/FileSystemBlobProviderConfiguration.cs b/framework/src/Volo.Abp.BlobStoring.FileSystem/Volo/Abp/BlobStoring/FileSystem/FileSystemBlobProviderConfiguration.cs
new file mode 100644
index 0000000000..4d4a786d16
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.FileSystem/Volo/Abp/BlobStoring/FileSystem/FileSystemBlobProviderConfiguration.cs
@@ -0,0 +1,28 @@
+namespace Volo.Abp.BlobStoring.FileSystem
+{
+ public class FileSystemBlobProviderConfiguration
+ {
+
+ public string BasePath
+ {
+ get => _containerConfiguration.GetConfiguration(FileSystemBlobProviderConfigurationNames.BasePath);
+ set => _containerConfiguration.SetConfiguration(FileSystemBlobProviderConfigurationNames.BasePath, Check.NotNullOrWhiteSpace(value, nameof(value)));
+ }
+
+ ///
+ /// Default value: true.
+ ///
+ public bool AppendContainerNameToBasePath
+ {
+ get => _containerConfiguration.GetConfigurationOrDefault(FileSystemBlobProviderConfigurationNames.AppendContainerNameToBasePath, true);
+ set => _containerConfiguration.SetConfiguration(FileSystemBlobProviderConfigurationNames.AppendContainerNameToBasePath, value);
+ }
+
+ private readonly BlobContainerConfiguration _containerConfiguration;
+
+ public FileSystemBlobProviderConfiguration(BlobContainerConfiguration containerConfiguration)
+ {
+ _containerConfiguration = containerConfiguration;
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.BlobStoring.FileSystem/Volo/Abp/BlobStoring/FileSystem/FileSystemBlobProviderConfigurationNames.cs b/framework/src/Volo.Abp.BlobStoring.FileSystem/Volo/Abp/BlobStoring/FileSystem/FileSystemBlobProviderConfigurationNames.cs
new file mode 100644
index 0000000000..56991fa0ea
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.FileSystem/Volo/Abp/BlobStoring/FileSystem/FileSystemBlobProviderConfigurationNames.cs
@@ -0,0 +1,8 @@
+namespace Volo.Abp.BlobStoring.FileSystem
+{
+ public static class FileSystemBlobProviderConfigurationNames
+ {
+ public const string BasePath = "FileSystem.BasePath";
+ public const string AppendContainerNameToBasePath = "FileSystem.AppendContainerNameToBasePath";
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.BlobStoring.FileSystem/Volo/Abp/BlobStoring/FileSystem/IBlogFilePathCalculator.cs b/framework/src/Volo.Abp.BlobStoring.FileSystem/Volo/Abp/BlobStoring/FileSystem/IBlogFilePathCalculator.cs
new file mode 100644
index 0000000000..9e4ec4713a
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.FileSystem/Volo/Abp/BlobStoring/FileSystem/IBlogFilePathCalculator.cs
@@ -0,0 +1,7 @@
+namespace Volo.Abp.BlobStoring.FileSystem
+{
+ public interface IBlogFilePathCalculator
+ {
+ string Calculate(BlobProviderArgs args);
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.BlobStoring/FodyWeavers.xml b/framework/src/Volo.Abp.BlobStoring/FodyWeavers.xml
new file mode 100644
index 0000000000..be0de3a908
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.BlobStoring/FodyWeavers.xsd b/framework/src/Volo.Abp.BlobStoring/FodyWeavers.xsd
new file mode 100644
index 0000000000..3f3946e282
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring/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/Volo.Abp.BlobStoring.csproj b/framework/src/Volo.Abp.BlobStoring/Volo.Abp.BlobStoring.csproj
new file mode 100644
index 0000000000..850f98b5f1
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring/Volo.Abp.BlobStoring.csproj
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+ netstandard2.0
+ Volo.Abp.BlobStoring
+ Volo.Abp.BlobStoring
+ $(AssetTargetFallback);portable-net45+win8+wp8+wpa81;
+ false
+ false
+ false
+
+
+
+
+
+
+
+
+
+
diff --git a/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/AbpBlobStoringModule.cs b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/AbpBlobStoringModule.cs
new file mode 100644
index 0000000000..35970ea169
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/AbpBlobStoringModule.cs
@@ -0,0 +1,28 @@
+using Microsoft.Extensions.DependencyInjection;
+using Volo.Abp.Modularity;
+using Volo.Abp.MultiTenancy;
+using Volo.Abp.Threading;
+
+namespace Volo.Abp.BlobStoring
+{
+ [DependsOn(
+ typeof(AbpMultiTenancyModule),
+ typeof(AbpThreadingModule)
+ )]
+ public class AbpBlobStoringModule : AbpModule
+ {
+ public override void ConfigureServices(ServiceConfigurationContext context)
+ {
+ context.Services.AddTransient(
+ typeof(IBlobContainer<>),
+ typeof(BlobContainer<>)
+ );
+
+ context.Services.AddTransient(
+ typeof(IBlobContainer),
+ serviceProvider => serviceProvider
+ .GetRequiredService>()
+ );
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/AbpBlobStoringOptions.cs b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/AbpBlobStoringOptions.cs
new file mode 100644
index 0000000000..4ed0ce69a0
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/AbpBlobStoringOptions.cs
@@ -0,0 +1,12 @@
+namespace Volo.Abp.BlobStoring
+{
+ public class AbpBlobStoringOptions
+ {
+ public BlobContainerConfigurations Containers { get; }
+
+ public AbpBlobStoringOptions()
+ {
+ Containers = new BlobContainerConfigurations();
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainer.cs b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainer.cs
new file mode 100644
index 0000000000..700549e957
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainer.cs
@@ -0,0 +1,198 @@
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Volo.Abp.MultiTenancy;
+using Volo.Abp.Threading;
+
+namespace Volo.Abp.BlobStoring
+{
+ public class BlobContainer : IBlobContainer
+ where TContainer : class
+ {
+ private readonly IBlobContainer _container;
+
+ public BlobContainer(IBlobContainerFactory blobContainerFactory)
+ {
+ _container = blobContainerFactory.Create();
+ }
+
+ public Task SaveAsync(
+ string name,
+ Stream stream,
+ bool overrideExisting = false,
+ CancellationToken cancellationToken = default)
+ {
+ return _container.SaveAsync(
+ name,
+ stream,
+ overrideExisting,
+ cancellationToken
+ );
+ }
+
+ public Task DeleteAsync(
+ string name,
+ CancellationToken cancellationToken = default)
+ {
+ return _container.DeleteAsync(
+ name,
+ cancellationToken
+ );
+ }
+
+ public Task ExistsAsync(
+ string name,
+ CancellationToken cancellationToken = default)
+ {
+ return _container.ExistsAsync(
+ name,
+ cancellationToken
+ );
+ }
+
+ public Task GetAsync(
+ string name,
+ CancellationToken cancellationToken = default)
+ {
+ return _container.GetAsync(
+ name,
+ cancellationToken
+ );
+ }
+
+ public Task GetOrNullAsync(
+ string name,
+ CancellationToken cancellationToken = default)
+ {
+ return _container.GetOrNullAsync(
+ name,
+ cancellationToken
+ );
+ }
+ }
+
+ public class BlobContainer : IBlobContainer
+ {
+ protected string ContainerName { get; }
+
+ protected BlobContainerConfiguration Configuration { get; }
+
+ protected IBlobProvider Provider { get; }
+
+ protected ICurrentTenant CurrentTenant { get; }
+
+ protected ICancellationTokenProvider CancellationTokenProvider { get; }
+
+ public BlobContainer(
+ string containerName,
+ BlobContainerConfiguration configuration,
+ IBlobProvider provider,
+ ICurrentTenant currentTenant,
+ ICancellationTokenProvider cancellationTokenProvider)
+ {
+ ContainerName = containerName;
+ Configuration = configuration;
+ Provider = provider;
+ CurrentTenant = currentTenant;
+ CancellationTokenProvider = cancellationTokenProvider;
+ }
+
+ public virtual Task SaveAsync(
+ string name,
+ Stream stream,
+ bool overrideExisting = false,
+ CancellationToken cancellationToken = default)
+ {
+ return Provider.SaveAsync(
+ new BlobProviderSaveArgs(
+ ContainerName,
+ Configuration,
+ name,
+ stream,
+ overrideExisting,
+ GetTenantIdOrNull(),
+ CancellationTokenProvider.FallbackToProvider(cancellationToken)
+ )
+ );
+ }
+
+ public virtual Task DeleteAsync(
+ string name,
+ CancellationToken cancellationToken = default)
+ {
+ return Provider.DeleteAsync(
+ new BlobProviderDeleteArgs(
+ ContainerName,
+ Configuration,
+ name,
+ GetTenantIdOrNull(),
+ CancellationTokenProvider.FallbackToProvider(cancellationToken)
+ )
+ );
+ }
+
+ public virtual Task ExistsAsync(
+ string name,
+ CancellationToken cancellationToken = default)
+ {
+ return Provider.ExistsAsync(
+ new BlobProviderExistsArgs(
+ ContainerName,
+ Configuration,
+ name,
+ GetTenantIdOrNull(),
+ CancellationTokenProvider.FallbackToProvider(cancellationToken)
+ )
+ );
+ }
+
+ public virtual Task GetAsync(
+ string name,
+ CancellationToken cancellationToken = default)
+ {
+ var stream = Provider.GetOrNullAsync(
+ new BlobProviderGetArgs(
+ ContainerName,
+ Configuration,
+ name,
+ GetTenantIdOrNull(),
+ CancellationTokenProvider.FallbackToProvider(cancellationToken)
+ )
+ );
+
+ if (stream == null)
+ {
+ //TODO: Consider to throw some type of "not found" exception and handle on the HTTP status side
+ throw new AbpException($"Could not found the requested BLOB '{name}' in the container '{ContainerName}'!");
+ }
+
+ return stream;
+ }
+
+ public virtual Task GetOrNullAsync(
+ string name,
+ CancellationToken cancellationToken = default)
+ {
+ return Provider.GetOrNullAsync(
+ new BlobProviderGetArgs(
+ ContainerName,
+ Configuration,
+ name,
+ GetTenantIdOrNull(),
+ CancellationTokenProvider.FallbackToProvider(cancellationToken)
+ )
+ );
+ }
+
+ protected virtual Guid? GetTenantIdOrNull()
+ {
+ if (!Configuration.IsMultiTenant)
+ {
+ return null;
+ }
+
+ return CurrentTenant.Id;
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainerConfiguration.cs b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainerConfiguration.cs
new file mode 100644
index 0000000000..46f13a1c3d
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainerConfiguration.cs
@@ -0,0 +1,71 @@
+using System;
+using System.Collections.Generic;
+using JetBrains.Annotations;
+
+namespace Volo.Abp.BlobStoring
+{
+ public class BlobContainerConfiguration
+ {
+ ///
+ /// The provider to be used to store BLOBs of this container.
+ ///
+ public Type ProviderType { get; set; }
+
+ ///
+ /// Indicates whether this container is multi-tenant or not.
+ ///
+ /// If this is false and your application is multi-tenant,
+ /// then the container is shared by all tenants in the system.
+ ///
+ /// This can be true even if your application is not multi-tenant.
+ ///
+ /// Default: true.
+ ///
+ public bool IsMultiTenant { get; set; } = true;
+
+ [NotNull] private readonly Dictionary _properties;
+
+ [CanBeNull] private readonly BlobContainerConfiguration _fallbackConfiguration;
+
+ public BlobContainerConfiguration(BlobContainerConfiguration fallbackConfiguration = null)
+ {
+ _fallbackConfiguration = fallbackConfiguration;
+ _properties = new Dictionary();
+ }
+
+ [CanBeNull]
+ public T GetConfigurationOrDefault(string name, T defaultValue = default)
+ {
+ return (T) GetConfigurationOrNull(name, defaultValue);
+ }
+
+ [CanBeNull]
+ public object GetConfigurationOrNull(string name, object defaultValue = null)
+ {
+ return _properties.GetOrDefault(name) ??
+ _fallbackConfiguration?.GetConfigurationOrNull(name, defaultValue) ??
+ defaultValue;
+ }
+
+ [NotNull]
+ public BlobContainerConfiguration SetConfiguration([NotNull] string name, [CanBeNull] object value)
+ {
+ Check.NotNullOrWhiteSpace(name, nameof(name));
+ Check.NotNull(value, nameof(value));
+
+ _properties[name] = value;
+
+ return this;
+ }
+
+ [NotNull]
+ public BlobContainerConfiguration ClearConfiguration([NotNull] string name)
+ {
+ Check.NotNullOrWhiteSpace(name, nameof(name));
+
+ _properties.Remove(name);
+
+ return this;
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainerConfigurationExtensions.cs b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainerConfigurationExtensions.cs
new file mode 100644
index 0000000000..ca437b0ad0
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainerConfigurationExtensions.cs
@@ -0,0 +1,27 @@
+using JetBrains.Annotations;
+
+namespace Volo.Abp.BlobStoring
+{
+ public static class BlobContainerConfigurationExtensions
+ {
+ public static T GetConfiguration(
+ [NotNull] this BlobContainerConfiguration containerConfiguration,
+ [NotNull] string name)
+ {
+ return (T) containerConfiguration.GetConfiguration(name);
+ }
+
+ public static object GetConfiguration(
+ [NotNull] this BlobContainerConfiguration containerConfiguration,
+ [NotNull] string name)
+ {
+ var value = containerConfiguration.GetConfigurationOrNull(name);
+ if (value == null)
+ {
+ throw new AbpException($"Could not find the configuration value for '{name}'!");
+ }
+
+ return value;
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainerConfigurationProviderExtensions.cs b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainerConfigurationProviderExtensions.cs
new file mode 100644
index 0000000000..a199d8a72f
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainerConfigurationProviderExtensions.cs
@@ -0,0 +1,11 @@
+namespace Volo.Abp.BlobStoring
+{
+ public static class BlobContainerConfigurationProviderExtensions
+ {
+ public static BlobContainerConfiguration Get(
+ this IBlobContainerConfigurationProvider configurationProvider)
+ {
+ return configurationProvider.Get(BlobContainerNameAttribute.GetContainerName());
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainerConfigurations.cs b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainerConfigurations.cs
new file mode 100644
index 0000000000..5e17793f21
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainerConfigurations.cs
@@ -0,0 +1,61 @@
+using System;
+using System.Collections.Generic;
+using JetBrains.Annotations;
+
+namespace Volo.Abp.BlobStoring
+{
+ public class BlobContainerConfigurations
+ {
+ public BlobContainerConfiguration Default { get; }
+
+ private readonly Dictionary _containers;
+
+ public BlobContainerConfigurations()
+ {
+ Default = new BlobContainerConfiguration();
+ _containers = new Dictionary();
+ }
+
+ public BlobContainerConfigurations Configure(
+ Action configureAction)
+ {
+ return Configure(
+ BlobContainerNameAttribute.GetContainerName(),
+ configureAction
+ );
+ }
+
+ public BlobContainerConfigurations Configure(
+ [NotNull] string name,
+ [NotNull] Action configureAction)
+ {
+ Check.NotNullOrWhiteSpace(name, nameof(name));
+ Check.NotNull(configureAction, nameof(configureAction));
+
+ configureAction(_containers.GetOrAdd(name, () => new BlobContainerConfiguration(Default)));
+
+ return this;
+ }
+
+ public BlobContainerConfigurations ConfigureDefault(Action configureAction)
+ {
+ configureAction(Default);
+ return this;
+ }
+
+ [NotNull]
+ public BlobContainerConfiguration GetConfiguration()
+ {
+ return GetConfiguration(BlobContainerNameAttribute.GetContainerName());
+ }
+
+ [NotNull]
+ public BlobContainerConfiguration GetConfiguration([NotNull] string name)
+ {
+ Check.NotNullOrWhiteSpace(name, nameof(name));
+
+ return _containers.GetOrDefault(name) ??
+ Default;
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainerFactory.cs b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainerFactory.cs
new file mode 100644
index 0000000000..62f6aa7a73
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainerFactory.cs
@@ -0,0 +1,43 @@
+using System.Threading;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.MultiTenancy;
+using Volo.Abp.Threading;
+
+namespace Volo.Abp.BlobStoring
+{
+ public class BlobContainerFactory : IBlobContainerFactory, ITransientDependency
+ {
+ protected IBlobProviderSelector ProviderSelector { get; }
+
+ protected IBlobContainerConfigurationProvider ConfigurationProvider { get; }
+
+ protected ICurrentTenant CurrentTenant { get; }
+
+ protected ICancellationTokenProvider CancellationTokenProvider { get; }
+
+ public BlobContainerFactory(
+ IBlobContainerConfigurationProvider configurationProvider,
+ ICurrentTenant currentTenant,
+ ICancellationTokenProvider cancellationTokenProvider,
+ IBlobProviderSelector providerSelector)
+ {
+ ConfigurationProvider = configurationProvider;
+ CurrentTenant = currentTenant;
+ CancellationTokenProvider = cancellationTokenProvider;
+ ProviderSelector = providerSelector;
+ }
+
+ public virtual IBlobContainer Create(string name, CancellationToken cancellationToken = default)
+ {
+ var configuration = ConfigurationProvider.Get(name);
+
+ return new BlobContainer(
+ name,
+ configuration,
+ ProviderSelector.Get(name),
+ CurrentTenant,
+ CancellationTokenProvider
+ );
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainerFactoryExtensions.cs b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainerFactoryExtensions.cs
new file mode 100644
index 0000000000..34483c1849
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainerFactoryExtensions.cs
@@ -0,0 +1,26 @@
+using System.Threading;
+
+namespace Volo.Abp.BlobStoring
+{
+ public static class BlobContainerFactoryExtensions
+ {
+ ///
+ /// Gets a named container.
+ ///
+ /// The blob container manager
+ /// Cancellation token
+ ///
+ /// The container object.
+ ///
+ public static IBlobContainer Create(
+ this IBlobContainerFactory blobContainerFactory,
+ CancellationToken cancellationToken = default
+ )
+ {
+ return blobContainerFactory.Create(
+ BlobContainerNameAttribute.GetContainerName(),
+ cancellationToken
+ );
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainerNameAttribute.cs b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainerNameAttribute.cs
new file mode 100644
index 0000000000..12bd91d669
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainerNameAttribute.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Reflection;
+using JetBrains.Annotations;
+
+namespace Volo.Abp.BlobStoring
+{
+ public class BlobContainerNameAttribute : Attribute
+ {
+ [NotNull]
+ public string Name { get; }
+
+ public BlobContainerNameAttribute([NotNull] string name)
+ {
+ Check.NotNullOrWhiteSpace(name, nameof(name));
+
+ Name = name;
+ }
+
+ public virtual string GetName(Type type)
+ {
+ return Name;
+ }
+
+ public static string GetContainerName()
+ {
+ return GetContainerName(typeof(T));
+ }
+
+ public static string GetContainerName(Type type)
+ {
+ var nameAttribute = type.GetCustomAttribute();
+
+ if (nameAttribute == null)
+ {
+ return type.FullName;
+ }
+
+ return nameAttribute.GetName(type);
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobProviderArgs.cs b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobProviderArgs.cs
new file mode 100644
index 0000000000..e1ad228839
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobProviderArgs.cs
@@ -0,0 +1,36 @@
+using System;
+using System.Threading;
+using JetBrains.Annotations;
+
+namespace Volo.Abp.BlobStoring
+{
+ public abstract class BlobProviderArgs
+ {
+ [NotNull]
+ public string ContainerName { get; }
+
+ [NotNull]
+ public BlobContainerConfiguration Configuration { get; }
+
+ [NotNull]
+ public string BlobName { get; }
+
+ public CancellationToken CancellationToken { get; }
+
+ public Guid? TenantId { get; }
+
+ protected BlobProviderArgs(
+ [NotNull] string containerName,
+ [NotNull] BlobContainerConfiguration configuration,
+ [NotNull] string blobName,
+ [CanBeNull] Guid? tenantId = null,
+ CancellationToken cancellationToken = default)
+ {
+ ContainerName = Check.NotNullOrWhiteSpace(containerName, nameof(containerName));
+ Configuration = Check.NotNull(configuration, nameof(configuration));
+ BlobName = Check.NotNullOrWhiteSpace(blobName, nameof(blobName));
+ TenantId = tenantId;
+ CancellationToken = cancellationToken;
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobProviderBase.cs b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobProviderBase.cs
new file mode 100644
index 0000000000..b1aafb6903
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobProviderBase.cs
@@ -0,0 +1,16 @@
+using System.IO;
+using System.Threading.Tasks;
+
+namespace Volo.Abp.BlobStoring
+{
+ public abstract class BlobProviderBase : IBlobProvider
+ {
+ public abstract Task SaveAsync(BlobProviderSaveArgs args);
+
+ public abstract Task DeleteAsync(BlobProviderDeleteArgs args);
+
+ public abstract Task ExistsAsync(BlobProviderExistsArgs args);
+
+ public abstract Task GetOrNullAsync(BlobProviderGetArgs args);
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobProviderDeleteArgs.cs b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobProviderDeleteArgs.cs
new file mode 100644
index 0000000000..aa4aae1eb2
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobProviderDeleteArgs.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Threading;
+using JetBrains.Annotations;
+
+namespace Volo.Abp.BlobStoring
+{
+ public class BlobProviderDeleteArgs : BlobProviderArgs
+ {
+ public BlobProviderDeleteArgs(
+ [NotNull] string containerName,
+ [NotNull] BlobContainerConfiguration configuration,
+ [NotNull] string blobName,
+ [CanBeNull] Guid? tenantId = null,
+ CancellationToken cancellationToken = default)
+ : base(
+ containerName,
+ configuration,
+ blobName,
+ tenantId,
+ cancellationToken)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobProviderExistsArgs.cs b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobProviderExistsArgs.cs
new file mode 100644
index 0000000000..a55b22a233
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobProviderExistsArgs.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Threading;
+using JetBrains.Annotations;
+
+namespace Volo.Abp.BlobStoring
+{
+ public class BlobProviderExistsArgs : BlobProviderArgs
+ {
+ public BlobProviderExistsArgs(
+ [NotNull] string containerName,
+ [NotNull] BlobContainerConfiguration configuration,
+ [NotNull] string blobName,
+ [CanBeNull] Guid? tenantId = null,
+ CancellationToken cancellationToken = default)
+ : base(
+ containerName,
+ configuration,
+ blobName,
+ tenantId,
+ cancellationToken)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobProviderGetArgs.cs b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobProviderGetArgs.cs
new file mode 100644
index 0000000000..94374698c7
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobProviderGetArgs.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Threading;
+using JetBrains.Annotations;
+
+namespace Volo.Abp.BlobStoring
+{
+ public class BlobProviderGetArgs : BlobProviderArgs
+ {
+ public BlobProviderGetArgs(
+ [NotNull] string containerName,
+ [NotNull] BlobContainerConfiguration configuration,
+ [NotNull] string blobName,
+ [CanBeNull] Guid? tenantId = null,
+ CancellationToken cancellationToken = default)
+ : base(
+ containerName,
+ configuration,
+ blobName,
+ tenantId,
+ cancellationToken)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobProviderSaveArgs.cs b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobProviderSaveArgs.cs
new file mode 100644
index 0000000000..a81938fb7d
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobProviderSaveArgs.cs
@@ -0,0 +1,34 @@
+using System;
+using System.IO;
+using System.Threading;
+using JetBrains.Annotations;
+
+namespace Volo.Abp.BlobStoring
+{
+ public class BlobProviderSaveArgs : BlobProviderArgs
+ {
+ [NotNull]
+ public Stream BlobStream { get; }
+
+ public bool OverrideExisting { get; }
+
+ public BlobProviderSaveArgs(
+ [NotNull] string containerName,
+ [NotNull] BlobContainerConfiguration configuration,
+ [NotNull] string blobName,
+ [NotNull] Stream blobStream,
+ bool overrideExisting = false,
+ [CanBeNull] Guid? tenantId = null,
+ CancellationToken cancellationToken = default)
+ : base(
+ containerName,
+ configuration,
+ blobName,
+ tenantId,
+ cancellationToken)
+ {
+ BlobStream = Check.NotNull(blobStream, nameof(blobStream));
+ OverrideExisting = overrideExisting;
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobProviderSelectorExtensions.cs b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobProviderSelectorExtensions.cs
new file mode 100644
index 0000000000..1d484ebd2e
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobProviderSelectorExtensions.cs
@@ -0,0 +1,15 @@
+using JetBrains.Annotations;
+
+namespace Volo.Abp.BlobStoring
+{
+ public static class BlobProviderSelectorExtensions
+ {
+ public static IBlobProvider Get(
+ [NotNull] this IBlobProviderSelector selector)
+ {
+ Check.NotNull(selector, nameof(selector));
+
+ return selector.Get(BlobContainerNameAttribute.GetContainerName());
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/DefaultBlobContainerConfigurationProvider.cs b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/DefaultBlobContainerConfigurationProvider.cs
new file mode 100644
index 0000000000..a30d584c9d
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/DefaultBlobContainerConfigurationProvider.cs
@@ -0,0 +1,20 @@
+using Microsoft.Extensions.Options;
+using Volo.Abp.DependencyInjection;
+
+namespace Volo.Abp.BlobStoring
+{
+ public class DefaultBlobContainerConfigurationProvider : IBlobContainerConfigurationProvider, ITransientDependency
+ {
+ protected AbpBlobStoringOptions Options { get; }
+
+ public DefaultBlobContainerConfigurationProvider(IOptions options)
+ {
+ Options = options.Value;
+ }
+
+ public virtual BlobContainerConfiguration Get(string name)
+ {
+ return Options.Containers.GetConfiguration(name);
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/DefaultBlobProviderSelector.cs b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/DefaultBlobProviderSelector.cs
new file mode 100644
index 0000000000..5f604155ca
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/DefaultBlobProviderSelector.cs
@@ -0,0 +1,49 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using JetBrains.Annotations;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.DynamicProxy;
+
+namespace Volo.Abp.BlobStoring
+{
+ public class DefaultBlobProviderSelector : IBlobProviderSelector, ITransientDependency
+ {
+ protected IEnumerable BlobProviders { get; }
+
+ protected IBlobContainerConfigurationProvider ConfigurationProvider { get; }
+
+ public DefaultBlobProviderSelector(
+ IBlobContainerConfigurationProvider configurationProvider,
+ IEnumerable blobProviders)
+ {
+ ConfigurationProvider = configurationProvider;
+ BlobProviders = blobProviders;
+ }
+
+ [NotNull]
+ public virtual IBlobProvider Get([NotNull] string containerName)
+ {
+ Check.NotNull(containerName, nameof(containerName));
+
+ var configuration = ConfigurationProvider.Get(containerName);
+
+ if (!BlobProviders.Any())
+ {
+ throw new AbpException("No BLOB Storage provider was registered! At least one provider must be registered to be able to use the Blog Storing System.");
+ }
+
+ foreach (var provider in BlobProviders)
+ {
+ if (ProxyHelper.GetUnProxiedType(provider).IsAssignableTo(configuration.ProviderType))
+ {
+ return provider;
+ }
+ }
+
+ throw new AbpException(
+ $"Could not find the BLOB Storage provider with the type ({configuration.ProviderType.AssemblyQualifiedName}) configured for the container {containerName} and no default provider was set."
+ );
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/DefaultContainer.cs b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/DefaultContainer.cs
new file mode 100644
index 0000000000..64f907e564
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/DefaultContainer.cs
@@ -0,0 +1,8 @@
+namespace Volo.Abp.BlobStoring
+{
+ [BlobContainerName("Default")]
+ public class DefaultContainer
+ {
+
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/IBlobContainer.cs b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/IBlobContainer.cs
new file mode 100644
index 0000000000..a596565b5f
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/IBlobContainer.cs
@@ -0,0 +1,88 @@
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Volo.Abp.BlobStoring
+{
+ public interface IBlobContainer : IBlobContainer
+ where TContainer: class
+ {
+
+ }
+
+ public interface IBlobContainer
+ {
+ ///
+ /// Saves a blob to the container.
+ ///
+ /// The name of the blob
+ /// A stream for the blob
+ ///
+ /// Set true to override if there is already a blob in the container with the given name.
+ /// If set to false (default), throws exception if there is already a blob in the container with the given name.
+ ///
+ /// Cancellation token
+ Task SaveAsync(
+ string name,
+ Stream stream,
+ bool overrideExisting = false,
+ CancellationToken cancellationToken = default
+ );
+
+ ///
+ /// Deletes a blob from the container.
+ ///
+ /// The name of the blob
+ /// Cancellation token
+ ///
+ /// Returns true if actually deleted the blob.
+ /// Returns false if the blob with the given was not exists.
+ ///
+ Task DeleteAsync(
+ string name,
+ CancellationToken cancellationToken = default
+ );
+
+ ///
+ /// Checks if a blob does exists in the container.
+ ///
+ /// The name of the blob
+ /// Cancellation token
+ Task ExistsAsync(
+ string name,
+ CancellationToken cancellationToken = default
+ );
+
+ ///
+ /// Gets a blob from the container.
+ /// It actually gets a to read the blob data.
+ /// It throws exception if there is no blob with the given .
+ /// Use if you want to get null if there is no blob with the given .
+ ///
+ /// The name of the blob
+ /// Cancellation token
+ ///
+ /// A to read the blob data.
+ ///
+ Task GetAsync(
+ string name,
+ CancellationToken cancellationToken = default
+ );
+
+ ///
+ /// Gets a blob from the container, or returns null if there is no blob with the given .
+ /// It actually gets a to read the blob data.
+ ///
+ /// The name of the blob
+ /// Cancellation token
+ ///
+ /// A to read the blob data.
+ ///
+ Task GetOrNullAsync(
+ string name,
+ CancellationToken cancellationToken = default
+ );
+
+ //TODO: Create shortcut extension methods: GetAsArraryAsync, GetAsStringAsync(encoding) (and null versions)
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/IBlobContainerConfigurationProvider.cs b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/IBlobContainerConfigurationProvider.cs
new file mode 100644
index 0000000000..f984a903a2
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/IBlobContainerConfigurationProvider.cs
@@ -0,0 +1,7 @@
+namespace Volo.Abp.BlobStoring
+{
+ public interface IBlobContainerConfigurationProvider
+ {
+ BlobContainerConfiguration Get(string name);
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/IBlobContainerFactory.cs b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/IBlobContainerFactory.cs
new file mode 100644
index 0000000000..8434e3ae23
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/IBlobContainerFactory.cs
@@ -0,0 +1,20 @@
+using System.Threading;
+
+namespace Volo.Abp.BlobStoring
+{
+ public interface IBlobContainerFactory
+ {
+ ///
+ /// Gets a named container.
+ ///
+ /// The name of the container
+ /// Cancellation token
+ ///
+ /// The container object.
+ ///
+ IBlobContainer Create(
+ string name,
+ CancellationToken cancellationToken = default
+ );
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/IBlobProvider.cs b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/IBlobProvider.cs
new file mode 100644
index 0000000000..d7332589c5
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/IBlobProvider.cs
@@ -0,0 +1,17 @@
+using System;
+using System.IO;
+using System.Threading.Tasks;
+
+namespace Volo.Abp.BlobStoring
+{
+ public interface IBlobProvider
+ {
+ Task SaveAsync(BlobProviderSaveArgs args);
+
+ Task DeleteAsync(BlobProviderDeleteArgs args);
+
+ Task ExistsAsync(BlobProviderExistsArgs args);
+
+ Task GetOrNullAsync(BlobProviderGetArgs args);
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/IBlobProviderSelector.cs b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/IBlobProviderSelector.cs
new file mode 100644
index 0000000000..defb8f0804
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/IBlobProviderSelector.cs
@@ -0,0 +1,10 @@
+using JetBrains.Annotations;
+
+namespace Volo.Abp.BlobStoring
+{
+ public interface IBlobProviderSelector
+ {
+ [NotNull]
+ IBlobProvider Get([NotNull] string containerName);
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/DynamicProxy/ProxyHelper.cs b/framework/src/Volo.Abp.Core/Volo/Abp/DynamicProxy/ProxyHelper.cs
index 96a1915e26..db7ff4b869 100644
--- a/framework/src/Volo.Abp.Core/Volo/Abp/DynamicProxy/ProxyHelper.cs
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/DynamicProxy/ProxyHelper.cs
@@ -1,4 +1,5 @@
-using System.Linq;
+using System;
+using System.Linq;
using System.Reflection;
namespace Volo.Abp.DynamicProxy
@@ -6,6 +7,7 @@ namespace Volo.Abp.DynamicProxy
public static class ProxyHelper
{
private const string ProxyNamespace = "Castle.Proxies";
+
///
/// Returns dynamic proxy target object if this is a proxied object, otherwise returns the given object.
/// It supports Castle Dynamic Proxies.
@@ -28,5 +30,10 @@ namespace Volo.Abp.DynamicProxy
return targetField.GetValue(obj);
}
+
+ public static Type GetUnProxiedType(object obj)
+ {
+ return UnProxy(obj).GetType();
+ }
}
}
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/IO/DirectoryHelper.cs b/framework/src/Volo.Abp.Core/Volo/Abp/IO/DirectoryHelper.cs
index b864695430..23f8de610a 100644
--- a/framework/src/Volo.Abp.Core/Volo/Abp/IO/DirectoryHelper.cs
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/IO/DirectoryHelper.cs
@@ -16,6 +16,14 @@ namespace Volo.Abp.IO
Directory.CreateDirectory(directory);
}
}
+
+ public static void CreateIfNotExists(DirectoryInfo directory)
+ {
+ if (!directory.Exists)
+ {
+ directory.Create();
+ }
+ }
public static bool IsSubDirectoryOf([NotNull] string parentDirectoryPath, [NotNull] string childDirectoryPath)
{
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/IO/FileHelper.cs b/framework/src/Volo.Abp.Core/Volo/Abp/IO/FileHelper.cs
index a945784352..8f4a15bdf5 100644
--- a/framework/src/Volo.Abp.Core/Volo/Abp/IO/FileHelper.cs
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/IO/FileHelper.cs
@@ -17,12 +17,15 @@ namespace Volo.Abp.IO
/// Checks and deletes given file if it does exists.
///
/// Path of the file
- public static void DeleteIfExists(string filePath)
+ public static bool DeleteIfExists(string filePath)
{
- if (File.Exists(filePath))
+ if (!File.Exists(filePath))
{
- File.Delete(filePath);
+ return false;
}
+
+ File.Delete(filePath);
+ return true;
}
///
diff --git a/framework/test/Volo.Abp.BlobStoring.FileSystem.Tests/Volo.Abp.BlobStoring.FileSystem.Tests.csproj b/framework/test/Volo.Abp.BlobStoring.FileSystem.Tests/Volo.Abp.BlobStoring.FileSystem.Tests.csproj
new file mode 100644
index 0000000000..1ac7ba2414
--- /dev/null
+++ b/framework/test/Volo.Abp.BlobStoring.FileSystem.Tests/Volo.Abp.BlobStoring.FileSystem.Tests.csproj
@@ -0,0 +1,17 @@
+
+
+
+
+
+ netcoreapp3.1
+
+
+
+
+
+
+
+
+
+
+
diff --git a/framework/test/Volo.Abp.BlobStoring.FileSystem.Tests/Volo/Abp/BlobStoring/FileSystem/AbpBlobStoringFileSystemTestModule.cs b/framework/test/Volo.Abp.BlobStoring.FileSystem.Tests/Volo/Abp/BlobStoring/FileSystem/AbpBlobStoringFileSystemTestModule.cs
new file mode 100644
index 0000000000..35fc30b84d
--- /dev/null
+++ b/framework/test/Volo.Abp.BlobStoring.FileSystem.Tests/Volo/Abp/BlobStoring/FileSystem/AbpBlobStoringFileSystemTestModule.cs
@@ -0,0 +1,27 @@
+using System;
+using System.IO;
+using Volo.Abp.BlobStoring.FileSystem.TestObjects;
+using Volo.Abp.Modularity;
+
+namespace Volo.Abp.BlobStoring.FileSystem
+{
+ [DependsOn(
+ typeof(AbpBlobStoringFileSystemModule)
+ )]
+ public class AbpBlobStoringFileSystemTestModule : AbpModule
+ {
+ public override void ConfigureServices(ServiceConfigurationContext context)
+ {
+ Configure(options =>
+ {
+ options.Containers.ConfigureDefault(container =>
+ {
+ container.UseFileSystem(fileSystem =>
+ {
+ fileSystem.BasePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
+ });
+ });
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/test/Volo.Abp.BlobStoring.FileSystem.Tests/Volo/Abp/BlobStoring/FileSystem/AbpBlobStoringTestBase.cs b/framework/test/Volo.Abp.BlobStoring.FileSystem.Tests/Volo/Abp/BlobStoring/FileSystem/AbpBlobStoringTestBase.cs
new file mode 100644
index 0000000000..8fdd832a5b
--- /dev/null
+++ b/framework/test/Volo.Abp.BlobStoring.FileSystem.Tests/Volo/Abp/BlobStoring/FileSystem/AbpBlobStoringTestBase.cs
@@ -0,0 +1,12 @@
+using Volo.Abp.Testing;
+
+namespace Volo.Abp.BlobStoring.FileSystem
+{
+ public abstract class AbpBlobStoringFileSystemTestBase : AbpIntegratedTest
+ {
+ protected override void SetAbpApplicationCreationOptions(AbpApplicationCreationOptions options)
+ {
+ options.UseAutofac();
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/test/Volo.Abp.BlobStoring.FileSystem.Tests/Volo/Abp/BlobStoring/FileSystem/BlogFilePathCalculator_Tests.cs b/framework/test/Volo.Abp.BlobStoring.FileSystem.Tests/Volo/Abp/BlobStoring/FileSystem/BlogFilePathCalculator_Tests.cs
new file mode 100644
index 0000000000..2493dab733
--- /dev/null
+++ b/framework/test/Volo.Abp.BlobStoring.FileSystem.Tests/Volo/Abp/BlobStoring/FileSystem/BlogFilePathCalculator_Tests.cs
@@ -0,0 +1,71 @@
+using System;
+using System.IO;
+using Shouldly;
+using Xunit;
+
+namespace Volo.Abp.BlobStoring.FileSystem
+{
+ public class BlogFilePathCalculator_Tests : AbpBlobStoringFileSystemTestBase
+ {
+ private readonly IBlogFilePathCalculator _calculator;
+
+ public BlogFilePathCalculator_Tests()
+ {
+ _calculator = GetRequiredService();
+ }
+
+ [Fact]
+ public void Default_Settings()
+ {
+ var separator = Path.DirectorySeparatorChar;
+
+ _calculator.Calculate(
+ GetArgs($"C:{separator}my-files", "my-container", "my-blob")
+ ).ShouldBe($"C:{separator}my-files{separator}host{separator}my-container{separator}my-blob");
+ }
+
+ [Fact]
+ public void Default_Settings_With_TenantId()
+ {
+ var separator = Path.DirectorySeparatorChar;
+ var tenantId = Guid.NewGuid();
+
+ _calculator.Calculate(
+ GetArgs($"C:{separator}my-files", "my-container", "my-blob", tenantId: tenantId)
+ ).ShouldBe($"C:{separator}my-files{separator}tenants{separator}{tenantId:D}{separator}my-container{separator}my-blob");
+ }
+
+ [Fact]
+ public void AppendContainerNameToBasePath_Set_To_False()
+ {
+ var separator = Path.DirectorySeparatorChar;
+
+ _calculator.Calculate(
+ GetArgs($"C:{separator}my-files", "my-container", "my-blob", appendContainerNameToBasePath: false)
+ ).ShouldBe($"C:{separator}my-files{separator}host{separator}my-blob");
+ }
+
+ private BlobProviderArgs GetArgs(
+ string basePath,
+ string containerName,
+ string blobName,
+ Guid? tenantId = null,
+ bool? appendContainerNameToBasePath = null)
+ {
+ return new BlobProviderGetArgs(
+ containerName,
+ new BlobContainerConfiguration()
+ .UseFileSystem(fs =>
+ {
+ fs.BasePath = basePath;
+ if (appendContainerNameToBasePath.HasValue)
+ {
+ fs.AppendContainerNameToBasePath = appendContainerNameToBasePath.Value;
+ }
+ }),
+ blobName,
+ tenantId
+ );
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/test/Volo.Abp.BlobStoring.FileSystem.Tests/Volo/Abp/BlobStoring/FileSystem/TestObjects/TestContainer.cs b/framework/test/Volo.Abp.BlobStoring.FileSystem.Tests/Volo/Abp/BlobStoring/FileSystem/TestObjects/TestContainer.cs
new file mode 100644
index 0000000000..91ac79f6c3
--- /dev/null
+++ b/framework/test/Volo.Abp.BlobStoring.FileSystem.Tests/Volo/Abp/BlobStoring/FileSystem/TestObjects/TestContainer.cs
@@ -0,0 +1,8 @@
+namespace Volo.Abp.BlobStoring.FileSystem.TestObjects
+{
+ [BlobContainerName("TestContainer")]
+ public class TestContainer
+ {
+
+ }
+}
\ No newline at end of file
diff --git a/framework/test/Volo.Abp.BlobStoring.Tests/Volo.Abp.BlobStoring.Tests.csproj b/framework/test/Volo.Abp.BlobStoring.Tests/Volo.Abp.BlobStoring.Tests.csproj
new file mode 100644
index 0000000000..d9535ff176
--- /dev/null
+++ b/framework/test/Volo.Abp.BlobStoring.Tests/Volo.Abp.BlobStoring.Tests.csproj
@@ -0,0 +1,17 @@
+
+
+
+
+
+ netcoreapp3.1
+
+
+
+
+
+
+
+
+
+
+
diff --git a/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/AbpBlobStoringOptions_Tests.cs b/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/AbpBlobStoringOptions_Tests.cs
new file mode 100644
index 0000000000..53c7c2079d
--- /dev/null
+++ b/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/AbpBlobStoringOptions_Tests.cs
@@ -0,0 +1,41 @@
+using System.Collections.Generic;
+using Microsoft.Extensions.Options;
+using Shouldly;
+using Volo.Abp.BlobStoring.Fakes;
+using Volo.Abp.BlobStoring.TestObjects;
+using Xunit;
+
+namespace Volo.Abp.BlobStoring
+{
+ public class AbpBlobStoringOptions_Tests : AbpBlobStoringTestBase
+ {
+ private readonly IBlobContainerConfigurationProvider _configurationProvider;
+
+ public AbpBlobStoringOptions_Tests()
+ {
+ _configurationProvider = GetRequiredService();
+ }
+
+ [Fact]
+ public void Should_Property_Set_And_Get_Options_For_Different_Containers()
+ {
+ var testContainer1Config = _configurationProvider.Get();
+ testContainer1Config.ProviderType.ShouldBe(typeof(FakeBlobProvider1));
+ testContainer1Config.GetConfigurationOrDefault("TestConfig1").ShouldBe("TestValue1");
+ testContainer1Config.GetConfigurationOrDefault("TestConfigDefault").ShouldBe("TestValueDefault");
+
+ var testContainer2Config = _configurationProvider.Get();
+ testContainer2Config.ProviderType.ShouldBe(typeof(FakeBlobProvider2));
+ testContainer2Config.GetConfigurationOrNull("TestConfig2").ShouldBe("TestValue2");
+ testContainer2Config.GetConfigurationOrNull("TestConfigDefault").ShouldBe("TestValueDefault");
+ }
+
+ [Fact]
+ public void Should_Fallback_To_Default_Configuration_If_Not_Specialized()
+ {
+ var config = _configurationProvider.Get();
+ config.ProviderType.ShouldBe(typeof(FakeBlobProvider1));
+ config.GetConfigurationOrNull("TestConfigDefault").ShouldBe("TestValueDefault");
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/AbpBlobStoringTestBase.cs b/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/AbpBlobStoringTestBase.cs
new file mode 100644
index 0000000000..07e34c599f
--- /dev/null
+++ b/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/AbpBlobStoringTestBase.cs
@@ -0,0 +1,12 @@
+using Volo.Abp.Testing;
+
+namespace Volo.Abp.BlobStoring
+{
+ public abstract class AbpBlobStoringTestBase : AbpIntegratedTest
+ {
+ protected override void SetAbpApplicationCreationOptions(AbpApplicationCreationOptions options)
+ {
+ options.UseAutofac();
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/AbpBlobStoringTestModule.cs b/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/AbpBlobStoringTestModule.cs
new file mode 100644
index 0000000000..0d150a20ea
--- /dev/null
+++ b/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/AbpBlobStoringTestModule.cs
@@ -0,0 +1,43 @@
+using Microsoft.Extensions.DependencyInjection;
+using NSubstitute;
+using Volo.Abp.Autofac;
+using Volo.Abp.BlobStoring.Fakes;
+using Volo.Abp.BlobStoring.TestObjects;
+using Volo.Abp.Modularity;
+
+namespace Volo.Abp.BlobStoring
+{
+ [DependsOn(
+ typeof(AbpBlobStoringModule),
+ typeof(AbpTestBaseModule),
+ typeof(AbpAutofacModule)
+ )]
+ public class AbpBlobStoringTestModule : AbpModule
+ {
+ public override void ConfigureServices(ServiceConfigurationContext context)
+ {
+ context.Services.AddSingleton(Substitute.For());
+ context.Services.AddSingleton(Substitute.For());
+
+ Configure(options =>
+ {
+ options.Containers
+ .ConfigureDefault(container =>
+ {
+ container.SetConfiguration("TestConfigDefault", "TestValueDefault");
+ container.ProviderType = typeof(FakeBlobProvider1);
+ })
+ .Configure(container =>
+ {
+ container.SetConfiguration("TestConfig1", "TestValue1");
+ container.ProviderType = typeof(FakeBlobProvider1);
+ })
+ .Configure(container =>
+ {
+ container.SetConfiguration("TestConfig2", "TestValue2");
+ container.ProviderType = typeof(FakeBlobProvider2);
+ });
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/BlobContainerFactory_Tests.cs b/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/BlobContainerFactory_Tests.cs
new file mode 100644
index 0000000000..359470106e
--- /dev/null
+++ b/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/BlobContainerFactory_Tests.cs
@@ -0,0 +1,76 @@
+using System.Threading.Tasks;
+using NSubstitute;
+using Volo.Abp.BlobStoring.Fakes;
+using Volo.Abp.BlobStoring.TestObjects;
+using Xunit;
+
+namespace Volo.Abp.BlobStoring
+{
+ public class BlobContainerFactory_Tests : AbpBlobStoringTestBase
+ {
+ private readonly IBlobContainerFactory _factory;
+ private readonly FakeProviders _fakeProviders;
+
+ public BlobContainerFactory_Tests()
+ {
+ _factory = GetRequiredService();
+ _fakeProviders = GetRequiredService();
+ }
+
+ [Fact]
+ public async Task Should_Create_Containers_With_Configured_Providers()
+ {
+ // TestContainer1 with FakeBlobProvider1
+
+ await _fakeProviders.Provider1
+ .DidNotReceiveWithAnyArgs()
+ .ExistsAsync(default);
+
+ await _factory
+ .Create()
+ .ExistsAsync("TestBlob1");
+
+ await _fakeProviders.Provider1
+ .Received(1)
+ .ExistsAsync(Arg.Is(args =>
+ args.ContainerName == BlobContainerNameAttribute.GetContainerName() &&
+ args.BlobName == "TestBlob1"
+ )
+ );
+
+ // TestContainer2 with FakeBlobProvider2
+
+ await _fakeProviders.Provider2
+ .DidNotReceiveWithAnyArgs()
+ .ExistsAsync(default);
+
+ await _factory
+ .Create()
+ .ExistsAsync("TestBlob2");
+
+ await _fakeProviders.Provider2
+ .Received(1)
+ .ExistsAsync(Arg.Is(args =>
+ args.ContainerName == BlobContainerNameAttribute.GetContainerName() &&
+ args.BlobName == "TestBlob2"
+ )
+ );
+
+ // TestContainer3 with FakeBlobProvider1 (default provider)
+
+ _fakeProviders.Provider1.ClearReceivedCalls();
+
+ await _factory
+ .Create()
+ .ExistsAsync("TestBlob3");
+
+ await _fakeProviders.Provider1
+ .Received(1)
+ .ExistsAsync(Arg.Is(t =>
+ t.ContainerName == BlobContainerNameAttribute.GetContainerName() &&
+ t.BlobName == "TestBlob3"
+ )
+ );
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/BlobContainerNameAttribute_Tests.cs b/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/BlobContainerNameAttribute_Tests.cs
new file mode 100644
index 0000000000..242b195a0c
--- /dev/null
+++ b/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/BlobContainerNameAttribute_Tests.cs
@@ -0,0 +1,25 @@
+using Shouldly;
+using Volo.Abp.BlobStoring.TestObjects;
+using Xunit;
+
+namespace Volo.Abp.BlobStoring
+{
+ public class BlobContainerNameAttribute_Tests
+ {
+ [Fact]
+ public void Should_Get_Specified_Name()
+ {
+ BlobContainerNameAttribute
+ .GetContainerName()
+ .ShouldBe("Test2");
+ }
+
+ [Fact]
+ public void Should_Get_Full_Class_Name_If_Not_Specified()
+ {
+ BlobContainerNameAttribute
+ .GetContainerName()
+ .ShouldBe(typeof(TestContainer1).FullName);
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/BlobContainer_Injection_Tests.cs b/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/BlobContainer_Injection_Tests.cs
new file mode 100644
index 0000000000..bd4ec2c47a
--- /dev/null
+++ b/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/BlobContainer_Injection_Tests.cs
@@ -0,0 +1,32 @@
+using Shouldly;
+using Volo.Abp.BlobStoring.TestObjects;
+using Xunit;
+
+namespace Volo.Abp.BlobStoring
+{
+ public class BlobContainer_Injection_Tests : AbpBlobStoringTestBase
+ {
+ [Fact]
+ public void Should_Inject_DefaultContainer_For_Non_Generic_Interface()
+ {
+ GetRequiredService()
+ .ShouldBeOfType>();
+ }
+
+ [Fact]
+ public void Should_Inject_Specified_Container_For_Generic_Interface()
+ {
+ GetRequiredService>()
+ .ShouldBeOfType>();
+
+ GetRequiredService>()
+ .ShouldBeOfType>();
+
+ GetRequiredService>()
+ .ShouldBeOfType>();
+
+ GetRequiredService>()
+ .ShouldBeOfType>();
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/BlobProviderSelector_Tests.cs b/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/BlobProviderSelector_Tests.cs
new file mode 100644
index 0000000000..f36129c19d
--- /dev/null
+++ b/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/BlobProviderSelector_Tests.cs
@@ -0,0 +1,31 @@
+using Shouldly;
+using Volo.Abp.BlobStoring.Fakes;
+using Volo.Abp.BlobStoring.TestObjects;
+using Volo.Abp.DynamicProxy;
+using Xunit;
+
+namespace Volo.Abp.BlobStoring
+{
+ public class BlobProviderSelector_Tests : AbpBlobStoringTestBase
+ {
+ private readonly IBlobProviderSelector _selector;
+
+ public BlobProviderSelector_Tests()
+ {
+ _selector = GetRequiredService();
+ }
+
+ [Fact]
+ public void Should_Select_Default_Provider_If_Not_Configured()
+ {
+ _selector.Get().ShouldBeAssignableTo();
+ }
+
+ [Fact]
+ public void Should_Select_Configured_Provider()
+ {
+ _selector.Get().ShouldBeAssignableTo();
+ _selector.Get().ShouldBeAssignableTo();
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/Fakes/FakeBlobProvider1.cs b/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/Fakes/FakeBlobProvider1.cs
new file mode 100644
index 0000000000..38fef18d66
--- /dev/null
+++ b/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/Fakes/FakeBlobProvider1.cs
@@ -0,0 +1,33 @@
+using System.IO;
+using System.Threading.Tasks;
+
+namespace Volo.Abp.BlobStoring.Fakes
+{
+ public class FakeBlobProvider1 : IBlobProvider
+ {
+ public virtual Task SaveAsync(BlobProviderSaveArgs args)
+ {
+ throw new System.NotImplementedException();
+ }
+
+ public virtual Task DeleteAsync(BlobProviderDeleteArgs args)
+ {
+ throw new System.NotImplementedException();
+ }
+
+ public virtual Task ExistsAsync(BlobProviderExistsArgs args)
+ {
+ throw new System.NotImplementedException();
+ }
+
+ public virtual Task GetAsync(BlobProviderGetArgs args)
+ {
+ throw new System.NotImplementedException();
+ }
+
+ public virtual Task GetOrNullAsync(BlobProviderGetArgs args)
+ {
+ throw new System.NotImplementedException();
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/Fakes/FakeBlobProvider2.cs b/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/Fakes/FakeBlobProvider2.cs
new file mode 100644
index 0000000000..dc0e40d3d8
--- /dev/null
+++ b/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/Fakes/FakeBlobProvider2.cs
@@ -0,0 +1,33 @@
+using System.IO;
+using System.Threading.Tasks;
+
+namespace Volo.Abp.BlobStoring.Fakes
+{
+ public class FakeBlobProvider2 : IBlobProvider
+ {
+ public virtual Task SaveAsync(BlobProviderSaveArgs args)
+ {
+ throw new System.NotImplementedException();
+ }
+
+ public virtual Task DeleteAsync(BlobProviderDeleteArgs args)
+ {
+ throw new System.NotImplementedException();
+ }
+
+ public virtual Task ExistsAsync(BlobProviderExistsArgs args)
+ {
+ throw new System.NotImplementedException();
+ }
+
+ public virtual Task GetAsync(BlobProviderGetArgs args)
+ {
+ throw new System.NotImplementedException();
+ }
+
+ public virtual Task GetOrNullAsync(BlobProviderGetArgs args)
+ {
+ throw new System.NotImplementedException();
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/Fakes/FakeProviders.cs b/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/Fakes/FakeProviders.cs
new file mode 100644
index 0000000000..e66e8b6adc
--- /dev/null
+++ b/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/Fakes/FakeProviders.cs
@@ -0,0 +1,19 @@
+using System.Collections.Generic;
+using System.Linq;
+using Volo.Abp.DependencyInjection;
+
+namespace Volo.Abp.BlobStoring.Fakes
+{
+ public class FakeProviders : ISingletonDependency
+ {
+ public FakeBlobProvider1 Provider1 { get; }
+
+ public FakeBlobProvider2 Provider2 { get; }
+
+ public FakeProviders(IEnumerable providers)
+ {
+ Provider1 = providers.OfType().Single();
+ Provider2 = providers.OfType().Single();
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/TestObjects/TestContainer1.cs b/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/TestObjects/TestContainer1.cs
new file mode 100644
index 0000000000..61c84a1148
--- /dev/null
+++ b/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/TestObjects/TestContainer1.cs
@@ -0,0 +1,7 @@
+namespace Volo.Abp.BlobStoring.TestObjects
+{
+ public class TestContainer1
+ {
+
+ }
+}
\ No newline at end of file
diff --git a/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/TestObjects/TestContainer2.cs b/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/TestObjects/TestContainer2.cs
new file mode 100644
index 0000000000..3193da3749
--- /dev/null
+++ b/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/TestObjects/TestContainer2.cs
@@ -0,0 +1,8 @@
+namespace Volo.Abp.BlobStoring.TestObjects
+{
+ [BlobContainerName("Test2")]
+ public class TestContainer2
+ {
+
+ }
+}
\ No newline at end of file
diff --git a/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/TestObjects/TestContainer3.cs b/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/TestObjects/TestContainer3.cs
new file mode 100644
index 0000000000..b96a71c1b7
--- /dev/null
+++ b/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/TestObjects/TestContainer3.cs
@@ -0,0 +1,7 @@
+namespace Volo.Abp.BlobStoring.TestObjects
+{
+ public class TestContainer3
+ {
+
+ }
+}
\ No newline at end of file
diff --git a/nupkg/common.ps1 b/nupkg/common.ps1
index 8f1fb99536..d056ae032d 100644
--- a/nupkg/common.ps1
+++ b/nupkg/common.ps1
@@ -55,6 +55,8 @@ $projects = (
"framework/src/Volo.Abp.BackgroundJobs.Quartz",
"framework/src/Volo.Abp.BackgroundWorkers",
"framework/src/Volo.Abp.BackgroundWorkers.Quartz",
+ "framework/src/Volo.Abp.BlobStoring",
+ "framework/src/Volo.Abp.BlobStoring.FileSystem",
"framework/src/Volo.Abp.Caching",
"framework/src/Volo.Abp.Castle.Core",
"framework/src/Volo.Abp.Cli.Core",