Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementing Language management. #44

Merged
merged 5 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions backend/Cms.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace Logitar.Cms.Core.Models;
namespace Logitar.Cms.Core.Actors;

public enum ActorType
{
Expand Down
14 changes: 14 additions & 0 deletions backend/src/Logitar.Cms.Core/DependencyInjectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -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()));
}
}
1 change: 1 addition & 0 deletions backend/src/Logitar.Cms.Core/Logitar.Cms.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
<ItemGroup>
<Using Include="System.Diagnostics.CodeAnalysis" />
<Using Include="System.Globalization" />
<Using Include="System.Reflection" />
<Using Include="System.Text" />
<Using Include="System.Text.Json.Serialization" />
</ItemGroup>
Expand Down
4 changes: 3 additions & 1 deletion backend/src/Logitar.Cms.Core/Models/ActorModel.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
namespace Logitar.Cms.Core.Models;
using Logitar.Cms.Core.Actors;

namespace Logitar.Cms.Core.Models;

public class ActorModel
{
Expand Down
56 changes: 56 additions & 0 deletions backend/src/Logitar.Cms.Infrastructure/Actors/ActorService.cs
Original file line number Diff line number Diff line change
@@ -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<ActorEntity> _actors;
private readonly ICacheService _cacheService;

public ActorService(ICacheService cacheService, CmsContext context)
{
_cacheService = cacheService;
_actors = context.Actors;
}

public async Task<IReadOnlyCollection<ActorModel>> FindAsync(IEnumerable<ActorId> ids, CancellationToken cancellationToken)
{
int capacity = ids.Count();
Dictionary<ActorId, ActorModel> actors = new(capacity);
HashSet<Guid> 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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Logitar.Cms.Core.Models;
using Logitar.EventSourcing;

namespace Logitar.Cms.Infrastructure.Actors;

public interface IActorService
{
Task<IReadOnlyCollection<ActorModel>> FindAsync(IEnumerable<ActorId> ids, CancellationToken cancellationToken = default);
}
31 changes: 31 additions & 0 deletions backend/src/Logitar.Cms.Infrastructure/Caching/CacheService.cs
Original file line number Diff line number Diff line change
@@ -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<ActorModel>(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<T>(object key) => _cache.TryGetValue(key, out object? value) ? (T?)value : default;
void SetValue<T>(object key, T value, TimeSpan duration)
{
_cache.Set(key, value, duration);
}
}
10 changes: 10 additions & 0 deletions backend/src/Logitar.Cms.Infrastructure/Caching/ICacheService.cs
Original file line number Diff line number Diff line change
@@ -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);
}
19 changes: 19 additions & 0 deletions backend/src/Logitar.Cms.Infrastructure/CmsContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Logitar.Cms.Infrastructure.Entities;
using Microsoft.EntityFrameworkCore;

namespace Logitar.Cms.Infrastructure;

public class CmsContext : DbContext
{
public CmsContext(DbContextOptions<CmsContext> options) : base(options)
{
}

internal DbSet<ActorEntity> Actors => Set<ActorEntity>();
internal DbSet<LanguageEntity> Languages => Set<LanguageEntity>();

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
}
}
18 changes: 18 additions & 0 deletions backend/src/Logitar.Cms.Infrastructure/CmsDb/Actors.cs
Original file line number Diff line number Diff line change
@@ -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);
}
6 changes: 6 additions & 0 deletions backend/src/Logitar.Cms.Infrastructure/CmsDb/Helper.cs
Original file line number Diff line number Diff line change
@@ -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();
}
26 changes: 26 additions & 0 deletions backend/src/Logitar.Cms.Infrastructure/CmsDb/Languages.cs
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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<InitializeDatabaseCommand>
{
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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
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;

namespace Logitar.Cms.Infrastructure.Configurations;

internal class ActorConfiguration : IEntityTypeConfiguration<ActorEntity>
{
public void Configure(EntityTypeBuilder<ActorEntity> 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.IdHash).HasMaxLength(ActorId.MaximumLength);
builder.Property(x => x.Type).HasMaxLength(byte.MaxValue).HasConversion(new EnumToStringConverter<ActorType>());
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
}
}
Original file line number Diff line number Diff line change
@@ -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<T> where T : AggregateEntity // TODO(fpion): refactor
{
public virtual void Configure(EntityTypeBuilder<T> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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<LanguageEntity>, IEntityTypeConfiguration<LanguageEntity>
{
public override void Configure(EntityTypeBuilder<LanguageEntity> 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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using Logitar.Cms.Core.Localization;

namespace Logitar.Cms.Infrastructure.Converters;

internal class LanguageIdConverter : JsonConverter<LanguageId>
{
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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using Logitar.Cms.Core.Localization;

namespace Logitar.Cms.Infrastructure.Converters;

internal class LocaleConverter : JsonConverter<Locale>
{
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);
}
}
Loading
Loading