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.
5 Answer(s)
-
0
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
- On the UI, when re-uploading after deletion, do not fire parallel HTTP requests to
POST /api/app/chat-attachment/upload. - 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.
- 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).
- On the UI, when re-uploading after deletion, do not fire parallel HTTP requests to
Server-side mitigation (if you can customize/override the Chat Attachment app service)
- Wrap the upload logic with an application-level lock keyed by the logical aggregate being updated (e.g.,
conversationIdormessageId), so only one upload modifies that aggregate at a time. - 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.
- Wrap the upload logic with an application-level lock keyed by the logical aggregate being updated (e.g.,
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/uploadconcurrency 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) -
0
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), whilePOST /api/app/chat-attachment/uploadis 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:- The upload and delete methods behind
ChatAttachmentAppService - The complete stack trace from your application logs, including inner exceptions
- Your ABP version, UI type and database provider
- Whether the upload uses File Management or calls
IBlobContainerdirectly, and which BLOB provider is configured - 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
DeleteMessageevent carrying the deleted message id to the target user's SignalR clients throughIRealTimeChatMessageSender, and deleting a conversation pushesDeleteConversation. 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
ReceiveMessageonly, 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) - The upload and delete methods behind
-
0
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 MBprivate 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) -
0
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 extraUPDATE ... SET ConcurrencyStamp = @new WHERE Id = @id AND ConcurrencyStamp = @oldon 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
ConfigureServicesof 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
UpdateAggregateRootWhenNavigationChangesoff 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
DeleteMessagewith the deleted message id to the target user's clients, so you can handle it on the hub connection you already use forReceiveMessage:connection.on('DeleteMessage', messageId => { // remove the message from your local state });DeleteConversationis 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) -
0
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
IgnoredUpdateAggregateRootSelectorsconfiguration 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)