Open Closed

Changes in Chat and File management modules #10826


User avatar
0
bpdeveloper created

In a modular monolith abp project we have added Chat and File management modules and both working fine. we have used sadcn template in UI with react , we don't have attachment option in chat module by default, we would like to have attachment option with chat and print screen image attach options in chat message window. How can we do that ?, please guide us.

Also please give suggestion on how can we attach any file against Entity , suppose we have Company Enity and i want to attach a company related documents with enity, is this possible here in abp framework project? Thanks

Markdown supported.
Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)

10 Answer(s)
  • User Avatar
    0
    AI-Bot created
    Support Team ABP Agent
    • Chat module “attachments in messages” (files / screenshots) is not provided out-of-the-box in the ABP Chat module UI/API. To implement it, you need to extend your solution (your own entity/table + API + React UI) and optionally reuse ABP’s streaming upload patterns and/or the File Management module for storage.

    1) Add file/screenshot attachments to Chat messages (recommended approach)

    1. Create your own domain entity to represent an attachment linked to a chat message (or linked to a chat “conversation + messageId”):

      • Example fields:
        • Id
        • ChatMessageId (or ConversationId + MessageId depending on your integration point)
        • FileName, MimeType, Size
        • BlobName / BlobId (where the binary is stored)
        • CreatorId, CreationTime
    2. Store the binary using ABP’s standard streaming upload style:

      • Add an application service method that accepts IRemoteStreamContent and saves the stream (file) into your storage (blob storage, file system, database—depending on your setup).
      • ABP’s recommended way for file upload/download from application services is IRemoteStreamContent.
    3. Extend the chat send message workflow

      • UI flow (React):
        1. Upload file/screenshot to your backend first (get back an attachmentId or fileId)
        2. Send the chat message text plus a list of attachment ids
      • Backend flow:
        1. Save the message using existing Chat module logic (or your own orchestration)
        2. Create attachment records linked to that message
    4. Screenshots (print screen) in React

      • Implement UI capture via browser APIs (e.g., paste from clipboard or screen capture), then upload as a normal file:
        • Convert pasted image / captured blob to a File object
        • POST it as multipart/form-data to your upload endpoint
      • This is purely a UI concern; on the ABP side it’s the same upload pipeline.
    5. Display attachments in the message window

      • When listing messages, also return attachment metadata (file name, mime type, download url/id).
      • For images, render inline previews; for other files, show a download link.

    2) Attach any file against a business entity (e.g., Company documents)

    Yes, this is possible and is a common pattern in ABP projects. You generally implement:

    1. A “document/attachment” entity (e.g., CompanyDocument) with:

      • CompanyId
      • FileName, MimeType, Size
      • BlobId/BlobName (or a reference to File Management module object if you choose)
      • Audit fields (creator/time)
    2. Upload endpoint (Application Service) using IRemoteStreamContent:

      • Upload file -> save binary -> create CompanyDocument row linked to CompanyId.
    3. List / download / delete endpoints:

      • GetCompanyDocuments(companyId)
      • Download(documentId) returning IRemoteStreamContent
      • Delete(documentId)

    Sources

    • https://abp.io/docs/latest/Application-Services#working-with-streams
    • https://abp.io/docs/latest/Customizing-Application-Modules-Guide
    • https://abp.io/docs/latest/modules/chat
    • https://abp.io/community/articles/que8cdr8

    This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    Hi,

    Both are possible, but neither is built-in, so you need a small amount of custom code on the server side. Since your UI is a custom React app, you already own the client part; here is the server part.

    Chat attachments (files / screenshots)

    The chat module's Message entity supports extra properties, so you can link a file to a message without modifying the module:

    1. Upload the file (or the pasted screenshot blob) from React first and get a file id back. Chat users usually don't have the file management permissions, so wrap your own upload endpoint around IFileManager.CreateAsync (pass tenantId: CurrentTenant.Id and overrideExisting: false) and apply your own authorization, size/MIME and storage quota checks there. Use unique file names (e.g. a GUID prefix) — pasted screenshots tend to reuse names like image.png, and uploading the same name into the same directory with overrideExisting replaces the content of previously sent attachments.

    2. Send the message through your own application service. It keeps the chat module's permission checks, verifies the sender owns the attached file, and saves the attachment id in the same transaction:

    using System;
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    using System.Linq;
    using System.Threading.Tasks;
    using Microsoft.AspNetCore.Authorization;
    using Volo.Abp;
    using Volo.Abp.Application.Services;
    using Volo.Abp.Authorization;
    using Volo.Abp.Data;
    using Volo.Abp.Features;
    using Volo.Abp.PermissionManagement;
    using Volo.Abp.Uow;
    using Volo.Abp.Users;
    using Volo.Abp.Validation;
    using Volo.Chat;
    using Volo.Chat.Authorization;
    using Volo.Chat.Conversations;
    using Volo.Chat.Messages;
    using Volo.Chat.Users;
    using Volo.FileManagement.Files;
    
    namespace MyCompanyName.MyProjectName.Chat;
    
    public class SendMessageWithAttachmentInput
    {
        public Guid TargetUserId { get; set; }
    
        [Required]
        [DynamicStringLength(typeof(ChatMessageConsts), nameof(ChatMessageConsts.MaxTextLength), nameof(ChatMessageConsts.MinTextLength))]
        public string Message { get; set; }
    
        public Guid? AttachmentFileId { get; set; }
    }
    
    [RequiresFeature(ChatFeatures.Enable)]
    [Authorize(ChatPermissions.Messaging)]
    public class ChatAttachmentAppService : ApplicationService
    {
        private readonly MessagingManager _messagingManager;
        private readonly IChatUserLookupService _chatUserLookupService;
        private readonly IMessageRepository _messageRepository;
        private readonly IRealTimeChatMessageSender _realTimeChatMessageSender;
        private readonly IPermissionFinder _permissionFinder;
        private readonly IFileDescriptorRepository _fileDescriptorRepository;
    
        public ChatAttachmentAppService(
            MessagingManager messagingManager,
            IChatUserLookupService chatUserLookupService,
            IMessageRepository messageRepository,
            IRealTimeChatMessageSender realTimeChatMessageSender,
            IPermissionFinder permissionFinder,
            IFileDescriptorRepository fileDescriptorRepository)
        {
            _messagingManager = messagingManager;
            _chatUserLookupService = chatUserLookupService;
            _messageRepository = messageRepository;
            _realTimeChatMessageSender = realTimeChatMessageSender;
            _permissionFinder = permissionFinder;
            _fileDescriptorRepository = fileDescriptorRepository;
        }
    
        public virtual async Task<Guid> SendMessageAsync(SendMessageWithAttachmentInput input)
        {
            var targetUser = await _chatUserLookupService.FindByIdAsync(input.TargetUserId);
            if (targetUser == null)
            {
                throw new BusinessException("Volo.Chat:010002");
            }
    
            if (!await _permissionFinder.IsGrantedAsync(targetUser.Id, ChatPermissions.Messaging))
            {
                throw new BusinessException("Volo.Chat:010004");
            }
    
            if (!await AuthorizationService.IsGrantedAsync(ChatPermissions.Searching) &&
                !await _messagingManager.HasConversationAsync(targetUser.Id))
            {
                throw new AbpAuthorizationException(code: AbpAuthorizationErrorCodes.GivenRequirementHasNotGrantedForGivenResource);
            }
    
            if (input.AttachmentFileId.HasValue)
            {
                // Only allow attaching files uploaded by the sender.
                var file = await _fileDescriptorRepository.GetAsync(input.AttachmentFileId.Value);
                if (file.CreatorId != CurrentUser.GetId())
                {
                    throw new AbpAuthorizationException(code: AbpAuthorizationErrorCodes.GivenRequirementHasNotGrantedForGivenResource);
                }
            }
    
            Message message;
            using (var uow = UnitOfWorkManager.Begin(requiresNew: true, isTransactional: true))
            {
                message = await _messagingManager.CreateNewMessage(
                    CurrentUser.GetId(),
                    targetUser.Id,
                    input.Message
                );
    
                if (input.AttachmentFileId.HasValue)
                {
                    message.SetProperty("AttachmentFileId", input.AttachmentFileId.Value);
                    await _messageRepository.UpdateAsync(message);
                }
    
                await uow.CompleteAsync();
            }
    
            var senderUser = await _chatUserLookupService.FindByIdAsync(CurrentUser.GetId());
            await _realTimeChatMessageSender.SendAsync(
                targetUser.Id,
                new ChatMessageRdto
                {
                    Id = message.Id,
                    SenderName = senderUser.Name,
                    SenderSurname = senderUser.Surname,
                    SenderUserId = senderUser.Id,
                    SenderUsername = senderUser.UserName,
                    Text = input.Message
                }
            );
    
            return message.Id;
        }
    
        public virtual async Task<List<ChatMessageWithAttachmentDto>> GetConversationAsync(GetConversationInput input)
        {
            var messages = await _messagingManager.ReadMessagesAsync(input.TargetUserId, input.SkipCount, input.MaxResultCount);
    
            return messages.Select(x => new ChatMessageWithAttachmentDto
            {
                Id = x.Message.Id,
                Message = x.Message.Text,
                MessageDate = x.Message.CreationTime,
                Side = x.UserMessage.Side,
                AttachmentFileId = x.Message.GetProperty<Guid?>("AttachmentFileId")
            }).ToList();
        }
    }
    
    public class ChatMessageWithAttachmentDto
    {
        public Guid Id { get; set; }
    
        public string Message { get; set; }
    
        public DateTime MessageDate { get; set; }
    
        public ChatMessageSide Side { get; set; }
    
        public Guid? AttachmentFileId { get; set; }
    }
    
    1. For real-time updates, keep using the module's built-in ReceiveMessage SignalR event as a "new message" notification on the React side, then call your GetConversationAsync endpoint to load the messages including the attachment ids. This way you don't need a second real-time DTO. Note that ReceiveMessage only goes to the receiver; the sender updates its own view after SendMessageAsync returns.

    2. For downloading, the chat receiver usually doesn't have the file management permissions, so expose your own download endpoint: check that the current user belongs to the conversation and the file is attached to a message in it, then return the file content.

    A few notes:

    • The message text is required by the module, so an attachment-only message still needs a caption text.
    • If you need multiple attachments per message, create your own link entity (e.g. ChatMessageAttachment { MessageId, FileDescriptorId }) instead of the single extra property.
    • The chat module doesn't know about your files: sending can fail after the upload, and deleting a message or a conversation only deletes the chat data. Clean up the unreferenced files yourself (on failure/deletion, or with a periodic job).
    • The file management APIs/UI don't know about your references either — a file or directory delete there doesn't check whether a message still points to the file. Keep the attachment files out of the regular file management UI (e.g. a dedicated directory) and delete them only through your own service.

    Attaching files to an entity (Company)

    The file management module is a plain directory/file tree; there is no built-in "attach file to entity" concept. The standard approach is a link entity in your own code:

    using System;
    using Volo.Abp.Domain.Entities.Auditing;
    using Volo.Abp.MultiTenancy;
    
    namespace MyCompanyName.MyProjectName.Companies;
    
    public class CompanyDocument : AuditedAggregateRoot<Guid>, IMultiTenant
    {
        public virtual Guid? TenantId { get; protected set; }
    
        public virtual Guid CompanyId { get; protected set; }
    
        public virtual Guid FileDescriptorId { get; protected set; }
    
        public virtual string DocumentType { get; set; }
    
        protected CompanyDocument()
        {
        }
    
        public CompanyDocument(Guid id, Guid companyId, Guid fileDescriptorId, Guid? tenantId)
            : base(id)
        {
            CompanyId = companyId;
            FileDescriptorId = fileDescriptorId;
            TenantId = tenantId;
        }
    }
    

    Store only the FileDescriptorId (don't add EF navigations to the file management entities), check the current user's access to both the company and the file in your endpoints (when creating a link, but also when listing/downloading/deleting — the file management APIs only check their own permissions, they don't know about the company), and manage the lifecycle in your own code, e.g. delete the file when the last CompanyDocument referencing it is deleted. If you don't need the file management UI at all, you can also skip the module and store the files directly with the BLOB storing system (IBlobContainer), keeping the blob name on your entity: https://abp.io/docs/latest/framework/infrastructure/blob-storing

    An alternative is one directory per company (created via IDirectoryDescriptorAppService.CreateAsync), keeping the directory id on the Company. This lets you reuse the file management APIs directly, but note that deleting a directory recursively deletes all files in it.

    Thanks

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    bpdeveloper created

    Hi, I am able to save document using IBlobContainer I kept [FileDescriptors] and CompanyDocument in one databaseA and [AbpBlobContainers] , [AbpBlobs] is in another database say DatabaseB. As per my understanding AbpBlobs contains the file data which is now in DatabaseB. can we cuztomize this and have the main table which has content in DatabaseA only with companyDocument table? If you want to know or asking more about this then please let me know. Thanks

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    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:

    1. Back up DatabaseB and stop file uploads/deletes during the switch.
    2. Add a migration to create the AbpBlobContainers / AbpBlobs tables in DatabaseA (if you use a separate migrations DbContext for DatabaseA, add the ConfigureBlobStoring() call there too).
    3. Copy the existing rows from DatabaseB — AbpBlobContainers first, then AbpBlobs (there is a foreign key between them), keeping the original ids.
    4. Deploy the ReplaceDbContext change and verify that existing files can be read and new uploads go to DatabaseA.
    5. Keep the old tables in DatabaseB for a while as a rollback option; once everything is stable, drop them and remove the 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

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    bpdeveloper created

    Thanks For your help i am able to do the changes at my end. One more thing i want to ask on this point the AbpBlobs table which contains the main binary data of files. can we make it multiple like AbpBlobs_CompanyDocument , AbpBlobs_AccountStatements etc like this?

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    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

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    bpdeveloper created

    can you please explain this section Then point the container to it:

    [BlobContainerName("CompanyDocument")] public class CompanyDocumentContainer { }

    Configure<AbpBlobStoringOptions>(options => { options.Containers.Configure<CompanyDocumentContainer>(container => { container.ProviderType = typeof(CompanyDocumentBlobProvider); }); });

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    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

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    bpdeveloper created

    Thanks for your help i am able to do this now,

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    Great news : )

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
Boost Your Development
ABP Live Training
Packages
See Trainings
Mastering ABP Framework Book
The Official Guide
Mastering
ABP Framework
Learn More
Mastering ABP Framework Book
Made with ❤️ on ABP v10.8.0-preview. Updated on September 21, 2026, 06:18
1
ABP Assistant
🔐 You need to be logged in to use the chatbot. Please log in first.