From 61c3b4faa6cc782c926aa8065bbf432c9dfe2e7c Mon Sep 17 00:00:00 2001 From: Bernd Dongus Date: Tue, 9 Apr 2024 10:01:19 +0200 Subject: [PATCH] 3.5.0: UpdateResoureKey command added --- idee5.Globalization.Test/UnitTestBase.cs | 1 + .../UpdateResourceKeyTests.cs | 77 +++++++++++++++++++ .../idee5.Globalization.Test.csproj | 2 + .../Commands/UpdateResourceKeyCommand.cs | 22 ++++++ .../UpdateResourceKeyCommandHandler.cs | 46 +++++++++++ idee5.Globalization/Log.cs | 11 ++- .../GetAllParlanceResourcesQueryHandler.cs | 2 +- .../Repositories/AResourceRepository.cs | 3 +- idee5.Globalization/Specifications.cs | 29 ++++++- .../idee5.Globalization.csproj | 4 +- 10 files changed, 188 insertions(+), 9 deletions(-) create mode 100644 idee5.Globalization.Test/UpdateResourceKeyTests.cs create mode 100644 idee5.Globalization/Commands/UpdateResourceKeyCommand.cs create mode 100644 idee5.Globalization/Commands/UpdateResourceKeyCommandHandler.cs diff --git a/idee5.Globalization.Test/UnitTestBase.cs b/idee5.Globalization.Test/UnitTestBase.cs index b8a0941..af19d9b 100644 --- a/idee5.Globalization.Test/UnitTestBase.cs +++ b/idee5.Globalization.Test/UnitTestBase.cs @@ -39,6 +39,7 @@ public void MyTestInitialize() { _connection = new SqliteConnection("DataSource=:memory:"); _connection.Open(); contextOptions.UseSqlite(_connection); + contextOptions.LogTo(message => System.Diagnostics.Debug.WriteLine(message)); contextOptions.EnableSensitiveDataLogging(); context = new GlobalizationDbContext(contextOptions.Options); context.Database.EnsureDeleted(); diff --git a/idee5.Globalization.Test/UpdateResourceKeyTests.cs b/idee5.Globalization.Test/UpdateResourceKeyTests.cs new file mode 100644 index 0000000..46b9a4f --- /dev/null +++ b/idee5.Globalization.Test/UpdateResourceKeyTests.cs @@ -0,0 +1,77 @@ +using idee5.Globalization.Commands; +using idee5.Globalization.Models; + +using MELT; + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace idee5.Globalization.Test; + +[TestClass] +public class UpdateResourceKeyTests : UnitTestBase { + private const string _resourceSet = "UpdateResourceKey"; + + [TestInitialize] + public void TestInitialize() { + if (!context.Resources.Any(Specifications.InResourceSet(_resourceSet))) { + resourceUnitOfWork.ResourceRepository.Add(new Resource { Id = "DeRemove", ResourceSet = _resourceSet, BinFile = null, Textfile = null, Comment = null, Customer = "", Industry = "", Language = "de", Value = "xx" }); + resourceUnitOfWork.ResourceRepository.Add(new Resource { Id = "DeRemove", ResourceSet = _resourceSet, BinFile = null, Textfile = null, Comment = null, Customer = "", Industry = "", Language = "en-GB", Value = "xxx" }); + resourceUnitOfWork.ResourceRepository.Add(new Resource { Id = "LogTest", ResourceSet = _resourceSet, BinFile = null, Textfile = null, Comment = null, Customer = "", Industry = "", Language = "en-GB", Value = "xxx" }); + context.SaveChanges(); + } + } + + [TestMethod] + public async Task CanCreateUpdateAndDeleteTranslation() { + // Arrange + var handler = new UpdateResourceKeyCommandHandler(resourceUnitOfWork, new NullLogger()); + var rk = new ResourceKey() { ResourceSet = _resourceSet, Id ="DeRemove" }; + var translations = new Dictionary { + { "en-GB", "lsmf" }, + { "it", "xyz" } + }; + var command = new UpdateResourceKeyCommand(rk, translations.ToImmutableDictionary()); + + // Act + await handler.HandleAsync(command, CancellationToken.None).ConfigureAwait(false); + + // Assert + var rsc = await resourceUnitOfWork.ResourceRepository.GetAsync(r => r.ResourceSet == _resourceSet).ConfigureAwait(false); + Assert.AreEqual(2, rsc.Count); + Assert.AreEqual("lsmf", rsc.SingleOrDefault(r => r.Language == "en-GB")?.Value); + Assert.AreEqual("xyz", rsc.SingleOrDefault(r => r.Language == "it")?.Value); + } + + [TestMethod] + public async Task CanCreateLogs() { + // Arrange + var loggerFactory = TestLoggerFactory.Create(); + var logger = loggerFactory.CreateLogger(); + var handler = new UpdateResourceKeyCommandHandler(resourceUnitOfWork, logger); + var rk = new ResourceKey() { ResourceSet = _resourceSet, Id ="LogTest" }; + var translations = new Dictionary { + { "en-GB", "lsmf" }, + { "it", "xyz" } + }; + var command = new UpdateResourceKeyCommand(rk, translations.ToImmutableDictionary()); + + // Act + await handler.HandleAsync(command, CancellationToken.None).ConfigureAwait(false); + + + // Assert + Assert.AreEqual(4, loggerFactory.Sink.LogEntries.Count()); + // 2 create/update events + Assert.AreEqual(2, loggerFactory.Sink.LogEntries.Count(le => le.EventId.Id == 4)); + Assert.AreEqual(2, loggerFactory.Sink.LogEntries.Single(le =>le.EventId == 2).Properties.Single(p => p.Key == "Count").Value); + } +} \ No newline at end of file diff --git a/idee5.Globalization.Test/idee5.Globalization.Test.csproj b/idee5.Globalization.Test/idee5.Globalization.Test.csproj index ae48c69..e29e9d8 100644 --- a/idee5.Globalization.Test/idee5.Globalization.Test.csproj +++ b/idee5.Globalization.Test/idee5.Globalization.Test.csproj @@ -30,6 +30,7 @@ + @@ -43,6 +44,7 @@ + diff --git a/idee5.Globalization/Commands/UpdateResourceKeyCommand.cs b/idee5.Globalization/Commands/UpdateResourceKeyCommand.cs new file mode 100644 index 0000000..f18d4cf --- /dev/null +++ b/idee5.Globalization/Commands/UpdateResourceKeyCommand.cs @@ -0,0 +1,22 @@ +using idee5.Globalization.Models; + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Text; + +namespace idee5.Globalization.Commands; +/// +/// The update resource key command +/// +public record UpdateResourceKeyCommand : ResourceKey { + public UpdateResourceKeyCommand(ResourceKey original, ImmutableDictionary translations) : base(original) { + Translations = translations ?? throw new ArgumentNullException(nameof(translations)); + } + + /// + /// Translations of the . + /// The dictionary key is the , the value is the + /// + public ImmutableDictionary Translations { get; set; } +} diff --git a/idee5.Globalization/Commands/UpdateResourceKeyCommandHandler.cs b/idee5.Globalization/Commands/UpdateResourceKeyCommandHandler.cs new file mode 100644 index 0000000..a9311ce --- /dev/null +++ b/idee5.Globalization/Commands/UpdateResourceKeyCommandHandler.cs @@ -0,0 +1,46 @@ +using idee5.Common; +using idee5.Globalization.Models; +using idee5.Globalization.Repositories; + +using Microsoft.Extensions.Logging; + +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace idee5.Globalization.Commands; + +/// +/// The update resource key command handler. Removes translations NOT in the given list. +/// +public class UpdateResourceKeyCommandHandler : ICommandHandlerAsync { + private readonly IResourceUnitOfWork _unitOfWork; + private readonly ILogger _logger; + + public UpdateResourceKeyCommandHandler(IResourceUnitOfWork unitOfWork, ILogger logger) { + _unitOfWork = unitOfWork; + _logger = logger; + } + + /// + public async Task HandleAsync(UpdateResourceKeyCommand command, CancellationToken cancellationToken = default) { + _logger.TranslationsReceived(command.Translations.Count, command); + Resource baseResource = new() { + ResourceSet = command.ResourceSet, + Id = command.Id, + Customer = command.Customer, + Industry = command.Industry + }; + // first remove all missing translations + _logger.RemovingTranslations(); + await _unitOfWork.ResourceRepository.RemoveAsync(Specifications.OfResourceKey(baseResource) & !Specifications.TranslatedTo(command.Translations.Keys), cancellationToken).ConfigureAwait(false); + + // then update or add the given translations + foreach (var translation in command.Translations) { + Resource rsc = baseResource with { Language = translation.Key, Value = translation.Value }; + _logger.CreateOrUpdateResource(rsc); + await _unitOfWork.ResourceRepository.UpdateOrAddAsync(rsc, cancellationToken).ConfigureAwait(false); + } + await _unitOfWork.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + } +} diff --git a/idee5.Globalization/Log.cs b/idee5.Globalization/Log.cs index c63518f..d917edd 100644 --- a/idee5.Globalization/Log.cs +++ b/idee5.Globalization/Log.cs @@ -1,7 +1,16 @@ -using Microsoft.Extensions.Logging; +using idee5.Globalization.Models; + +using Microsoft.Extensions.Logging; namespace idee5.Globalization; internal static partial class Log { [LoggerMessage(1, LogLevel.Warning, "Resx file '{FilePath}' not found!")] public static partial void ResxFileNotFound(this ILogger logger, string FilePath); + [LoggerMessage(2,LogLevel.Debug, "Received {Count} translations for {Resource}")] + public static partial void TranslationsReceived(this ILogger logger, int Count, ResourceKey Resource); + + [LoggerMessage(3, LogLevel.Debug, "Removing translations ...")] + public static partial void RemovingTranslations(this ILogger logger); + [LoggerMessage(4, LogLevel.Debug, "Create or update Resource: {Resource}")] + public static partial void CreateOrUpdateResource(this ILogger logger, Resource Resource); } diff --git a/idee5.Globalization/Queries/GetAllParlanceResourcesQueryHandler.cs b/idee5.Globalization/Queries/GetAllParlanceResourcesQueryHandler.cs index 07b8759..e88d8b1 100644 --- a/idee5.Globalization/Queries/GetAllParlanceResourcesQueryHandler.cs +++ b/idee5.Globalization/Queries/GetAllParlanceResourcesQueryHandler.cs @@ -49,7 +49,7 @@ public async Task> HandleAsync(GetAllParlanceResourcesQuer ASpec customerClause = String.IsNullOrEmpty(query.CustomerId) ? CustomerNeutral : CustomerParlance(query.CustomerId); ASpec whereClause = industryClause & customerClause; if (query.LocalResources != null) - whereClause &= query.LocalResources == true ? LocalResources : !LocalResources; + whereClause &= query.LocalResources == true ? IsLocalResources : !IsLocalResources; List resList = await _repository.GetAsync(whereClause, cancellationToken).ConfigureAwait(false); // TODO: Let the database do the ordering and grouping return resList.OrderBy(r => r.ResourceSet).ThenBy(r => r.Language).ThenBy(r => r.Id).ThenByDescending(r => r.Industry).ThenByDescending(r => r.Customer) diff --git a/idee5.Globalization/Repositories/AResourceRepository.cs b/idee5.Globalization/Repositories/AResourceRepository.cs index 0d0bdda..872e942 100644 --- a/idee5.Globalization/Repositories/AResourceRepository.cs +++ b/idee5.Globalization/Repositories/AResourceRepository.cs @@ -41,11 +41,12 @@ public override async Task UpdateOrAddAsync(Resource resource, CancellationToken throw new ArgumentNullException(nameof(resource.Value)); } + // from the domain perspective the following properties can be NULL + // from the database perspective composite key columns cannot resource.Language ??= String.Empty; if (resource.Language.Length > 0) _ = new CultureInfo(resource.Language); // throws an exception if not valid. - // default values resource.Industry ??= String.Empty; resource.Customer ??= String.Empty; diff --git a/idee5.Globalization/Specifications.cs b/idee5.Globalization/Specifications.cs index 6168fda..747d538 100644 --- a/idee5.Globalization/Specifications.cs +++ b/idee5.Globalization/Specifications.cs @@ -1,6 +1,9 @@ -using idee5.Globalization.Models; +using idee5.Common; +using idee5.Globalization.Models; using NSpecifications; using System; +using System.Collections.Generic; +using System.Linq; namespace idee5.Globalization; /// @@ -42,7 +45,7 @@ public static class Specifications { /// /// Check if contains a dot (".") /// - public static readonly ASpec LocalResources = new Spec(r => r.ResourceSet.Contains(".")); + public static readonly ASpec IsLocalResources = new Spec(r => r.ResourceSet.Contains(".")); #endregion Public Fields @@ -124,8 +127,8 @@ public static class Specifications { /// /// Search the for a value in the resource set, id, value,industry, customer or comment /// - /// Resource set to search in. - /// The search value. + /// Resource set to search in + /// The search value /// An ASpec public static ASpec ContainsInResourceSet(string resourceSet, string searchValue) => new Spec(r => r.ResourceSet == resourceSet @@ -135,5 +138,23 @@ public static class Specifications { || (r.Customer != null && r.Customer.Contains(searchValue)) || (r.Comment !=null && r.Comment.Contains(searchValue))) ); + + /// + /// Select all resources of a specific + /// + /// The resource key + /// An ASpec + public static ASpec OfResourceKey(ResourceKey resourceKey) => new Spec(r => + r.ResourceSet == resourceKey.ResourceSet + && r.Id == resourceKey.Id + && (r.Industry == resourceKey.Industry || r.Industry == "" && resourceKey.Industry == null) + && (r.Customer == resourceKey.Customer || r.Customer == "" && resourceKey.Customer == null)); + + /// + /// Check if is one of the given language codes + /// + /// The language codes + /// An ASpec + public static ASpec TranslatedTo(IEnumerable translations) => new Spec(r => translations.Contains(r.Language ?? "")); #endregion Public Methods } \ No newline at end of file diff --git a/idee5.Globalization/idee5.Globalization.csproj b/idee5.Globalization/idee5.Globalization.csproj index 1c031ac..9d1147f 100644 --- a/idee5.Globalization/idee5.Globalization.csproj +++ b/idee5.Globalization/idee5.Globalization.csproj @@ -13,9 +13,9 @@ Globalization extensions. Enables database support for localization resources and parlances for industries and customers.. idee5 © idee5 2016 - 2024 - 3.4.0 + 3.5.0 idee5, Globalization, Localization - Resx import updated to AsyncDataImporter + Update resource key added enable Bernd Dongus Globalization tool for parlances for industries and customers