| name | store-blobs |
| description | Save, read and delete named binary objects (BLOBs) in an ABP app through the storage-agnostic IBlobContainer abstraction. USE FOR: storing/reading profile pictures, uploads, attachments or other binary data via IBlobContainer (default or typed), defining typed containers, configuring providers (file system, database, Azure, AWS/S3, MinIO), per-tenant BLOB isolation. DO NOT USE FOR: embedding static js/css/image/localization assets into an assembly or overriding a module's embedded files (use manage-virtual-files); building a full file-management UI with folders/permissions/sharing (blob storing is intentionally not that).
|
| license | MIT |
Storing BLOBs with ABP Blob Storing
ABP's Blob Storing system is a storage-agnostic abstraction for saving, reading and deleting named binary objects (BLOBs). You write against IBlobContainer and pick a provider (file system, database, Azure, AWS/S3, MinIO, ...) via configuration, so the application code never changes when you switch storage.
Package: Volo.Abp.BlobStoring (AbpBlobStoringModule). Add it with abp add-package Volo.Abp.BlobStoring or a PackageReference plus [DependsOn(typeof(AbpBlobStoringModule))].
When to Use
- Saving/reading/deleting binary files (profile pictures, uploads, attachments) without touching the file system or a provider SDK directly.
- Defining a typed container (
IBlobContainer<T>) for a specific kind of BLOB.
- Selecting or switching a storage provider (file system, database, Azure, AWS/S3, MinIO) through configuration.
- Controlling per-tenant BLOB isolation.
When Not to Use
- Embedding static assets (js/css/images/localization) into an assembly or overriding a depended-on module's embedded files — use the manage-virtual-files skill.
- Building a file-management system with folders, permissions or sharing — blob storing is deliberately not that; build such features on top.
How it works
The default container
Inject IBlobContainer to use the built-in default container (its name is default). SaveAsync takes a Stream by default; an extension method accepts byte[].
using Volo.Abp.BlobStoring;
using Volo.Abp.DependencyInjection;
public class MyService : ITransientDependency
{
private readonly IBlobContainer _blobContainer;
public MyService(IBlobContainer blobContainer)
{
_blobContainer = blobContainer;
}
public async Task SaveBytesAsync(byte[] bytes)
{
await _blobContainer.SaveAsync("my-blob-1", bytes, overrideExisting: true);
}
public async Task<byte[]?> GetBytesAsync()
{
return await _blobContainer.GetAllBytesOrNullAsync("my-blob-1");
}
}
A BLOB name is an arbitrary string that must be unique per container (and per tenant, see below).
Core methods (on IBlobContainer)
SaveAsync(name, stream, overrideExisting = false, cancellationToken) — save/replace. With overrideExisting: false it throws if the name already exists. An extension overload takes byte[] instead of Stream.
GetAsync(name) — returns a Stream; throws if the BLOB is missing. Always dispose the stream.
GetOrNullAsync(name) — like GetAsync but returns null when missing.
GetAllBytesAsync(name) — extension method returning byte[]; throws if missing.
GetAllBytesOrNullAsync(name) — extension method returning byte[]?; null when missing.
DeleteAsync(name) — returns true if it actually deleted something, false if the name was not found. Does not throw when missing.
ExistsAsync(name) — returns bool.
Typed containers
For anything beyond the default container — and always in reusable modules — define a typed container. Decorate a marker class with [BlobContainerName] and inject IBlobContainer<T>, which exposes the same methods.
[BlobContainerName("profile-pictures")]
public class ProfilePictureContainer { }
[Authorize]
public class ProfileAppService : ApplicationService
{
private readonly IBlobContainer<ProfilePictureContainer> _blobContainer;
public ProfileAppService(IBlobContainer<ProfilePictureContainer> blobContainer)
{
_blobContainer = blobContainer;
}
public async Task SaveAsync(byte[] bytes)
{
await _blobContainer.SaveAsync(CurrentUser.GetId().ToString(), bytes, overrideExisting: true);
}
}
Without [BlobContainerName] the container name falls back to the class full name, so always add the attribute to keep the name stable across renames. Typed containers are just named containers; IBlobContainerFactory.Create("profile-pictures") / Create<ProfilePictureContainer>() resolves the same thing by name or type.
Configuring providers
AbpBlobStoringOptions.Containers holds per-container configuration. Configure it in ConfigureServices. Each container selects a provider through a Use... extension on BlobContainerConfiguration.
Every provider — including the file system one — lives in its own package. For UseFileSystem add Volo.Abp.BlobStoring.FileSystem and depend on AbpBlobStoringFileSystemModule; the extension is in the Volo.Abp.BlobStoring.FileSystem namespace:
using Volo.Abp.BlobStoring;
using Volo.Abp.BlobStoring.FileSystem;
[DependsOn(
typeof(AbpBlobStoringModule),
typeof(AbpBlobStoringFileSystemModule)
)]
public class MyModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpBlobStoringOptions>(options =>
{
options.Containers.Configure<ProfilePictureContainer>(container =>
{
container.UseFileSystem(fileSystem =>
{
fileSystem.BasePath = "C:\\my-files";
});
});
});
}
}
Configure<TContainer>(...) / Configure("name", ...) — one specific container. Called again for the same container, it mutates the same configuration, so a later call can override an earlier one.
ConfigureDefault(...) — the default container. A container without its own configuration falls back to the default, so this sets a baseline for all containers.
ConfigureAll((name, config) => ...) — runs the action once over the containers that already exist at that moment. It is not a permanent global rule: it doesn't touch containers configured afterwards, and a later Configure<T>(...) still overrides what it set.
Provider is chosen by the Use... call. Each provider ships in its own package and is a variant of the same shape (add the package + configure credentials/endpoint):
UseFileSystem(...) — Volo.Abp.BlobStoring.FileSystem
UseDatabase(...) — Volo.Abp.BlobStoring.Database
UseAzure(...) — Volo.Abp.BlobStoring.Azure
UseAws(...) — Volo.Abp.BlobStoring.Aws (also S3-compatible: MinIO, R2, Spaces, ... via ServiceURL)
UseMinio(...) — Volo.Abp.BlobStoring.Minio
Blob storing does nothing until a provider is configured for the container you use.
Mainstream storage providers
Each provider ships in its own package: add the package, depend on its module, and select it with the matching Use... call on the container. The blocks below configure the default container (ConfigureDefault), so every container without its own configuration uses it. The File System example above shows the same shape for a typed container.
Azure — package Volo.Abp.BlobStoring.Azure:
[DependsOn(typeof(AbpBlobStoringModule), typeof(AbpBlobStoringAzureModule))]
public class MyModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
Configure<AbpBlobStoringOptions>(options =>
{
options.Containers.ConfigureDefault(container =>
{
container.UseAzure(azure =>
{
azure.ConnectionString = configuration["Blob:Azure:ConnectionString"];
azure.ContainerName = "my-container";
azure.CreateContainerIfNotExists = true;
});
});
});
}
}
AWS S3 — package Volo.Abp.BlobStoring.Aws. The S3 bucket is set via ContainerName (there is no Bucket property); point ServiceURL at an S3-compatible endpoint for MinIO/R2/Spaces:
[DependsOn(typeof(AbpBlobStoringModule), typeof(AbpBlobStoringAwsModule))]
public class MyModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
Configure<AbpBlobStoringOptions>(options =>
{
options.Containers.ConfigureDefault(container =>
{
container.UseAws(aws =>
{
aws.AccessKeyId = configuration["Aws:AccessKeyId"];
aws.SecretAccessKey = configuration["Aws:SecretAccessKey"];
aws.UseCredentials = true;
aws.Region = "us-east-1";
aws.ContainerName = "my-bucket";
aws.CreateContainerIfNotExists = true;
});
});
});
}
}
Database — package Volo.Abp.BlobStoring.Database; depend on the EF Core (BlobStoringDatabaseEntityFrameworkCoreModule) or MongoDB (BlobStoringDatabaseMongoDbModule) module. UseDatabase() takes no options — it stores BLOBs in your database (uses the AbpBlobStoring connection string if defined, otherwise Default). For EF Core also call builder.ConfigureBlobStoring() in your migration DbContext's OnModelCreating:
[DependsOn(typeof(AbpBlobStoringModule), typeof(BlobStoringDatabaseEntityFrameworkCoreModule))]
public class MyModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpBlobStoringOptions>(options =>
{
options.Containers.ConfigureDefault(container =>
{
container.UseDatabase();
});
});
}
}
MinIO — package Volo.Abp.BlobStoring.Minio:
[DependsOn(typeof(AbpBlobStoringModule), typeof(AbpBlobStoringMinioModule))]
public class MyModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
Configure<AbpBlobStoringOptions>(options =>
{
options.Containers.ConfigureDefault(container =>
{
container.UseMinio(minio =>
{
minio.EndPoint = "localhost:9000";
minio.AccessKey = configuration["Minio:AccessKey"];
minio.SecretKey = configuration["Minio:SecretKey"];
minio.BucketName = "my-bucket";
minio.WithSSL = false;
minio.CreateBucketIfNotExists = true;
});
});
});
}
}
The remaining providers (Aliyun, Google Cloud, Bunny, Memory) follow the same UseXxx pattern and are configured per the ABP docs.
Multi-tenancy
All providers isolate BLOBs per tenant automatically, so the same BLOB name can coexist across tenants. To share a container's BLOBs across all tenants, disable multi-tenancy for it:
Configure<AbpBlobStoringOptions>(options =>
{
options.Containers.Configure<ProfilePictureContainer>(container =>
{
container.IsMultiTenant = false;
});
});
Validation
- Call
SaveAsync then ExistsAsync/GetAllBytesOrNullAsync for the same name and confirm the bytes round-trip. If no provider is registered/configured for that container, resolving the IBlobContainer<T> throws AbpException — the container factory runs the provider selector as it creates the container, so resolution fails before any blob operation runs, rather than silently succeeding.
- With the file system provider, check the configured
BasePath on disk for the written file.
- To confirm per-tenant isolation, save the same BLOB name under two tenants and verify both coexist.
Common Pitfalls
- Blob storing needs a provider registered and configured for the container you use — otherwise the provider selector throws
AbpException while the container is being resolved/created (in the container factory), not silently no-op.
- Don't hardcode provider credentials (Azure connection strings, AWS/MinIO access keys and secrets). Read them from configuration, environment variables, or a secret store — keep them out of source control, command history, and logs.
SaveAsync defaults overrideExisting to false and throws BlobAlreadyExistsException on a name clash; pass overrideExisting: true to replace.
GetAsync / GetAllBytesAsync throw when the BLOB is missing; use the ...OrNull... variants to get null instead.
- Always add
[BlobContainerName] to a typed container's marker class — without it the name falls back to the class full name and breaks on rename.
- A container without its own configuration falls back to the default container's configuration.
- Blob storing is not a file management system (no folders, permissions or sharing) — build those on top if needed.