From e4e3c667ef3c18f192f1b615757650fa1d91f74f Mon Sep 17 00:00:00 2001 From: Francis Pion Date: Wed, 18 Dec 2024 18:07:44 -0500 Subject: [PATCH 1/5] Implementing Language management. --- backend/Cms.sln | 6 ++ .../DependencyInjectionExtensions.cs | 14 +++ .../Logitar.Cms.Core/Logitar.Cms.Core.csproj | 1 + .../Logitar.Cms.Infrastructure/CmsContext.cs | 18 ++++ .../CmsDb/Helper.cs | 6 ++ .../CmsDb/Languages.cs | 26 ++++++ .../CmsEventSerializer.cs | 14 +++ .../Commands/InitializeDatabaseCommand.cs | 25 ++++++ .../Configurations/AggregateConfiguration.cs | 22 +++++ .../Configurations/LanguageConfiguration.cs | 25 ++++++ .../Converters/LanguageIdConverter.cs | 17 ++++ .../Converters/LocaleConverter.cs | 17 ++++ .../DependencyInjectionExtensions.cs | 34 ++++++++ .../Entities/AggregateEntity.cs | 55 ++++++++++++ .../Entities/LanguageEntity.cs | 64 ++++++++++++++ .../Logitar.Cms.Infrastructure/EventBus.cs | 20 +++++ .../Handlers/LanguageHandlers.cs | 52 +++++++++++ .../Logitar.Cms.Infrastructure.csproj | 31 +++++++ .../src/Logitar.Cms.Infrastructure/Mapper.cs | 69 +++++++++++++++ .../Queriers/LanguageQuerier.cs | 87 +++++++++++++++++++ .../Repositories/LanguageRepository.cs | 30 +++++++ .../Logitar.Cms.Web/Logitar.Cms.Web.csproj | 2 +- 22 files changed, 634 insertions(+), 1 deletion(-) create mode 100644 backend/src/Logitar.Cms.Core/DependencyInjectionExtensions.cs create mode 100644 backend/src/Logitar.Cms.Infrastructure/CmsContext.cs create mode 100644 backend/src/Logitar.Cms.Infrastructure/CmsDb/Helper.cs create mode 100644 backend/src/Logitar.Cms.Infrastructure/CmsDb/Languages.cs create mode 100644 backend/src/Logitar.Cms.Infrastructure/CmsEventSerializer.cs create mode 100644 backend/src/Logitar.Cms.Infrastructure/Commands/InitializeDatabaseCommand.cs create mode 100644 backend/src/Logitar.Cms.Infrastructure/Configurations/AggregateConfiguration.cs create mode 100644 backend/src/Logitar.Cms.Infrastructure/Configurations/LanguageConfiguration.cs create mode 100644 backend/src/Logitar.Cms.Infrastructure/Converters/LanguageIdConverter.cs create mode 100644 backend/src/Logitar.Cms.Infrastructure/Converters/LocaleConverter.cs create mode 100644 backend/src/Logitar.Cms.Infrastructure/DependencyInjectionExtensions.cs create mode 100644 backend/src/Logitar.Cms.Infrastructure/Entities/AggregateEntity.cs create mode 100644 backend/src/Logitar.Cms.Infrastructure/Entities/LanguageEntity.cs create mode 100644 backend/src/Logitar.Cms.Infrastructure/EventBus.cs create mode 100644 backend/src/Logitar.Cms.Infrastructure/Handlers/LanguageHandlers.cs create mode 100644 backend/src/Logitar.Cms.Infrastructure/Logitar.Cms.Infrastructure.csproj create mode 100644 backend/src/Logitar.Cms.Infrastructure/Mapper.cs create mode 100644 backend/src/Logitar.Cms.Infrastructure/Queriers/LanguageQuerier.cs create mode 100644 backend/src/Logitar.Cms.Infrastructure/Repositories/LanguageRepository.cs diff --git a/backend/Cms.sln b/backend/Cms.sln index 28e2933..6380c2c 100644 --- a/backend/Cms.sln +++ b/backend/Cms.sln @@ -20,6 +20,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{49A3AE69 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Logitar.Cms.UnitTests", "tests\Logitar.Cms.UnitTests\Logitar.Cms.UnitTests.csproj", "{F367B008-1CAD-49A5-9EB1-5160661FF9CC}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Logitar.Cms.Infrastructure", "src\Logitar.Cms.Infrastructure\Logitar.Cms.Infrastructure.csproj", "{15549282-0316-4CA0-A3B2-ADE7E89C4A3D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -38,6 +40,10 @@ Global {F367B008-1CAD-49A5-9EB1-5160661FF9CC}.Debug|Any CPU.Build.0 = Debug|Any CPU {F367B008-1CAD-49A5-9EB1-5160661FF9CC}.Release|Any CPU.ActiveCfg = Release|Any CPU {F367B008-1CAD-49A5-9EB1-5160661FF9CC}.Release|Any CPU.Build.0 = Release|Any CPU + {15549282-0316-4CA0-A3B2-ADE7E89C4A3D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {15549282-0316-4CA0-A3B2-ADE7E89C4A3D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {15549282-0316-4CA0-A3B2-ADE7E89C4A3D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {15549282-0316-4CA0-A3B2-ADE7E89C4A3D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/backend/src/Logitar.Cms.Core/DependencyInjectionExtensions.cs b/backend/src/Logitar.Cms.Core/DependencyInjectionExtensions.cs new file mode 100644 index 0000000..d846fb1 --- /dev/null +++ b/backend/src/Logitar.Cms.Core/DependencyInjectionExtensions.cs @@ -0,0 +1,14 @@ +using Logitar.EventSourcing; +using Microsoft.Extensions.DependencyInjection; + +namespace Logitar.Cms.Core; + +public static class DependencyInjectionExtensions +{ + public static IServiceCollection AddLogitarCmsCore(this IServiceCollection services) + { + return services + .AddLogitarEventSourcing() + .AddMediatR(config => config.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly())); + } +} diff --git a/backend/src/Logitar.Cms.Core/Logitar.Cms.Core.csproj b/backend/src/Logitar.Cms.Core/Logitar.Cms.Core.csproj index 260f946..a117314 100644 --- a/backend/src/Logitar.Cms.Core/Logitar.Cms.Core.csproj +++ b/backend/src/Logitar.Cms.Core/Logitar.Cms.Core.csproj @@ -23,6 +23,7 @@ + diff --git a/backend/src/Logitar.Cms.Infrastructure/CmsContext.cs b/backend/src/Logitar.Cms.Infrastructure/CmsContext.cs new file mode 100644 index 0000000..b48ad0d --- /dev/null +++ b/backend/src/Logitar.Cms.Infrastructure/CmsContext.cs @@ -0,0 +1,18 @@ +using Logitar.Cms.Infrastructure.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Logitar.Cms.Infrastructure; + +public class CmsContext : DbContext +{ + public CmsContext(DbContextOptions options) : base(options) + { + } + + internal DbSet Languages => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly()); + } +} diff --git a/backend/src/Logitar.Cms.Infrastructure/CmsDb/Helper.cs b/backend/src/Logitar.Cms.Infrastructure/CmsDb/Helper.cs new file mode 100644 index 0000000..8c4165f --- /dev/null +++ b/backend/src/Logitar.Cms.Infrastructure/CmsDb/Helper.cs @@ -0,0 +1,6 @@ +namespace Logitar.Cms.Infrastructure.CmsDb; + +public static class Helper // TODO(fpion): refactor +{ + public static string Normalize(string value) => value.Trim().ToUpperInvariant(); +} diff --git a/backend/src/Logitar.Cms.Infrastructure/CmsDb/Languages.cs b/backend/src/Logitar.Cms.Infrastructure/CmsDb/Languages.cs new file mode 100644 index 0000000..b56e439 --- /dev/null +++ b/backend/src/Logitar.Cms.Infrastructure/CmsDb/Languages.cs @@ -0,0 +1,26 @@ +using Logitar.Cms.Infrastructure.Entities; +using Logitar.Data; + +namespace Logitar.Cms.Infrastructure.CmsDb; + +public static class Languages +{ + public static readonly TableId Table = new(nameof(CmsContext.Languages)); + + public static readonly ColumnId CreatedBy = new(nameof(LanguageEntity.CreatedBy), Table); + public static readonly ColumnId CreatedOn = new(nameof(LanguageEntity.CreatedOn), Table); + public static readonly ColumnId StreamId = new(nameof(LanguageEntity.StreamId), Table); + public static readonly ColumnId UpdatedBy = new(nameof(LanguageEntity.UpdatedBy), Table); + public static readonly ColumnId UpdatedOn = new(nameof(LanguageEntity.UpdatedOn), Table); + public static readonly ColumnId Version = new(nameof(LanguageEntity.Version), Table); + + public static readonly ColumnId Code = new(nameof(LanguageEntity.Code), Table); + public static readonly ColumnId CodeNormalized = new(nameof(LanguageEntity.CodeNormalized), Table); + public static readonly ColumnId DisplayName = new(nameof(LanguageEntity.DisplayName), Table); + public static readonly ColumnId EnglishName = new(nameof(LanguageEntity.EnglishName), Table); + public static readonly ColumnId Id = new(nameof(LanguageEntity.Id), Table); + public static readonly ColumnId IsDefault = new(nameof(LanguageEntity.IsDefault), Table); + public static readonly ColumnId LanguageId = new(nameof(LanguageEntity.LanguageId), Table); + public static readonly ColumnId LCID = new(nameof(LanguageEntity.LCID), Table); + public static readonly ColumnId NativeName = new(nameof(LanguageEntity.NativeName), Table); +} diff --git a/backend/src/Logitar.Cms.Infrastructure/CmsEventSerializer.cs b/backend/src/Logitar.Cms.Infrastructure/CmsEventSerializer.cs new file mode 100644 index 0000000..6f1aa5c --- /dev/null +++ b/backend/src/Logitar.Cms.Infrastructure/CmsEventSerializer.cs @@ -0,0 +1,14 @@ +using Logitar.Cms.Infrastructure.Converters; + +namespace Logitar.Cms.Infrastructure; + +internal class EventSerializer : EventSourcing.Infrastructure.EventSerializer +{ + protected override void RegisterConverters() + { + base.RegisterConverters(); + + SerializerOptions.Converters.Add(new LanguageIdConverter()); + SerializerOptions.Converters.Add(new LocaleConverter()); + } +} diff --git a/backend/src/Logitar.Cms.Infrastructure/Commands/InitializeDatabaseCommand.cs b/backend/src/Logitar.Cms.Infrastructure/Commands/InitializeDatabaseCommand.cs new file mode 100644 index 0000000..595be05 --- /dev/null +++ b/backend/src/Logitar.Cms.Infrastructure/Commands/InitializeDatabaseCommand.cs @@ -0,0 +1,25 @@ +using Logitar.EventSourcing.EntityFrameworkCore.Relational; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Logitar.Cms.Infrastructure.Commands; + +public record InitializeDatabaseCommand : INotification; + +public class InitializeDatabaseCommandHandler : INotificationHandler +{ + private readonly EventContext _eventContext; + private readonly CmsContext _cmsContext; + + public InitializeDatabaseCommandHandler(EventContext eventContext, CmsContext cmsContext) + { + _eventContext = eventContext; + _cmsContext = cmsContext; + } + + public async Task Handle(InitializeDatabaseCommand command, CancellationToken cancellationToken) + { + await _eventContext.Database.MigrateAsync(cancellationToken); + await _cmsContext.Database.MigrateAsync(cancellationToken); + } +} diff --git a/backend/src/Logitar.Cms.Infrastructure/Configurations/AggregateConfiguration.cs b/backend/src/Logitar.Cms.Infrastructure/Configurations/AggregateConfiguration.cs new file mode 100644 index 0000000..aea9448 --- /dev/null +++ b/backend/src/Logitar.Cms.Infrastructure/Configurations/AggregateConfiguration.cs @@ -0,0 +1,22 @@ +using Logitar.Cms.Infrastructure.Entities; +using Logitar.EventSourcing; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Logitar.Cms.Infrastructure.Configurations; + +internal abstract class AggregateConfiguration where T : AggregateEntity // TODO(fpion): refactor +{ + public virtual void Configure(EntityTypeBuilder builder) + { + builder.HasIndex(x => x.StreamId).IsUnique(); + builder.HasIndex(x => x.Version); + builder.HasIndex(x => x.CreatedBy); + builder.HasIndex(x => x.CreatedOn); + builder.HasIndex(x => x.UpdatedBy); + builder.HasIndex(x => x.UpdatedOn); + + builder.Property(x => x.StreamId).HasMaxLength(StreamId.MaximumLength); + builder.Property(x => x.CreatedBy).HasMaxLength(ActorId.MaximumLength); + builder.Property(x => x.UpdatedBy).HasMaxLength(ActorId.MaximumLength); + } +} diff --git a/backend/src/Logitar.Cms.Infrastructure/Configurations/LanguageConfiguration.cs b/backend/src/Logitar.Cms.Infrastructure/Configurations/LanguageConfiguration.cs new file mode 100644 index 0000000..f59d071 --- /dev/null +++ b/backend/src/Logitar.Cms.Infrastructure/Configurations/LanguageConfiguration.cs @@ -0,0 +1,25 @@ +using Logitar.Cms.Infrastructure.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Logitar.Cms.Infrastructure.Configurations; + +internal class LanguageConfiguration : AggregateConfiguration, IEntityTypeConfiguration +{ + public override void Configure(EntityTypeBuilder builder) + { + base.Configure(builder); + + builder.ToTable(CmsDb.Languages.Table.Table ?? string.Empty, CmsDb.Languages.Table.Schema); + builder.HasKey(x => x.LanguageId); + + builder.HasIndex(x => x.Id).IsUnique(); + builder.HasIndex(x => x.IsDefault); + builder.HasIndex(x => x.LCID); + builder.HasIndex(x => x.Code); + builder.HasIndex(x => x.CodeNormalized).IsUnique(); + builder.HasIndex(x => x.DisplayName); + builder.HasIndex(x => x.EnglishName); + builder.HasIndex(x => x.NativeName); + } +} diff --git a/backend/src/Logitar.Cms.Infrastructure/Converters/LanguageIdConverter.cs b/backend/src/Logitar.Cms.Infrastructure/Converters/LanguageIdConverter.cs new file mode 100644 index 0000000..f73cd50 --- /dev/null +++ b/backend/src/Logitar.Cms.Infrastructure/Converters/LanguageIdConverter.cs @@ -0,0 +1,17 @@ +using Logitar.Cms.Core.Localization; + +namespace Logitar.Cms.Infrastructure.Converters; + +internal class LanguageIdConverter : JsonConverter +{ + public override LanguageId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + string? value = reader.GetString(); + return string.IsNullOrWhiteSpace(value) ? new LanguageId() : new(value); + } + + public override void Write(Utf8JsonWriter writer, LanguageId languageId, JsonSerializerOptions options) + { + writer.WriteStringValue(languageId.Value); + } +} diff --git a/backend/src/Logitar.Cms.Infrastructure/Converters/LocaleConverter.cs b/backend/src/Logitar.Cms.Infrastructure/Converters/LocaleConverter.cs new file mode 100644 index 0000000..6fc48d9 --- /dev/null +++ b/backend/src/Logitar.Cms.Infrastructure/Converters/LocaleConverter.cs @@ -0,0 +1,17 @@ +using Logitar.Cms.Core.Localization; + +namespace Logitar.Cms.Infrastructure.Converters; + +internal class LocaleConverter : JsonConverter +{ + public override Locale? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + string? value = reader.GetString(); + return string.IsNullOrWhiteSpace(value) ? null : new Locale(value); + } + + public override void Write(Utf8JsonWriter writer, Locale locale, JsonSerializerOptions options) + { + writer.WriteStringValue(locale.Value); + } +} diff --git a/backend/src/Logitar.Cms.Infrastructure/DependencyInjectionExtensions.cs b/backend/src/Logitar.Cms.Infrastructure/DependencyInjectionExtensions.cs new file mode 100644 index 0000000..2ecaae3 --- /dev/null +++ b/backend/src/Logitar.Cms.Infrastructure/DependencyInjectionExtensions.cs @@ -0,0 +1,34 @@ +using Logitar.Cms.Core; +using Logitar.Cms.Core.Localization; +using Logitar.Cms.Infrastructure.Queriers; +using Logitar.Cms.Infrastructure.Repositories; +using Logitar.EventSourcing.Infrastructure; +using Microsoft.Extensions.DependencyInjection; + +namespace Logitar.Cms.Infrastructure; + +public static class DependencyInjectionExtensions +{ + public static IServiceCollection AddLogitarCmsInfrastructure(this IServiceCollection services) + { + return services + .AddLogitarCmsCore() + .AddMediatR(config => config.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly())) + .AddSingleton() + .AddScoped() + .AddQueriers() + .AddRepositories(); + } + + private static IServiceCollection AddQueriers(this IServiceCollection services) + { + return services + .AddScoped(); + } + + private static IServiceCollection AddRepositories(this IServiceCollection services) + { + return services + .AddScoped(); + } +} diff --git a/backend/src/Logitar.Cms.Infrastructure/Entities/AggregateEntity.cs b/backend/src/Logitar.Cms.Infrastructure/Entities/AggregateEntity.cs new file mode 100644 index 0000000..a07b8c6 --- /dev/null +++ b/backend/src/Logitar.Cms.Infrastructure/Entities/AggregateEntity.cs @@ -0,0 +1,55 @@ +using Logitar.EventSourcing; + +namespace Logitar.Cms.Infrastructure.Entities; + +internal abstract class AggregateEntity // TODO(fpion): refactor +{ + public string StreamId { get; private set; } = string.Empty; + public long Version { get; private set; } + + public string? CreatedBy { get; private set; } + public DateTime CreatedOn { get; private set; } + + public string? UpdatedBy { get; private set; } + public DateTime UpdatedOn { get; private set; } + + protected AggregateEntity() + { + } + + protected AggregateEntity(DomainEvent @event) + { + StreamId = @event.StreamId.Value; + + CreatedBy = @event.ActorId?.Value; + CreatedOn = @event.OccurredOn.AsUniversalTime(); + + Update(@event); + } + + public virtual IReadOnlyCollection GetActorIds() + { + List actorIds = new(capacity: 2); + if (CreatedBy != null) + { + actorIds.Add(new(CreatedBy)); + } + if (UpdatedBy != null) + { + actorIds.Add(new(UpdatedBy)); + } + return actorIds.AsReadOnly(); + } + + protected virtual void Update(DomainEvent @event) + { + Version = @event.Version; + + UpdatedBy = @event.ActorId?.Value; + UpdatedOn = @event.OccurredOn.AsUniversalTime(); + } + + public override bool Equals(object? obj) => obj is AggregateEntity aggregate && aggregate.StreamId == StreamId; + public override int GetHashCode() => StreamId.GetHashCode(); + public override string ToString() => $"{GetType()} (StreamId={StreamId})"; +} diff --git a/backend/src/Logitar.Cms.Infrastructure/Entities/LanguageEntity.cs b/backend/src/Logitar.Cms.Infrastructure/Entities/LanguageEntity.cs new file mode 100644 index 0000000..8f0d345 --- /dev/null +++ b/backend/src/Logitar.Cms.Infrastructure/Entities/LanguageEntity.cs @@ -0,0 +1,64 @@ +using Logitar.Cms.Core.Localization; +using Logitar.Cms.Core.Localization.Events; +using System.Globalization; + +namespace Logitar.Cms.Infrastructure.Entities; + +internal class LanguageEntity : AggregateEntity +{ + public int LanguageId { get; private set; } + public Guid Id { get; private set; } + + public bool IsDefault { get; private set; } + + public int LCID { get; private set; } + public string Code { get; private set; } = string.Empty; + public string CodeNormalized + { + get => CmsDb.Helper.Normalize(Code); + private set { } + } + public string DisplayName { get; private set; } = string.Empty; + public string EnglishName { get; private set; } = string.Empty; + public string NativeName { get; private set; } = string.Empty; + + public LanguageEntity(LanguageCreated @event) : base(@event) + { + Id = @event.StreamId.ToGuid(); + + IsDefault = @event.IsDefault; + + SetLocale(@event.Locale); + } + + private LanguageEntity() : base() + { + } + + public void SetDefault(LanguageSetDefault @event) + { + base.Update(@event); + + IsDefault = @event.IsDefault; + } + + public void Update(LanguageUpdated @event) + { + base.Update(@event); + + if (@event.Locale != null) + { + SetLocale(@event.Locale); + } + } + + private void SetLocale(Locale locale) + { + CultureInfo culture = locale.Culture; + LCID = culture.LCID; + Code = culture.Name; + DisplayName = culture.DisplayName; + EnglishName = culture.EnglishName; + NativeName = culture.NativeName; + } +} diff --git a/backend/src/Logitar.Cms.Infrastructure/EventBus.cs b/backend/src/Logitar.Cms.Infrastructure/EventBus.cs new file mode 100644 index 0000000..3f25d8f --- /dev/null +++ b/backend/src/Logitar.Cms.Infrastructure/EventBus.cs @@ -0,0 +1,20 @@ +using Logitar.EventSourcing; +using Logitar.EventSourcing.Infrastructure; +using MediatR; + +namespace Logitar.Cms.Infrastructure; + +internal class EventBus : IEventBus +{ + private readonly IMediator _mediator; + + public EventBus(IMediator mediator) + { + _mediator = mediator; + } + + public async Task PublishAsync(IEvent @event, CancellationToken cancellationToken) + { + await _mediator.Publish(@event, cancellationToken); + } +} diff --git a/backend/src/Logitar.Cms.Infrastructure/Handlers/LanguageHandlers.cs b/backend/src/Logitar.Cms.Infrastructure/Handlers/LanguageHandlers.cs new file mode 100644 index 0000000..bfcf8b5 --- /dev/null +++ b/backend/src/Logitar.Cms.Infrastructure/Handlers/LanguageHandlers.cs @@ -0,0 +1,52 @@ +using Logitar.Cms.Core.Localization.Events; +using Logitar.Cms.Infrastructure.Entities; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Logitar.Cms.Infrastructure.Handlers; + +internal class LanguageHandlers : INotificationHandler, INotificationHandler, INotificationHandler +{ + private readonly CmsContext _context; + + public LanguageHandlers(CmsContext context) + { + _context = context; + } + + public async Task Handle(LanguageCreated @event, CancellationToken cancellationToken) + { + LanguageEntity? language = await _context.Languages.AsNoTracking() + .SingleOrDefaultAsync(x => x.StreamId == @event.StreamId.Value, cancellationToken); + if (language == null) + { + language = new(@event); + + _context.Languages.Add(language); + + await _context.SaveChangesAsync(cancellationToken); + } + } + + public async Task Handle(LanguageSetDefault @event, CancellationToken cancellationToken) + { + LanguageEntity language = await _context.Languages + .SingleOrDefaultAsync(x => x.StreamId == @event.StreamId.Value, cancellationToken) + ?? throw new InvalidOperationException($"The language entity 'StreamId={@event.StreamId}' could not be found."); + + language.SetDefault(@event); + + await _context.SaveChangesAsync(cancellationToken); + } + + public async Task Handle(LanguageUpdated @event, CancellationToken cancellationToken) + { + LanguageEntity language = await _context.Languages + .SingleOrDefaultAsync(x => x.StreamId == @event.StreamId.Value, cancellationToken) + ?? throw new InvalidOperationException($"The language entity 'StreamId={@event.StreamId}' could not be found."); + + language.Update(@event); + + await _context.SaveChangesAsync(cancellationToken); + } +} diff --git a/backend/src/Logitar.Cms.Infrastructure/Logitar.Cms.Infrastructure.csproj b/backend/src/Logitar.Cms.Infrastructure/Logitar.Cms.Infrastructure.csproj new file mode 100644 index 0000000..915ec15 --- /dev/null +++ b/backend/src/Logitar.Cms.Infrastructure/Logitar.Cms.Infrastructure.csproj @@ -0,0 +1,31 @@ + + + + net9.0 + enable + enable + + + + True + + + + True + + + + + + + + + + + + + + + + + diff --git a/backend/src/Logitar.Cms.Infrastructure/Mapper.cs b/backend/src/Logitar.Cms.Infrastructure/Mapper.cs new file mode 100644 index 0000000..e8278a9 --- /dev/null +++ b/backend/src/Logitar.Cms.Infrastructure/Mapper.cs @@ -0,0 +1,69 @@ +using Logitar.Cms.Core.Localization.Models; +using Logitar.Cms.Core.Models; +using Logitar.Cms.Infrastructure.Entities; +using Logitar.EventSourcing; + +namespace Logitar.Cms.Infrastructure; + +internal class Mapper +{ + private readonly Dictionary _actors = []; + private readonly ActorModel _system = ActorModel.System; + + public Mapper() + { + } + + public Mapper(IEnumerable actors) + { + foreach (ActorModel actor in actors) + { + ActorId actorId = new(actor.Id); + _actors[actorId] = actor; + } + } + + public LanguageModel ToLanguage(LanguageEntity source) + { + LanguageModel destination = new() + { + IsDefault = source.IsDefault, + Locale = new LocaleModel(source.Code) + }; + + MapAggregate(source, destination); + + return destination; + } + + private void MapAggregate(AggregateEntity source, AggregateModel destination) + { + try + { + destination.Id = new StreamId(source.StreamId).ToGuid(); + } + catch (Exception) + { + } + destination.Version = source.Version; + + destination.CreatedBy = FindActor(source.CreatedBy); + destination.CreatedOn = source.CreatedOn.AsUniversalTime(); + + destination.UpdatedBy = FindActor(source.UpdatedBy); + destination.UpdatedOn = source.UpdatedOn.AsUniversalTime(); + } + private ActorModel FindActor(string? id) + { + if (id != null) + { + ActorId actorId = new(id); + if (_actors.TryGetValue(actorId, out ActorModel? actor)) + { + return actor; + } + } + + return _system; + } +} diff --git a/backend/src/Logitar.Cms.Infrastructure/Queriers/LanguageQuerier.cs b/backend/src/Logitar.Cms.Infrastructure/Queriers/LanguageQuerier.cs new file mode 100644 index 0000000..39366ba --- /dev/null +++ b/backend/src/Logitar.Cms.Infrastructure/Queriers/LanguageQuerier.cs @@ -0,0 +1,87 @@ +using Logitar.Cms.Core.Localization; +using Logitar.Cms.Core.Localization.Models; +using Logitar.Cms.Infrastructure.Entities; +using Logitar.EventSourcing; +using Microsoft.EntityFrameworkCore; + +namespace Logitar.Cms.Infrastructure.Queriers; + +internal class LanguageQuerier : ILanguageQuerier +{ + private readonly DbSet _languages; + + public LanguageQuerier(CmsContext context) + { + _languages = context.Languages; + } + + public async Task FindDefaultIdAsync(CancellationToken cancellationToken) + { + string streamId = await _languages.AsNoTracking() + .Where(x => x.IsDefault) + .Select(x => x.StreamId) + .SingleOrDefaultAsync(cancellationToken) + ?? throw new InvalidOperationException("The default language entity could not be found."); + + return new LanguageId(streamId); + } + public async Task FindIdAsync(Locale locale, CancellationToken cancellationToken) + { + string codeNormalized = CmsDb.Helper.Normalize(locale.Value); + + string? streamId = await _languages.AsNoTracking() + .Where(x => x.CodeNormalized == codeNormalized) + .Select(x => x.StreamId) + .SingleOrDefaultAsync(cancellationToken); + + return streamId == null ? null : new LanguageId(streamId); + } + + public async Task ReadAsync(Language language, CancellationToken cancellationToken) + { + return await ReadAsync(language.Id, cancellationToken) + ?? throw new InvalidOperationException($"The language 'StreamId={language.Id}' could not be found."); + } + public async Task ReadAsync(LanguageId id, CancellationToken cancellationToken) + { + return await ReadAsync(id.ToGuid(), cancellationToken); + } + public async Task ReadAsync(Guid id, CancellationToken cancellationToken) + { + LanguageEntity? language = await _languages.AsNoTracking() + .SingleOrDefaultAsync(x => x.Id == id, cancellationToken); + + return language == null ? null : await MapAsync(language, cancellationToken); + } + public async Task ReadAsync(string locale, CancellationToken cancellationToken) + { + string codeNormalized = CmsDb.Helper.Normalize(locale); + + LanguageEntity? language = await _languages.AsNoTracking() + .SingleOrDefaultAsync(x => x.CodeNormalized == codeNormalized, cancellationToken); + + return language == null ? null : await MapAsync(language, cancellationToken); + } + + public async Task ReadDefaultAsync(CancellationToken cancellationToken) + { + LanguageEntity language = await _languages.AsNoTracking() + .SingleOrDefaultAsync(x => x.IsDefault) + ?? throw new InvalidOperationException("The default language entity could not be found."); + + return await MapAsync(language, cancellationToken); + } + + private async Task MapAsync(LanguageEntity language, CancellationToken cancellationToken) + { + return (await MapAsync([language], cancellationToken)).Single(); + } + private async Task> MapAsync(IEnumerable languages, CancellationToken cancellationToken) + { + IEnumerable actorIds = languages.SelectMany(language => language.GetActorIds()); + await Task.Delay(1, cancellationToken); // TODO(fpion): actors + Mapper mapper = new(); // TODO(fpion): actors + + return languages.Select(mapper.ToLanguage).ToArray(); + } +} diff --git a/backend/src/Logitar.Cms.Infrastructure/Repositories/LanguageRepository.cs b/backend/src/Logitar.Cms.Infrastructure/Repositories/LanguageRepository.cs new file mode 100644 index 0000000..b3c4f9a --- /dev/null +++ b/backend/src/Logitar.Cms.Infrastructure/Repositories/LanguageRepository.cs @@ -0,0 +1,30 @@ +using Logitar.Cms.Core.Localization; +using Logitar.EventSourcing; + +namespace Logitar.Cms.Infrastructure.Repositories; + +internal class LanguageRepository : Repository, ILanguageRepository +{ + public LanguageRepository(IEventStore eventStore) : base(eventStore) + { + } + + public async Task LoadAsync(LanguageId id, CancellationToken cancellationToken) + { + return await LoadAsync(id, version: null, cancellationToken); + } + public async Task LoadAsync(LanguageId id, long? version, CancellationToken cancellationToken) + { + return await LoadAsync(id.StreamId, version, cancellationToken); + } + + public async Task SaveAsync(Language language, CancellationToken cancellationToken) + { + await base.SaveAsync(language, cancellationToken); + } + + public async Task SaveAsync(IEnumerable languages, CancellationToken cancellationToken) + { + await base.SaveAsync(languages, cancellationToken); + } +} diff --git a/backend/src/Logitar.Cms.Web/Logitar.Cms.Web.csproj b/backend/src/Logitar.Cms.Web/Logitar.Cms.Web.csproj index 780e96a..c76e2af 100644 --- a/backend/src/Logitar.Cms.Web/Logitar.Cms.Web.csproj +++ b/backend/src/Logitar.Cms.Web/Logitar.Cms.Web.csproj @@ -15,7 +15,7 @@ - + From 536e88b846f12b928e0dc9c4afbf76486f1f008e Mon Sep 17 00:00:00 2001 From: Francis Pion Date: Wed, 18 Dec 2024 18:08:27 -0500 Subject: [PATCH 2/5] using --- .../src/Logitar.Cms.Infrastructure/Entities/LanguageEntity.cs | 1 - .../Logitar.Cms.Infrastructure/Logitar.Cms.Infrastructure.csproj | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/Logitar.Cms.Infrastructure/Entities/LanguageEntity.cs b/backend/src/Logitar.Cms.Infrastructure/Entities/LanguageEntity.cs index 8f0d345..3e9a36a 100644 --- a/backend/src/Logitar.Cms.Infrastructure/Entities/LanguageEntity.cs +++ b/backend/src/Logitar.Cms.Infrastructure/Entities/LanguageEntity.cs @@ -1,6 +1,5 @@ using Logitar.Cms.Core.Localization; using Logitar.Cms.Core.Localization.Events; -using System.Globalization; namespace Logitar.Cms.Infrastructure.Entities; diff --git a/backend/src/Logitar.Cms.Infrastructure/Logitar.Cms.Infrastructure.csproj b/backend/src/Logitar.Cms.Infrastructure/Logitar.Cms.Infrastructure.csproj index 915ec15..0223d4f 100644 --- a/backend/src/Logitar.Cms.Infrastructure/Logitar.Cms.Infrastructure.csproj +++ b/backend/src/Logitar.Cms.Infrastructure/Logitar.Cms.Infrastructure.csproj @@ -23,6 +23,7 @@ + From 4ae113310ad8b3d2c145e536a3c52a989d95ebb8 Mon Sep 17 00:00:00 2001 From: Francis Pion Date: Wed, 18 Dec 2024 18:08:52 -0500 Subject: [PATCH 3/5] rename --- .../{CmsEventSerializer.cs => EventSerializer.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename backend/src/Logitar.Cms.Infrastructure/{CmsEventSerializer.cs => EventSerializer.cs} (100%) diff --git a/backend/src/Logitar.Cms.Infrastructure/CmsEventSerializer.cs b/backend/src/Logitar.Cms.Infrastructure/EventSerializer.cs similarity index 100% rename from backend/src/Logitar.Cms.Infrastructure/CmsEventSerializer.cs rename to backend/src/Logitar.Cms.Infrastructure/EventSerializer.cs From a8bc575a236d0bacb40e8d63c60b59ed154368de Mon Sep 17 00:00:00 2001 From: Francis Pion Date: Wed, 18 Dec 2024 23:07:53 -0500 Subject: [PATCH 4/5] actors --- .../{Models => Actors}/ActorType.cs | 2 +- .../src/Logitar.Cms.Core/Models/ActorModel.cs | 4 +- .../Actors/ActorService.cs | 56 +++++++++++++++++++ .../Actors/IActorService.cs | 9 +++ .../Caching/CacheService.cs | 31 ++++++++++ .../Caching/ICacheService.cs | 10 ++++ .../Logitar.Cms.Infrastructure/CmsContext.cs | 1 + .../CmsDb/Actors.cs | 18 ++++++ .../Configurations/ActorConfiguration.cs | 28 ++++++++++ .../DependencyInjectionExtensions.cs | 14 +++++ .../Entities/ActorEntity.cs | 25 +++++++++ .../Logitar.Cms.Infrastructure.csproj | 1 + .../src/Logitar.Cms.Infrastructure/Mapper.cs | 9 +++ .../Queriers/LanguageQuerier.cs | 12 ++-- .../Settings/CachingSettings.cs | 8 +++ 15 files changed, 222 insertions(+), 6 deletions(-) rename backend/src/Logitar.Cms.Core/{Models => Actors}/ActorType.cs (63%) create mode 100644 backend/src/Logitar.Cms.Infrastructure/Actors/ActorService.cs create mode 100644 backend/src/Logitar.Cms.Infrastructure/Actors/IActorService.cs create mode 100644 backend/src/Logitar.Cms.Infrastructure/Caching/CacheService.cs create mode 100644 backend/src/Logitar.Cms.Infrastructure/Caching/ICacheService.cs create mode 100644 backend/src/Logitar.Cms.Infrastructure/CmsDb/Actors.cs create mode 100644 backend/src/Logitar.Cms.Infrastructure/Configurations/ActorConfiguration.cs create mode 100644 backend/src/Logitar.Cms.Infrastructure/Entities/ActorEntity.cs create mode 100644 backend/src/Logitar.Cms.Infrastructure/Settings/CachingSettings.cs diff --git a/backend/src/Logitar.Cms.Core/Models/ActorType.cs b/backend/src/Logitar.Cms.Core/Actors/ActorType.cs similarity index 63% rename from backend/src/Logitar.Cms.Core/Models/ActorType.cs rename to backend/src/Logitar.Cms.Core/Actors/ActorType.cs index 58a1434..a2ad4b5 100644 --- a/backend/src/Logitar.Cms.Core/Models/ActorType.cs +++ b/backend/src/Logitar.Cms.Core/Actors/ActorType.cs @@ -1,4 +1,4 @@ -namespace Logitar.Cms.Core.Models; +namespace Logitar.Cms.Core.Actors; public enum ActorType { diff --git a/backend/src/Logitar.Cms.Core/Models/ActorModel.cs b/backend/src/Logitar.Cms.Core/Models/ActorModel.cs index 522b801..31ef574 100644 --- a/backend/src/Logitar.Cms.Core/Models/ActorModel.cs +++ b/backend/src/Logitar.Cms.Core/Models/ActorModel.cs @@ -1,4 +1,6 @@ -namespace Logitar.Cms.Core.Models; +using Logitar.Cms.Core.Actors; + +namespace Logitar.Cms.Core.Models; public class ActorModel { diff --git a/backend/src/Logitar.Cms.Infrastructure/Actors/ActorService.cs b/backend/src/Logitar.Cms.Infrastructure/Actors/ActorService.cs new file mode 100644 index 0000000..ac00d95 --- /dev/null +++ b/backend/src/Logitar.Cms.Infrastructure/Actors/ActorService.cs @@ -0,0 +1,56 @@ +using Logitar.Cms.Core.Models; +using Logitar.Cms.Infrastructure.Caching; +using Logitar.Cms.Infrastructure.Entities; +using Logitar.EventSourcing; +using Microsoft.EntityFrameworkCore; + +namespace Logitar.Cms.Infrastructure.Actors; + +internal class ActorService : IActorService +{ + private readonly DbSet _actors; + private readonly ICacheService _cacheService; + + public ActorService(ICacheService cacheService, CmsContext context) + { + _cacheService = cacheService; + _actors = context.Actors; + } + + public async Task> FindAsync(IEnumerable ids, CancellationToken cancellationToken) + { + int capacity = ids.Count(); + Dictionary actors = new(capacity); + HashSet missingIds = new(capacity); + + foreach (ActorId id in ids) + { + ActorModel? actor = _cacheService.GetActor(id); + if (actor == null) + { + missingIds.Add(id.ToGuid()); + } + else + { + actors[id] = actor; + } + } + + ActorEntity[] entities = await _actors.AsNoTracking() + .Where(actor => missingIds.Contains(actor.Id)) + .ToArrayAsync(cancellationToken); + foreach (ActorEntity entity in entities) + { + ActorId id = new(entity.Id); + ActorModel actor = Mapper.ToActor(entity); + actors[id] = actor; + } + + foreach (ActorModel actor in actors.Values) + { + _cacheService.SetActor(actor); + } + + return actors.Values; + } +} diff --git a/backend/src/Logitar.Cms.Infrastructure/Actors/IActorService.cs b/backend/src/Logitar.Cms.Infrastructure/Actors/IActorService.cs new file mode 100644 index 0000000..fde2729 --- /dev/null +++ b/backend/src/Logitar.Cms.Infrastructure/Actors/IActorService.cs @@ -0,0 +1,9 @@ +using Logitar.Cms.Core.Models; +using Logitar.EventSourcing; + +namespace Logitar.Cms.Infrastructure.Actors; + +public interface IActorService +{ + Task> FindAsync(IEnumerable ids, CancellationToken cancellationToken = default); +} diff --git a/backend/src/Logitar.Cms.Infrastructure/Caching/CacheService.cs b/backend/src/Logitar.Cms.Infrastructure/Caching/CacheService.cs new file mode 100644 index 0000000..060d482 --- /dev/null +++ b/backend/src/Logitar.Cms.Infrastructure/Caching/CacheService.cs @@ -0,0 +1,31 @@ +using Logitar.Cms.Core.Models; +using Logitar.Cms.Infrastructure.Settings; +using Logitar.EventSourcing; +using Microsoft.Extensions.Caching.Memory; + +namespace Logitar.Cms.Infrastructure.Caching; + +internal class CacheService : ICacheService +{ + private readonly IMemoryCache _cache; + private readonly CachingSettings _settings; + + public CacheService(IMemoryCache cache, CachingSettings settings) + { + _cache = cache; + _settings = settings; + } + + public ActorModel? GetActor(ActorId id) => TryGetValue(GetActorKey(id)); + public void SetActor(ActorModel actor) + { + SetValue(GetActorKey(new ActorId(actor.Id)), actor, _settings.ActorLifetime); + } + private static string GetActorKey(ActorId id) => $"Actor.Id:{id}"; + + private T? TryGetValue(object key) => _cache.TryGetValue(key, out object? value) ? (T?)value : default; + void SetValue(object key, T value, TimeSpan duration) + { + _cache.Set(key, value, duration); + } +} diff --git a/backend/src/Logitar.Cms.Infrastructure/Caching/ICacheService.cs b/backend/src/Logitar.Cms.Infrastructure/Caching/ICacheService.cs new file mode 100644 index 0000000..73068d9 --- /dev/null +++ b/backend/src/Logitar.Cms.Infrastructure/Caching/ICacheService.cs @@ -0,0 +1,10 @@ +using Logitar.Cms.Core.Models; +using Logitar.EventSourcing; + +namespace Logitar.Cms.Infrastructure.Caching; + +public interface ICacheService +{ + ActorModel? GetActor(ActorId id); + void SetActor(ActorModel actor); +} diff --git a/backend/src/Logitar.Cms.Infrastructure/CmsContext.cs b/backend/src/Logitar.Cms.Infrastructure/CmsContext.cs index b48ad0d..3395898 100644 --- a/backend/src/Logitar.Cms.Infrastructure/CmsContext.cs +++ b/backend/src/Logitar.Cms.Infrastructure/CmsContext.cs @@ -9,6 +9,7 @@ public CmsContext(DbContextOptions options) : base(options) { } + internal DbSet Actors => Set(); internal DbSet Languages => Set(); protected override void OnModelCreating(ModelBuilder modelBuilder) diff --git a/backend/src/Logitar.Cms.Infrastructure/CmsDb/Actors.cs b/backend/src/Logitar.Cms.Infrastructure/CmsDb/Actors.cs new file mode 100644 index 0000000..80fd873 --- /dev/null +++ b/backend/src/Logitar.Cms.Infrastructure/CmsDb/Actors.cs @@ -0,0 +1,18 @@ +using Logitar.Cms.Infrastructure.Entities; +using Logitar.Data; + +namespace Logitar.Cms.Infrastructure.CmsDb; + +public static class Actors +{ + public static readonly TableId Table = new(nameof(CmsContext.Actors)); + + public static readonly ColumnId ActorId = new(nameof(ActorEntity.ActorId), Table); + public static readonly ColumnId DisplayName = new(nameof(ActorEntity.DisplayName), Table); + public static readonly ColumnId EmailAddress = new(nameof(ActorEntity.EmailAddress), Table); + public static readonly ColumnId Id = new(nameof(ActorEntity.Id), Table); + public static readonly ColumnId IdHash = new(nameof(ActorEntity.IdHash), Table); + public static readonly ColumnId IsDeleted = new(nameof(ActorEntity.IsDeleted), Table); + public static readonly ColumnId PictureUrl = new(nameof(ActorEntity.PictureUrl), Table); + public static readonly ColumnId Type = new(nameof(ActorEntity.Type), Table); +} diff --git a/backend/src/Logitar.Cms.Infrastructure/Configurations/ActorConfiguration.cs b/backend/src/Logitar.Cms.Infrastructure/Configurations/ActorConfiguration.cs new file mode 100644 index 0000000..786ecc4 --- /dev/null +++ b/backend/src/Logitar.Cms.Infrastructure/Configurations/ActorConfiguration.cs @@ -0,0 +1,28 @@ +using Logitar.Cms.Core.Actors; +using Logitar.Cms.Infrastructure.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Logitar.Cms.Infrastructure.Configurations; + +internal class ActorConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(CmsDb.Actors.Table.Table ?? string.Empty, CmsDb.Actors.Table.Schema); + builder.HasKey(x => x.ActorId); + + builder.HasIndex(x => x.Id).IsUnique(); + builder.HasIndex(x => x.IdHash).IsUnique(); + builder.HasIndex(x => x.Type); + builder.HasIndex(x => x.IsDeleted); + builder.HasIndex(x => x.DisplayName); + builder.HasIndex(x => x.EmailAddress); + + builder.Property(x => x.Type).HasMaxLength(byte.MaxValue).HasConversion(new EnumToStringConverter()); + builder.Property(x => x.DisplayName).HasMaxLength(byte.MaxValue); // TODO(fpion): use constant + builder.Property(x => x.EmailAddress).HasMaxLength(byte.MaxValue); // TODO(fpion): use constant + builder.Property(x => x.PictureUrl).HasMaxLength(2048); // TODO(fpion): use constant + } +} diff --git a/backend/src/Logitar.Cms.Infrastructure/DependencyInjectionExtensions.cs b/backend/src/Logitar.Cms.Infrastructure/DependencyInjectionExtensions.cs index 2ecaae3..05f66f8 100644 --- a/backend/src/Logitar.Cms.Infrastructure/DependencyInjectionExtensions.cs +++ b/backend/src/Logitar.Cms.Infrastructure/DependencyInjectionExtensions.cs @@ -1,8 +1,12 @@ using Logitar.Cms.Core; using Logitar.Cms.Core.Localization; +using Logitar.Cms.Infrastructure.Actors; +using Logitar.Cms.Infrastructure.Caching; using Logitar.Cms.Infrastructure.Queriers; using Logitar.Cms.Infrastructure.Repositories; +using Logitar.Cms.Infrastructure.Settings; using Logitar.EventSourcing.Infrastructure; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace Logitar.Cms.Infrastructure; @@ -14,12 +18,22 @@ public static IServiceCollection AddLogitarCmsInfrastructure(this IServiceCollec return services .AddLogitarCmsCore() .AddMediatR(config => config.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly())) + .AddMemoryCache() + .AddSingleton(InitializeCachingSettings) + .AddSingleton() .AddSingleton() + .AddScoped() .AddScoped() .AddQueriers() .AddRepositories(); } + private static CachingSettings InitializeCachingSettings(IServiceProvider serviceProvider) + { + IConfiguration configuration = serviceProvider.GetRequiredService(); + return configuration.GetSection(CachingSettings.SectionKey).Get() ?? new(); + } + private static IServiceCollection AddQueriers(this IServiceCollection services) { return services diff --git a/backend/src/Logitar.Cms.Infrastructure/Entities/ActorEntity.cs b/backend/src/Logitar.Cms.Infrastructure/Entities/ActorEntity.cs new file mode 100644 index 0000000..66e4753 --- /dev/null +++ b/backend/src/Logitar.Cms.Infrastructure/Entities/ActorEntity.cs @@ -0,0 +1,25 @@ +using Logitar.Cms.Core.Actors; + +namespace Logitar.Cms.Infrastructure.Entities; + +internal class ActorEntity +{ + public int ActorId { get; private set; } + public Guid Id { get; private set; } + public string IdHash { get; private set; } = string.Empty; + + public ActorType Type { get; private set; } + public bool IsDeleted { get; private set; } + + public string DisplayName { get; private set; } = string.Empty; + public string? EmailAddress { get; private set; } + public string? PictureUrl { get; private set; } + + private ActorEntity() + { + } + + public override bool Equals(object? obj) => obj is ActorEntity actor && actor.Id == Id; + public override int GetHashCode() => Id.GetHashCode(); + public override string ToString() => $"{GetType()} (Id={Id})"; +} diff --git a/backend/src/Logitar.Cms.Infrastructure/Logitar.Cms.Infrastructure.csproj b/backend/src/Logitar.Cms.Infrastructure/Logitar.Cms.Infrastructure.csproj index 0223d4f..1a56011 100644 --- a/backend/src/Logitar.Cms.Infrastructure/Logitar.Cms.Infrastructure.csproj +++ b/backend/src/Logitar.Cms.Infrastructure/Logitar.Cms.Infrastructure.csproj @@ -16,6 +16,7 @@ + diff --git a/backend/src/Logitar.Cms.Infrastructure/Mapper.cs b/backend/src/Logitar.Cms.Infrastructure/Mapper.cs index e8278a9..5a59c97 100644 --- a/backend/src/Logitar.Cms.Infrastructure/Mapper.cs +++ b/backend/src/Logitar.Cms.Infrastructure/Mapper.cs @@ -23,6 +23,15 @@ public Mapper(IEnumerable actors) } } + public static ActorModel ToActor(ActorEntity actor) => new(actor.DisplayName) + { + Id = actor.Id, + Type = actor.Type, + IsDeleted = actor.IsDeleted, + EmailAddress = actor.EmailAddress, + PictureUrl = actor.PictureUrl + }; + public LanguageModel ToLanguage(LanguageEntity source) { LanguageModel destination = new() diff --git a/backend/src/Logitar.Cms.Infrastructure/Queriers/LanguageQuerier.cs b/backend/src/Logitar.Cms.Infrastructure/Queriers/LanguageQuerier.cs index 39366ba..23abd96 100644 --- a/backend/src/Logitar.Cms.Infrastructure/Queriers/LanguageQuerier.cs +++ b/backend/src/Logitar.Cms.Infrastructure/Queriers/LanguageQuerier.cs @@ -1,5 +1,7 @@ using Logitar.Cms.Core.Localization; using Logitar.Cms.Core.Localization.Models; +using Logitar.Cms.Core.Models; +using Logitar.Cms.Infrastructure.Actors; using Logitar.Cms.Infrastructure.Entities; using Logitar.EventSourcing; using Microsoft.EntityFrameworkCore; @@ -8,10 +10,12 @@ namespace Logitar.Cms.Infrastructure.Queriers; internal class LanguageQuerier : ILanguageQuerier { + private readonly IActorService _actorService; private readonly DbSet _languages; - public LanguageQuerier(CmsContext context) + public LanguageQuerier(IActorService actorService, CmsContext context) { + _actorService = actorService; _languages = context.Languages; } @@ -66,7 +70,7 @@ public async Task ReadAsync(Language language, CancellationToken public async Task ReadDefaultAsync(CancellationToken cancellationToken) { LanguageEntity language = await _languages.AsNoTracking() - .SingleOrDefaultAsync(x => x.IsDefault) + .SingleOrDefaultAsync(x => x.IsDefault, cancellationToken) ?? throw new InvalidOperationException("The default language entity could not be found."); return await MapAsync(language, cancellationToken); @@ -79,8 +83,8 @@ private async Task MapAsync(LanguageEntity language, Cancellation private async Task> MapAsync(IEnumerable languages, CancellationToken cancellationToken) { IEnumerable actorIds = languages.SelectMany(language => language.GetActorIds()); - await Task.Delay(1, cancellationToken); // TODO(fpion): actors - Mapper mapper = new(); // TODO(fpion): actors + IReadOnlyCollection actors = await _actorService.FindAsync(actorIds, cancellationToken); + Mapper mapper = new(actors); return languages.Select(mapper.ToLanguage).ToArray(); } diff --git a/backend/src/Logitar.Cms.Infrastructure/Settings/CachingSettings.cs b/backend/src/Logitar.Cms.Infrastructure/Settings/CachingSettings.cs new file mode 100644 index 0000000..3a394a5 --- /dev/null +++ b/backend/src/Logitar.Cms.Infrastructure/Settings/CachingSettings.cs @@ -0,0 +1,8 @@ +namespace Logitar.Cms.Infrastructure.Settings; + +internal record CachingSettings +{ + public const string SectionKey = "Caching"; + + public TimeSpan ActorLifetime { get; set; } = TimeSpan.FromMinutes(15); +} From 06ead317513942a903abbafe652cfe6f85ab4367 Mon Sep 17 00:00:00 2001 From: Francis Pion Date: Wed, 18 Dec 2024 23:09:37 -0500 Subject: [PATCH 5/5] maxlength --- .../Configurations/ActorConfiguration.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/src/Logitar.Cms.Infrastructure/Configurations/ActorConfiguration.cs b/backend/src/Logitar.Cms.Infrastructure/Configurations/ActorConfiguration.cs index 786ecc4..630c9a2 100644 --- a/backend/src/Logitar.Cms.Infrastructure/Configurations/ActorConfiguration.cs +++ b/backend/src/Logitar.Cms.Infrastructure/Configurations/ActorConfiguration.cs @@ -1,5 +1,6 @@ using Logitar.Cms.Core.Actors; using Logitar.Cms.Infrastructure.Entities; +using Logitar.EventSourcing; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; @@ -20,6 +21,7 @@ public void Configure(EntityTypeBuilder builder) builder.HasIndex(x => x.DisplayName); builder.HasIndex(x => x.EmailAddress); + builder.Property(x => x.IdHash).HasMaxLength(ActorId.MaximumLength); builder.Property(x => x.Type).HasMaxLength(byte.MaxValue).HasConversion(new EnumToStringConverter()); builder.Property(x => x.DisplayName).HasMaxLength(byte.MaxValue); // TODO(fpion): use constant builder.Property(x => x.EmailAddress).HasMaxLength(byte.MaxValue); // TODO(fpion): use constant