Great news : )
Hi,
Sure. Those two pieces are what connects a container name to your provider.
The class is only a marker — ABP never instantiates it. It exists so you can write IBlobContainer<CompanyDocumentContainer>; at runtime ABP reads the attribute from that type to get the container name, and that name is what your provider receives in args.ContainerName. Without the attribute the name would be the full type name (MyCompanyName.MyProjectName.Blobs.CompanyDocumentContainer):
[BlobContainerName("CompanyDocument")]
public class CompanyDocumentContainer
{
}
The Configure<AbpBlobStoringOptions> part goes into the ConfigureServices method of your module, next to your other configuration. It says which provider serves that container — without it, the container falls back to the default configuration (the database provider in your case) and the content goes to AbpBlobs as before:
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpBlobStoringOptions>(options =>
{
options.Containers.Configure<CompanyDocumentContainer>(container =>
{
container.ProviderType = typeof(CompanyDocumentBlobProvider);
});
});
}
Note that this configuration only routes the container to your provider — it doesn't create AbpBlobs_CompanyDocument. That table comes from the entity mapping and the migration; the provider is what reads and writes it.
After that you inject the typed container and use it as usual — the name resolves to CompanyDocument, the configuration for that name points to your provider, and the provider writes to AbpBlobs_CompanyDocument:
public class CompanyDocumentAppService : ApplicationService
{
private readonly IBlobContainer<CompanyDocumentContainer> _blobContainer;
public CompanyDocumentAppService(IBlobContainer<CompanyDocumentContainer> blobContainer)
{
_blobContainer = blobContainer;
}
public async Task SaveAsync(string name, byte[] content)
{
await _blobContainer.SaveAsync(name, content);
}
}
One correction to the mapping in my previous answer: the unique index needs HasFilter(null).
b.HasIndex(x => new { x.TenantId, x.Name }).IsUnique().HasFilter(null);
TenantId is nullable, so EF Core otherwise generates that index on SQL Server with a [TenantId] IS NOT NULL filter and host-level rows stay outside the unique constraint. I've updated the previous answer as well.
If you already applied a migration with the old mapping, changing the Fluent API alone won't touch the database — add a new migration so the index is recreated without the filter. Clear out duplicate (TenantId, Name) rows before you apply it, especially host rows where TenantId is null, otherwise SQL Server refuses to create the unique index.
Not about the configuration itself, but since your new table is IMultiTenant: the per-tenant filtering on it comes from ABP's global query filters. If you ever need a filter of your own on top of that, I wrote up how to do it here — the example there switches data by organization unit: https://abp.io/community/articles/switching-between-organization-units-i5tokpzt
Thanks
Hi,
Not with the built-in database provider — it always maps the blob entity to the single AbpBlobs table, and containers only separate the rows logically (through the ContainerId column). So if you just want to keep the areas apart for querying or cleanup, a container per area already does that.
For real separate tables, write a small blob provider per area and point the container to it. Your own entity and table:
using System;
using Volo.Abp;
using Volo.Abp.Auditing;
using Volo.Abp.Domain.Entities;
using Volo.Abp.MultiTenancy;
public class CompanyDocumentBlob : AggregateRoot<Guid>, IMultiTenant
{
public const int MaxNameLength = 256;
public virtual Guid? TenantId { get; protected set; }
public virtual string Name { get; protected set; }
[DisableAuditing]
public virtual byte[] Content { get; protected set; }
protected CompanyDocumentBlob()
{
}
public CompanyDocumentBlob(Guid id, string name, byte[] content, Guid? tenantId)
: base(id)
{
Name = Check.NotNullOrWhiteSpace(name, nameof(name), MaxNameLength);
Content = Check.NotNull(content, nameof(content));
TenantId = tenantId;
}
public virtual void SetContent(byte[] content)
{
Content = Check.NotNull(content, nameof(content));
}
}
Add a DbSet for it in your DbContext — the default repository registration looks at the DbSet properties, so without it IRepository<CompanyDocumentBlob, Guid> won't be registered and the provider fails to resolve:
public DbSet<CompanyDocumentBlob> CompanyDocumentBlobs { get; set; }
And map the table in OnModelCreating (ConfigureByConvention() comes from Volo.Abp.EntityFrameworkCore.Modeling):
builder.Entity<CompanyDocumentBlob>(b =>
{
b.ToTable("AbpBlobs_CompanyDocument");
b.ConfigureByConvention();
b.Property(x => x.Name).IsRequired().HasMaxLength(CompanyDocumentBlob.MaxNameLength);
b.HasIndex(x => new { x.TenantId, x.Name }).IsUnique().HasFilter(null);
});
The provider — inherit BlobProviderBase, not DatabaseBlobProvider, because the provider selector matches by assignability and a subclass of DatabaseBlobProvider could also be picked for the containers you configured with UseDatabase(). IUnitOfWorkEnabled is there so that one provider call (the lookup plus the insert/update) runs in a single unit of work, also when it is triggered outside an ambient one, like from a background job:
using System;
using System.IO;
using System.Threading.Tasks;
using Volo.Abp.BlobStoring;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Guids;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Uow;
public class CompanyDocumentBlobProvider : BlobProviderBase, ITransientDependency, IUnitOfWorkEnabled
{
protected IRepository<CompanyDocumentBlob, Guid> Repository { get; }
protected IGuidGenerator GuidGenerator { get; }
protected ICurrentTenant CurrentTenant { get; }
public CompanyDocumentBlobProvider(
IRepository<CompanyDocumentBlob, Guid> repository,
IGuidGenerator guidGenerator,
ICurrentTenant currentTenant)
{
Repository = repository;
GuidGenerator = guidGenerator;
CurrentTenant = currentTenant;
}
public override async Task SaveAsync(BlobProviderSaveArgs args)
{
var content = await args.BlobStream.GetAllBytesAsync(args.CancellationToken);
var blob = await Repository.FindAsync(x => x.Name == args.BlobName, cancellationToken: args.CancellationToken);
if (blob != null)
{
if (!args.OverrideExisting)
{
throw new BlobAlreadyExistsException($"BLOB '{args.BlobName}' already exists in the container '{args.ContainerName}'!");
}
blob.SetContent(content);
await Repository.UpdateAsync(blob, autoSave: true, cancellationToken: args.CancellationToken);
return;
}
await Repository.InsertAsync(
new CompanyDocumentBlob(GuidGenerator.Create(), args.BlobName, content, CurrentTenant.Id),
autoSave: true,
cancellationToken: args.CancellationToken
);
}
public override async Task<Stream?> GetOrNullAsync(BlobProviderGetArgs args)
{
var blob = await Repository.FindAsync(x => x.Name == args.BlobName, cancellationToken: args.CancellationToken);
return blob == null ? null : new MemoryStream(blob.Content);
}
public override async Task<bool> ExistsAsync(BlobProviderExistsArgs args)
{
return await Repository.FindAsync(x => x.Name == args.BlobName, cancellationToken: args.CancellationToken) != null;
}
public override async Task<bool> DeleteAsync(BlobProviderDeleteArgs args)
{
var blob = await Repository.FindAsync(x => x.Name == args.BlobName, cancellationToken: args.CancellationToken);
if (blob == null)
{
return false;
}
await Repository.DeleteAsync(blob, autoSave: true, cancellationToken: args.CancellationToken);
return true;
}
}
Then point the container to it:
[BlobContainerName("CompanyDocument")]
public class CompanyDocumentContainer
{
}
Configure<AbpBlobStoringOptions>(options =>
{
options.Containers.Configure<CompanyDocumentContainer>(container =>
{
container.ProviderType = typeof(CompanyDocumentBlobProvider);
});
});
The unique index is what stops two concurrent saves of the same name from creating duplicate rows. HasFilter(null) matters there: TenantId is nullable, and by default EF Core generates the unique index on SQL Server with a [TenantId] IS NOT NULL filter, which leaves host-level rows out of the constraint. On PostgreSQL/MySQL two NULLs count as different values in a unique index either way, so index a non-null tenant key instead if you keep host-level blobs there.
Repeat the entity + provider for AccountStatements and any other area (the provider body is the same except for the entity type) — one table per container, since the table has no ContainerId column. Add a migration for the new tables and make sure your DbContext registration has options.AddDefaultRepositories().
One thing to watch: if a container already has blobs in AbpBlobs, the new provider only looks at the new table. Pause uploads/deletes for that container, copy the rows over, check the result, and switch the ProviderType after that. The new table has no ContainerId, so you copy the shared columns only (Id, TenantId, Name, Content, ExtraProperties, ConcurrencyStamp), and in a multi-tenant app the same container name has one AbpBlobContainers row per tenant — cover all of those ContainerId values. If the documents sit in the default container today, don't move the whole container: pick out the rows that are company documents, otherwise you drag every other blob into the new table.
On the calling side, the calls have to go to that named container: inject IBlobContainer<CompanyDocumentContainer> (or resolve it with IBlobContainerFactory.Create("CompanyDocument")). A plain injected IBlobContainer is the default container, so if you save company documents through that one today, switch those calls over. Everything else keeps using the built-in AbpBlobs table.
Thanks
Hi,
hookToQuery re-runs its request callback only when the ListService's internal query stream emits (paging, sorting, filter changes or a listService.get() call), so it doesn't react to signal changes by itself. The way to connect them is to trigger listService.get() from an effect whenever eagleId changes:
import { effect, untracked } from '@angular/core';
constructor() {
effect(() => {
this.eagleId();
untracked(() => this.listService.get());
});
}
You can keep the rest of your component as-is. The request callback reads this.eagleId() on every run, so each refresh picks up the current value. One thing to watch out for: create the effect in the constructor — ngOnInit is not an injection context, so effect there throws NG0203 unless you pass an injector.
With ListService's default 300ms debounce, there is also no duplicate request on the first load — the initial get() from ListService and the effect's first run are debounced into a single request.
Thanks
Hi,
Could you check and share the details below so we can sort this out:
Smart_Ozel_Guvenlik_Hiz_AS organization on abp.io. If you registered with a different email, please register again with your abp.io email.If the license still doesn't show up there, share your abp.io account email here (or send it to liming.ma@volosoft.com if you don't want to post it publicly), and we will check it with the Blazorise team.
Thanks
Hi,
Yes, you can. The database blob provider has its own BlobStoringDbContext with the connection string name AbpBlobStoring. In your current setup, that context resolves to DatabaseB; if AbpBlobStoring isn't configured directly, ABP falls back to the Default connection string — that's how the AbpBlobContainers / AbpBlobs tables ended up there.
To keep them in DatabaseA together with CompanyDocument, let your DatabaseA DbContext take over the blob storing tables (this is the same pattern the ABP microservice template uses):
using Microsoft.EntityFrameworkCore;
using Volo.Abp.BlobStoring.Database;
using Volo.Abp.BlobStoring.Database.EntityFrameworkCore;
using Volo.Abp.Data;
using Volo.Abp.EntityFrameworkCore;
[ConnectionStringName("DatabaseA")] // your existing connection string name
public class CompanyDbContext : AbpDbContext<CompanyDbContext>, IBlobStoringDbContext
{
public DbSet<DatabaseBlobContainer> BlobContainers { get; set; }
public DbSet<DatabaseBlob> Blobs { get; set; }
public CompanyDbContext(DbContextOptions<CompanyDbContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.ConfigureBlobStoring();
}
}
In the EntityFrameworkCore project of that module, reference the Volo.Abp.BlobStoring.Database.EntityFrameworkCore package, add BlobStoringDatabaseEntityFrameworkCoreModule to your module's DependsOn, and add the replace option to your existing AddAbpDbContext<CompanyDbContext> registration:
context.Services.AddAbpDbContext<CompanyDbContext>(options =>
{
options.ReplaceDbContext<IBlobStoringDbContext>();
});
Then move the data:
AbpBlobContainers / AbpBlobs tables in DatabaseA (if you use a separate migrations DbContext for DatabaseA, add the ConfigureBlobStoring() call there too).AbpBlobContainers first, then AbpBlobs (there is a foreign key between them), keeping the original ids.ReplaceDbContext change and verify that existing files can be read and new uploads go to DatabaseA.ConfigureBlobStoring() call from the migrations DbContext that created them there.After that, the blobs live in DatabaseA, and when a blob is saved in the same unit of work as your CompanyDocument changes, they share the same transaction.
Thanks
Hi,
Manual configuration inside the Suite-generated builder.Entity<...>(b => { ... }) blocks is lost when the entity is regenerated — Suite rebuilds those blocks from the entity definition. Adding a second builder.Entity<OutboxJournalEntry>(...) block for the same entity isn't safe either; it can get removed by the regeneration too.
For multiple precision groups, keep the ConfigureConventions default for your most common one (18, 6) and configure the exceptions by entity type and property name, directly in OnModelCreating but outside any Suite-generated builder.Entity<...> block, so it survives regeneration:
var decimalOverrides =
new Dictionary<(Type EntityType, string PropertyName), (int Precision, int Scale)>
{
[(typeof(OutboxJournalEntry), nameof(OutboxJournalEntry.BuyingROE))] = (18, 10),
[(typeof(OutboxJournalEntry), nameof(OutboxJournalEntry.SellingROE))] = (18, 10),
// Add your (10, 2) and (18, 2) properties here in the same way.
};
foreach (var entityType in builder.Model.GetEntityTypes())
{
var clrType = entityType.ClrType;
if (clrType == null)
{
continue;
}
foreach (var property in entityType.GetProperties())
{
if (decimalOverrides.TryGetValue((clrType, property.Name), out var precisionScale))
{
property.SetPrecision(precisionScale.Precision);
property.SetScale(precisionScale.Scale);
}
}
}
We've also added precision/scale settings for decimal properties to Suite's entity designer. This will be available in ABP Suite 10.7, so you'll be able to set these per property in the UI and remove the override map and loop above.
Thanks
Hi,
You can upgrade from 9.3.x directly to the latest stable (currently 10.6.0) in one step, there is no need to install intermediate versions. What matters is applying the migration guides for each version in order (10.0 → 10.6) while you do it: https://abp.io/docs/latest/release-info/migration-guides
The 10.0 guide is the biggest one for you: .NET 9 → .NET 10, EF Core 10 and OpenIddict 6 → 7. The others (10.1 - 10.6) are smaller, but a few of them matter for a microservice solution.
Things that will actually need attention in your setup:
Backend
OpenIddictToken entity, and the inbox/outbox IncomingEventRecord entity also changed in 10.0. Add a new EF Core migration for each affected DbContext / service database after the package upgrade and remove the ones that come out empty. The OpenIddict migration lands in your auth server database.public DbSet<UserInvitation> UserInvitations { get; set; } to your Identity DbContext. Both need migrations.integration-api/permission-management/permissions/is-granted) changed from GET to POST in 10.3, and there is no GET fallback. Cross-service permission checks use this endpoint, so a 9.3 service calling a 10.x service (or the reverse) will fail. Don't do a service-by-service rollout — upgrade everything in one go, within a maintenance window or as a blue/green switch, so old and new instances don't call each other.Microsoft.OpenApi 2.x) changed the filter signatures, see the 10.1 guide.Volo.Abp.AutoMapper is still there and pins the free AutoMapper 14. Migrating your own mappings to Mapperly is optional, see the AutoMapper section in the 10.0 guide: https://abp.io/docs/latest/release-info/migration-guides/abp-10-0client_credentials requests now forward the incoming access token to downstream services in 10.6. Re-test login, 2FA, password reset and machine-to-machine flows.Angular
provideZoneChangeDetection(), tsconfig updates), add the @angular/aria package from 10.2, then follow the Angular 22 guide: https://abp.io/docs/latest/release-info/migration-guides/abp-10-6-angular-22@abp/* / @volo/* / LeptonX versions — check your shared/singleton version constraints in the federation config. Plan most of your UI effort here.For the solution review: create a private GitHub repository with your solution (or the relevant modules) and invite https://github.com/maliming — remove license codes, connection strings and other secrets first. We'll do a high-level pass over the structure and key customizations (module dependencies, EF Core configuration, shared contracts, auth server and gateway customizations) and point out upgrade blockers and any major drift from the recommended microservice patterns. If GitHub isn't an option, a zip via wetransfer to liming.ma@volosoft.com also works.
Suggested order for the upgrade itself:
Thanks
Hi,
In the microservice solution, each microservice writes its own permission definitions into the Administration database when it starts, using the AdministrationService connection string. This runs in a background task: if saving fails, the service still completes startup and keeps serving requests, and the failure only shows up in its logs. Clearing Redis doesn't help here because the cache is not what writes these rows.
Since it works on localhost but not in AKS, check two things for the new microservice:
The effective environment of the running pod (kubectl describe pod or kubectl exec ... env): ConnectionStrings__AdministrationService must point to the Administration database used in AKS. The built-in services all get it from their helm charts (etc/k8s/.../charts/<service>/templates/<service>-deployment.yaml). If the new service's chart doesn't set it, the pod uses the appsettings.json value baked into the image (localhost), which is not reachable inside the cluster.
The new microservice pod's logs, from startup onwards - search for errors from Volo.Abp.PermissionManagement and SQL connection errors. Note the save is retried with increasing delays for about an hour after startup, so check the full log, not just the first minutes.
After fixing the connection string, restart the new microservice pods so the permission definitions are saved, then restart the Administration service - its startup seeder grants the new permissions to the admin role.
If both look fine, share the logs with us (you can send them to liming.ma@volosoft.com if they're too large for the thread) and we'll dig further.
Thanks