Open Closed

Chat File delete Issue #10839


User avatar
0
bpdeveloper created

The chat module currently does not provide a SignalR event for message deletion. Issue when upload attachments on the chat module: Endpoint: https://localhost:44371/api/app/chat-attachment/upload Payload: file (binary) Error Response: { "error": { "code": null, "message": "The data you have submitted has already been changed by another user. Discard your changes and try again.", "details": null, "data": null, "validationErrors": null } } When I delete multiple uploaded images and immediately try to upload the same images again at the same time, the API returns the error shown above. However, if I delete the images and then re-upload them one by one, the upload works correctly without any errors.

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

5 Answer(s)
  • User Avatar
    0
    AI-Bot created
    Support Team ABP Agent
    • This error message is ABP’s concurrency exception (optimistic concurrency):

      “The data you have submitted has already been changed by another user…”
      In your scenario it happens because multiple upload requests are hitting the same “chat attachment aggregate / message state” very close to a delete operation, so the upload flow tries to update an entity row with an outdated concurrency stamp / row version.
      When you upload the same images “at the same time” (parallel requests), you increase the chance that two uploads update the same DB row, so one succeeds and the other(s) fail with concurrency. Uploading one-by-one serializes the updates, so you don’t see the conflict.

    • Workaround / fix on your side (no framework modification): serialize uploads per chat message/conversation

      1. On the UI, when re-uploading after deletion, do not fire parallel HTTP requests to POST /api/app/chat-attachment/upload.
      2. Upload sequentially (queue) OR send them as a single request if you have that option.
        • If you control the client code, the simplest approach is an “upload queue” with concurrency = 1 for that specific chat thread/message.
      3. Also add a small delay (e.g., 250–500ms) after delete completes before starting uploads (helps if delete triggers additional DB updates/events in background).
    • Server-side mitigation (if you can customize/override the Chat Attachment app service)

      1. Wrap the upload logic with an application-level lock keyed by the logical aggregate being updated (e.g., conversationId or messageId), so only one upload modifies that aggregate at a time.
      2. Alternatively, implement a retry policy when the exception is an AbpDbConcurrencyException / DbUpdateConcurrencyException (reload state and retry a limited number of times). This is a common mitigation for optimistic concurrency when parallel writes are expected.
    • About “no SignalR event for message deletion” If you need real-time deletion sync, the typical approach is to extend the module by publishing your own distributed/local event on delete and broadcasting via your own SignalR hub (or extending the existing hub if the module allows). There’s no verified official doc/source in the available context for a built-in “message deleted” SignalR event for the Chat module.

    (No links provided because I couldn’t retrieve any official/verified document specific to api/app/chat-attachment/upload concurrency behavior from the available tools’ indexed sources.)


    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,

    There are two separate issues here.

    Attachments: the Chat module doesn't provide an attachment API. Its HTTP APIs are all under api/chat/... (conversation, contact, settings), while POST /api/app/chat-attachment/upload is an endpoint in your own application.

    The response you get is ABP's default client message for AbpDbConcurrencyException, meaning an optimistic concurrency check failed while saving. It doesn't say which record was involved, and the message text is the same for every entity, so we need a bit more to pin it down:

    1. The upload and delete methods behind ChatAttachmentAppService
    2. The complete stack trace from your application logs, including inner exceptions
    3. Your ABP version, UI type and database provider
    4. Whether the upload uses File Management or calls IBlobContainer directly, and which BLOB provider is configured
    5. Whether all delete requests return before any upload request is sent, or whether they overlap in the browser's Network tab

    With those we can point at the exact entity or repository call that conflicts. Until then, re-uploading the files one by one keeps it working, as you already found.

    Message deletion event: the server already sends one. Deleting a message pushes a DeleteMessage event carrying the deleted message id to the target user's SignalR clients through IRealTimeChatMessageSender, and deleting a conversation pushes DeleteConversation. The MVC and Blazor clients handle both.

    So no custom server-side event is needed. The Angular client is where the gap is: it registers ReceiveMessage only, so an already-open conversation on the other side does not remove the deleted message in real time. Let us know your UI type and we'll follow up on the Angular side.

    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

    With help of below ticket we have implemented the extended code to attach file with chat Our Support Ticket

    We have used controller `using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Volo.Abp.AspNetCore.Mvc; using Volo.Abp.BlobStoring;

    namespace Elephant.ChatAttachment;

    ///

    /// HTTP API for chat file attachments. /// Upload returns a blob ID that the client embeds inside a standard chat message /// as a JSON token: [ATTACHMENT:{"blobId":"...","fileName":"...","mimeType":"...","size":N}] /// Download streams the bytes back by blob ID. /// [Authorize] [ApiController] [Route("api/app/chat-attachment")] public class ChatAttachmentController : AbpControllerBase { private const long MaxFileSizeBytes = 20 * 1024 * 1024; // 20 MB

    private static readonly HashSet<string> AllowedMimeTypes = new(StringComparer.OrdinalIgnoreCase)
    {
        // Images
        "image/jpeg", "image/png", "image/gif", "image/webp", "image/bmp", "image/svg+xml",
        // Documents
        "application/pdf",
        "application/msword",
        "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
        "application/vnd.ms-excel",
        "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
        "application/vnd.ms-powerpoint",
        "application/vnd.openxmlformats-officedocument.presentationml.presentation",
        // Archives & generic
        "application/zip",
        "application/x-zip-compressed",
        "text/plain",
        "text/csv",
    };
    
    private readonly IBlobContainer<ChatAttachmentContainer> _blobContainer;
    
    public ChatAttachmentController(IBlobContainer<ChatAttachmentContainer> blobContainer)
    {
        _blobContainer = blobContainer;
    }
    
    /// <summary>
    /// Accepts a single file via multipart/form-data, stores it in the
    /// chat-attachment blob container, and returns the blob metadata.
    /// </summary>
    [HttpPost("upload")]
    [Consumes("multipart/form-data")]
    [RequestSizeLimit(MaxFileSizeBytes + 4096)] // small extra for form overhead
    [ApiExplorerSettings(IgnoreApi = true)]     // Swashbuckle 10.x cannot introspect IFormFile; injected via ChatAttachmentDocumentFilter
    public async Task<ChatAttachmentDto> UploadAsync([FromForm] IFormFile file)
    {
        if (file == null || file.Length == 0)
        {
            throw new Volo.Abp.UserFriendlyException("No file was uploaded.");
        }
    
        if (file.Length > MaxFileSizeBytes)
        {
            throw new Volo.Abp.UserFriendlyException(
                $"File size {file.Length / (1024 * 1024):F1} MB exceeds the 20 MB limit.");
        }
    
        var mimeType = file.ContentType?.ToLowerInvariant() ?? "application/octet-stream";
        if (!AllowedMimeTypes.Contains(mimeType))
        {
            throw new Volo.Abp.UserFriendlyException(
                $"File type '{mimeType}' is not allowed.");
        }
    
        var blobId = Guid.NewGuid().ToString("N");
    
        await using var stream = file.OpenReadStream();
        await _blobContainer.SaveAsync(blobId, stream, overrideExisting: false);
    
        return new ChatAttachmentDto
        {
            BlobId   = blobId,
            FileName = file.FileName,
            MimeType = mimeType,
            Size     = file.Length,
        };
    }
    
    /// <summary>
    /// Streams an uploaded attachment back to the caller.
    /// The blob ID acts as an opaque capability token — any authenticated user
    /// who possesses the ID may download the file.
    /// </summary>
    [HttpGet("{blobId}")]
    public async Task<IActionResult> DownloadAsync(string blobId)
    {
        if (string.IsNullOrWhiteSpace(blobId))
        {
            return BadRequest();
        }
    
        var stream = await _blobContainer.GetOrNullAsync(blobId);
        if (stream == null)
        {
            return NotFound();
        }
    
        // We do not persist the original MIME or filename on the server,
        // so we serve as octet-stream. Clients that know the MIME from the
        // attachment token can render appropriately without relying on this header.
        return File(stream, "application/octet-stream");
    }
    

    } `

    Now we are using these api's in react.js UI .We are storing data in sql table using blob container.

    All the delete request completes successfully Only after the delete request completes , i try to reupload the same file then i get the concurrency error.

    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,

    Thanks for the code, that was enough to track it down.

    Your controller is fine, and the deletes aren't part of it. What breaks is uploading into the same BLOB container in parallel: saving a blob also touches the container row in AbpBlobContainers, because ABP marks an aggregate root as updated when a related row is inserted or deleted. That gives every upload an extra UPDATE ... SET ConcurrencyStamp = @new WHERE Id = @id AND ConcurrencyStamp = @old on the single container row, so with parallel uploads the first one wins and the rest fail with that message. Uploading one by one never overlaps, which is why it works.

    Add this to ConfigureServices of your module, no change to your upload code:

    using Volo.Abp;
    using Volo.Abp.BlobStoring.Database;
    using Volo.Abp.Domain.Entities.Events;
    
    Configure<AbpEntityChangeOptions>(options =>
    {
        options.IgnoredUpdateAggregateRootSelectors.Add(
            new NamedTypeSelector(
                "DatabaseBlobContainer",
                type => type == typeof(DatabaseBlobContainer)));
    });
    

    That keeps the container row out of the aggregate-root update, and parallel uploads go through. Don't turn UpdateAggregateRootWhenNavigationChanges off globally, other modules rely on it. We'll handle this on the framework side so the container row is no longer written on every blob save.

    For the deletion event on your React UI: the server already pushes DeleteMessage with the deleted message id to the target user's clients, so you can handle it on the hub connection you already use for ReceiveMessage:

    connection.on('DeleteMessage', messageId => {
      // remove the message from your local state
    });
    

    DeleteConversation is pushed the same way when a conversation is deleted.

    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,

    This turned out to be a framework issue, not something in your code. Saving a BLOB also updated the container row, because a foreign key only relation was treated as a navigation change of the aggregate root. Every upload and delete wrote that single container row, so parallel uploads into the same container collided on it.

    The fix is in https://github.com/abpframework/abp/pull/25937 and will be available with 10.7. The IgnoredUpdateAggregateRootSelectors configuration from the previous answer is safe to keep until you upgrade, and you can drop it after that.

    We've refunded your ticket.

    Thanks

    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 October 06, 2026, 12:23
1
ABP Assistant
🔐 You need to be logged in to use the chatbot. Please log in first.