From 85ddc6396ea1669f6b04641066021c24b22c8e6e Mon Sep 17 00:00:00 2001 From: Alexey Shibanov <83034617+alexeyshibanov@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:13:45 +0300 Subject: [PATCH 1/4] fix: sync DB-generated values back to original models in CrudService.SaveChangesAsync Co-Authored-By: Claude Opus 4.6 --- src/VirtoCommerce.Platform.Data/GenericCrud/CrudService.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/VirtoCommerce.Platform.Data/GenericCrud/CrudService.cs b/src/VirtoCommerce.Platform.Data/GenericCrud/CrudService.cs index b340033ceab..6b47af1561c 100644 --- a/src/VirtoCommerce.Platform.Data/GenericCrud/CrudService.cs +++ b/src/VirtoCommerce.Platform.Data/GenericCrud/CrudService.cs @@ -226,7 +226,10 @@ public virtual async Task SaveChangesAsync(IList models) foreach (var (changedEntry, i) in changedEntries.Select((x, i) => (x, i))) { - changedEntry.NewEntry = ToModel(changedEntities[i]); + // Update the original model in place instead of creating a new one. + // This ensures DB-generated values are synced back to the original model objects + // passed to SaveChangesAsync. + changedEntities[i].ToModel(changedEntry.NewEntry); } await AfterSaveChangesAsync(models, changedEntries); From 8dff15c6800fba0f52b9ad19c9726b1aa6480bf6 Mon Sep 17 00:00:00 2001 From: Alexey Shibanov <83034617+alexeyshibanov@users.noreply.github.com> Date: Wed, 11 Mar 2026 13:20:45 +0300 Subject: [PATCH 2/4] fix: route post-save model sync through virtual ToModel method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change ToModel signature to accept optional target model parameter. When target is provided, updates it in-place; when null, creates new instance (preserving existing behavior for GetAsync and change tracking). This is a breaking change for CrudService overrides of ToModel(entity) — they must update to the new signature ToModel(entity, model). Co-Authored-By: Claude Opus 4.6 --- .../GenericCrud/CrudService.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/VirtoCommerce.Platform.Data/GenericCrud/CrudService.cs b/src/VirtoCommerce.Platform.Data/GenericCrud/CrudService.cs index 6b47af1561c..1a5817df812 100644 --- a/src/VirtoCommerce.Platform.Data/GenericCrud/CrudService.cs +++ b/src/VirtoCommerce.Platform.Data/GenericCrud/CrudService.cs @@ -75,7 +75,7 @@ protected virtual async Task> GetByIdsNoCache(IList ids, s { using var repository = _repositoryFactory(); - // Disable DBContext change tracking for better performance + // Disable DBContext change tracking for better performance repository.DisableChangesTracking(); var entities = await LoadEntities(repository, ids, responseGroup); @@ -226,10 +226,8 @@ public virtual async Task SaveChangesAsync(IList models) foreach (var (changedEntry, i) in changedEntries.Select((x, i) => (x, i))) { - // Update the original model in place instead of creating a new one. - // This ensures DB-generated values are synced back to the original model objects - // passed to SaveChangesAsync. - changedEntities[i].ToModel(changedEntry.NewEntry); + // Sync DB-generated values back to the original model objects passed to SaveChangesAsync + changedEntry.NewEntry = ToModel(changedEntities[i], changedEntry.NewEntry); } await AfterSaveChangesAsync(models, changedEntries); @@ -333,9 +331,9 @@ protected virtual void ClearSearchCache(IList models) GenericSearchCachingRegion.ExpireRegion(); } - protected virtual TModel ToModel(TEntity entity) + protected virtual TModel ToModel(TEntity entity, TModel model = null) { - return entity.ToModel(); + return model is null ? entity.ToModel() : entity.ToModel(model); } protected virtual TEntity FromModel(TModel model, PrimaryKeyResolvingMap keyMap) From bcae5eb52cea0e78d1e7c6ff0926c2b195b25d28 Mon Sep 17 00:00:00 2001 From: Artem Dudarev Date: Fri, 13 Mar 2026 13:19:57 +0200 Subject: [PATCH 3/4] Avoid breaking changes --- .../GenericCrud/CrudService.cs | 32 ++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/src/VirtoCommerce.Platform.Data/GenericCrud/CrudService.cs b/src/VirtoCommerce.Platform.Data/GenericCrud/CrudService.cs index 1a5817df812..022f10777b9 100644 --- a/src/VirtoCommerce.Platform.Data/GenericCrud/CrudService.cs +++ b/src/VirtoCommerce.Platform.Data/GenericCrud/CrudService.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Threading.Tasks; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Primitives; @@ -32,6 +33,7 @@ public abstract class CrudService _repositoryFactory; + private readonly bool _isToModelOverridden; /// /// Construct new CrudService @@ -44,6 +46,10 @@ protected CrudService(Func repositoryFactory, IPlatformMemoryCache _repositoryFactory = repositoryFactory; _platformMemoryCache = platformMemoryCache; _eventPublisher = eventPublisher; + + _isToModelOverridden = GetType() + .GetMethod(nameof(ToModel), BindingFlags.Instance | BindingFlags.NonPublic, [typeof(TEntity)]) + ?.DeclaringType != typeof(CrudService); } /// @@ -91,7 +97,7 @@ protected virtual void ConfigureCache(MemoryCacheEntryOptions cacheOptions, stri protected virtual IList ProcessModels(IList entities, string responseGroup) { return entities - ?.Select(x => ProcessModel(responseGroup, x, ToModel(x))) + ?.Select(x => ProcessModel(responseGroup, x, ToModel(x, model: null))) .ToList(); } @@ -196,7 +202,7 @@ public virtual async Task SaveChangesAsync(IList models) // https://docs.microsoft.com/en-us/ef/core/what-is-new/ef-core-3.0/breaking-changes#detectchanges-honors-store-generated-key-values repository.TrackModifiedAsAddedForNewChildEntities(originalEntity); - var originalModel = ToModel(originalEntity); + var originalModel = ToModel(originalEntity, model: null); originalModels.Add(originalModel); changedEntries.Add(new GenericChangedEntry(model, originalModel, EntryState.Modified)); modifiedEntity.Patch(originalEntity); @@ -226,7 +232,7 @@ public virtual async Task SaveChangesAsync(IList models) foreach (var (changedEntry, i) in changedEntries.Select((x, i) => (x, i))) { - // Sync DB-generated values back to the original model objects passed to SaveChangesAsync + // Sync database-generated values back to the original models changedEntry.NewEntry = ToModel(changedEntities[i], changedEntry.NewEntry); } @@ -331,9 +337,25 @@ protected virtual void ClearSearchCache(IList models) GenericSearchCachingRegion.ExpireRegion(); } - protected virtual TModel ToModel(TEntity entity, TModel model = null) + protected virtual TModel ToModel(TEntity entity, TModel model) + { + // Call the obsolete method temporarily if it has been overridden in a derived class, to avoid breaking changes. + if (_isToModelOverridden) + { +#pragma warning disable VC0014 // Type or member is obsolete + return ToModel(entity); +#pragma warning restore VC0014 // Type or member is obsolete + } + + model ??= AbstractTypeFactory.TryCreateInstance(); + + return entity.ToModel(model); + } + + [Obsolete("Use ToModel(entity, model)", DiagnosticId = "VC0014", UrlFormat = "https://docs.virtocommerce.org/products/products-virto3-versions")] + protected virtual TModel ToModel(TEntity entity) { - return model is null ? entity.ToModel() : entity.ToModel(model); + return entity.ToModel(); } protected virtual TEntity FromModel(TModel model, PrimaryKeyResolvingMap keyMap) From f97a504ee7741cd80368cb1657cef0c747a7fbea Mon Sep 17 00:00:00 2001 From: Artem Dudarev Date: Mon, 16 Mar 2026 17:04:30 +0200 Subject: [PATCH 4/4] Revert to previous behavior --- src/VirtoCommerce.Platform.Data/GenericCrud/CrudService.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/VirtoCommerce.Platform.Data/GenericCrud/CrudService.cs b/src/VirtoCommerce.Platform.Data/GenericCrud/CrudService.cs index 022f10777b9..fbb9ec57d67 100644 --- a/src/VirtoCommerce.Platform.Data/GenericCrud/CrudService.cs +++ b/src/VirtoCommerce.Platform.Data/GenericCrud/CrudService.cs @@ -347,9 +347,7 @@ protected virtual TModel ToModel(TEntity entity, TModel model) #pragma warning restore VC0014 // Type or member is obsolete } - model ??= AbstractTypeFactory.TryCreateInstance(); - - return entity.ToModel(model); + return entity.ToModel(); } [Obsolete("Use ToModel(entity, model)", DiagnosticId = "VC0014", UrlFormat = "https://docs.virtocommerce.org/products/products-virto3-versions")]