From ff3de65b291c1772607af3f444c2335a28d28349 Mon Sep 17 00:00:00 2001 From: maliming Date: Tue, 21 Jul 2026 16:23:04 +0800 Subject: [PATCH] Avoid multiple enumeration in MultiLingualObjectManager --- .../MultiLingualObjectManager.cs | 27 +++--- .../MultiLingualObjectManager_Tests.cs | 90 +++++++++++++++++++ 2 files changed, 105 insertions(+), 12 deletions(-) diff --git a/framework/src/Volo.Abp.MultiLingualObjects/Volo/Abp/MultiLingualObjects/MultiLingualObjectManager.cs b/framework/src/Volo.Abp.MultiLingualObjects/Volo/Abp/MultiLingualObjects/MultiLingualObjectManager.cs index 8625355e52..cc5a9a7b4e 100644 --- a/framework/src/Volo.Abp.MultiLingualObjects/Volo/Abp/MultiLingualObjects/MultiLingualObjectManager.cs +++ b/framework/src/Volo.Abp.MultiLingualObjects/Volo/Abp/MultiLingualObjects/MultiLingualObjectManager.cs @@ -28,12 +28,13 @@ public class MultiLingualObjectManager : IMultiLingualObjectManager, ITransientD { culture ??= CultureInfo.CurrentUICulture.Name; - if (translations == null || !translations.Any()) + var translationList = translations?.ToList(); + if (translationList == null || translationList.Count == 0) { return null; } - var translation = translations.FirstOrDefault(pt => pt.Language == culture); + var translation = translationList.FirstOrDefault(pt => pt.Language == culture); if (translation != null) { return translation; @@ -43,7 +44,7 @@ public class MultiLingualObjectManager : IMultiLingualObjectManager, ITransientD { translation = GetTranslationBasedOnCulturalRecursive( CultureInfo.CurrentUICulture.Parent, - translations, + translationList, 0 ); @@ -55,13 +56,13 @@ public class MultiLingualObjectManager : IMultiLingualObjectManager, ITransientD var defaultLanguage = await SettingProvider.GetOrNullAsync(LocalizationSettingNames.DefaultLanguage); - translation = translations.FirstOrDefault(pt => pt.Language == defaultLanguage); + translation = translationList.FirstOrDefault(pt => pt.Language == defaultLanguage); if (translation != null) { return translation; } - translation = translations.FirstOrDefault(); + translation = translationList.FirstOrDefault(); return translation; } @@ -96,16 +97,17 @@ public class MultiLingualObjectManager : IMultiLingualObjectManager, ITransientD { culture ??= CultureInfo.CurrentUICulture.Name; - if (translationsCombined == null || !translationsCombined.Any()) + var translationsCombinedList = translationsCombined?.Select(translations => translations.ToList()).ToList(); + if (translationsCombinedList == null || translationsCombinedList.Count == 0) { return new(); } var someHaveNoTranslations = false; var res = new List(); - foreach (var translations in translationsCombined) + foreach (var translations in translationsCombinedList) { - if (!translations.Any()) + if (translations.Count == 0) { //if the src has no translations, don't try to find a translation res.Add(null); @@ -150,10 +152,10 @@ public class MultiLingualObjectManager : IMultiLingualObjectManager, ITransientD var defaultLanguage = await SettingProvider.GetOrNullAsync(LocalizationSettingNames.DefaultLanguage); var index = 0; - foreach (var translations in translationsCombined) + foreach (var translations in translationsCombinedList) { //if the src has no translations, don't try to find a translation - if (translations.Any() && res[index] == null) + if (translations.Count > 0 && res[index] == null) { res[index] = translations.FirstOrDefault(pt => pt.Language == defaultLanguage) ?? translations.FirstOrDefault(); @@ -168,10 +170,11 @@ public class MultiLingualObjectManager : IMultiLingualObjectManager, ITransientD where TMultiLingual : IMultiLingualObject where TTranslation : class, IObjectTranslation { - var resInitial = await GetBulkTranslationsAsync(multiLinguals.Select(x => x.Translations), culture, fallbackToParentCultures); + var multiLingualList = multiLinguals.ToList(); + var resInitial = await GetBulkTranslationsAsync(multiLingualList.Select(x => x.Translations), culture, fallbackToParentCultures); var index = 0; var res = new List<(TMultiLingual entity, TTranslation? translation)>(); - foreach (var item in multiLinguals) + foreach (var item in multiLingualList) { var t = resInitial[index++]; res.Add((item, t)); diff --git a/framework/test/Volo.Abp.MultiLingualObjects.Tests/Volo/Abp/MultiLingualObjects/MultiLingualObjectManager_Tests.cs b/framework/test/Volo.Abp.MultiLingualObjects.Tests/Volo/Abp/MultiLingualObjects/MultiLingualObjectManager_Tests.cs index ef65c6a133..acb8116916 100644 --- a/framework/test/Volo.Abp.MultiLingualObjects.Tests/Volo/Abp/MultiLingualObjects/MultiLingualObjectManager_Tests.cs +++ b/framework/test/Volo.Abp.MultiLingualObjects.Tests/Volo/Abp/MultiLingualObjects/MultiLingualObjectManager_Tests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Frozen; using System.Collections.Generic; using System.Linq; @@ -188,6 +189,68 @@ public class MultiLingualObjectManager_Tests : AbpIntegratedTest(_book.Translations)); + + translation.ShouldNotBeNull(); + translation.Name.ShouldBe(_testTranslations["en"]); + } + } + + [Fact] + public async Task GetBulkTranslationsAsync_Should_Enumerate_Entities_Once() + { + using (CultureHelper.Use("fr-FR")) + { + var books = new List + { + //resolved by the fallback pass + GetTestBook("ar", "en"), + //has no translations + GetTestBook(), + //resolved by the first-translation fallback + GetTestBook("ar") + }; + + var translations = await _multiLingualObjectManager.GetBulkTranslationsAsync( + new OneTimeEnumerable(books)); + + translations.Count.ShouldBe(3); + translations[0].entity.ShouldBe(books[0]); + translations[0].translation!.Name.ShouldBe(_testTranslations["en"]); + translations[1].entity.ShouldBe(books[1]); + translations[1].translation.ShouldBeNull(); + translations[2].entity.ShouldBe(books[2]); + translations[2].translation!.Name.ShouldBe(_testTranslations["ar"]); + } + } + + [Fact] + public async Task GetBulkTranslationsAsync_Should_Enumerate_Translation_Sequences_Once() + { + using (CultureHelper.Use("fr-FR")) + { + var translationsCombined = new OneTimeEnumerable>( + _books + .Select(x => (IEnumerable)new OneTimeEnumerable(x.Translations)) + .ToList()); + + var translations = await _multiLingualObjectManager.GetBulkTranslationsAsync(translationsCombined); + + translations.Count.ShouldBe(_books.Count); + translations[0].ShouldBeNull(); + translations[1]!.Name.ShouldBe(_testTranslations["en"]); + translations[2]!.Name.ShouldBe(_testTranslations["ar"]); + translations[3]!.Name.ShouldBe(_testTranslations["en"]); + translations[4]!.Name.ShouldBe(_testTranslations["en"]); + } + } + [Fact] public async Task TestBulkMapping() { @@ -210,4 +273,31 @@ public class MultiLingualObjectManager_Tests : AbpIntegratedTest : IEnumerable + { + private readonly IEnumerable _source; + private bool _enumerated; + + public OneTimeEnumerable(IEnumerable source) + { + _source = source; + } + + public IEnumerator GetEnumerator() + { + if (_enumerated) + { + throw new InvalidOperationException("The sequence was enumerated more than once"); + } + + _enumerated = true; + return _source.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } }