Skip to content

Commit

Permalink
Implemented a relocation tool.
Browse files Browse the repository at this point in the history
  • Loading branch information
Utar94 committed Oct 23, 2024
1 parent f94ebda commit 6cc904b
Show file tree
Hide file tree
Showing 14 changed files with 373 additions and 1 deletion.
9 changes: 8 additions & 1 deletion backend/Faktur.sln
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Faktur.EntityFrameworkCore.
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{40B8B75C-4DBD-488B-9F94-3E0626726761}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Faktur.ETL.Worker", "tools\Faktur.ETL.Worker\Faktur.ETL.Worker.csproj", "{705E503D-E904-47CD-8C42-266594D5D783}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Faktur.ETL.Worker", "tools\Faktur.ETL.Worker\Faktur.ETL.Worker.csproj", "{705E503D-E904-47CD-8C42-266594D5D783}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Faktur.Relocation.Worker", "tools\Faktur.Relocation.Worker\Faktur.Relocation.Worker.csproj", "{E1229D28-8EBE-494C-95CC-2686F8AEA1F2}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down Expand Up @@ -94,6 +96,10 @@ Global
{705E503D-E904-47CD-8C42-266594D5D783}.Debug|Any CPU.Build.0 = Debug|Any CPU
{705E503D-E904-47CD-8C42-266594D5D783}.Release|Any CPU.ActiveCfg = Release|Any CPU
{705E503D-E904-47CD-8C42-266594D5D783}.Release|Any CPU.Build.0 = Release|Any CPU
{E1229D28-8EBE-494C-95CC-2686F8AEA1F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E1229D28-8EBE-494C-95CC-2686F8AEA1F2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E1229D28-8EBE-494C-95CC-2686F8AEA1F2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E1229D28-8EBE-494C-95CC-2686F8AEA1F2}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -103,6 +109,7 @@ Global
{16B07B53-284A-4D6A-88E4-CB20CACCC4C5} = {D6C0BA2F-F805-4BAB-AFD7-9D5A92DF1352}
{10259F5B-ABEA-4ED5-AB22-DA3F4D79326D} = {D6C0BA2F-F805-4BAB-AFD7-9D5A92DF1352}
{705E503D-E904-47CD-8C42-266594D5D783} = {40B8B75C-4DBD-488B-9F94-3E0626726761}
{E1229D28-8EBE-494C-95CC-2686F8AEA1F2} = {40B8B75C-4DBD-488B-9F94-3E0626726761}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C3414F6E-6810-45E8-ADAA-2782088197F3}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using Logitar.EventSourcing;
using Logitar.EventSourcing.EntityFrameworkCore.Relational;
using Logitar.EventSourcing.Infrastructure;
using MediatR;
using Microsoft.EntityFrameworkCore;

namespace Faktur.Relocation.Worker.Commands;

internal record ExtractChangesCommand : IRequest<IReadOnlyCollection<DomainEvent>>;

internal class ExtractChangesCommandHandler : IRequestHandler<ExtractChangesCommand, IReadOnlyCollection<DomainEvent>>
{
private readonly IEventSerializer _eventSerializer;
private readonly ILogger<ExtractChangesCommandHandler> _logger;
private readonly SourceContext _source;

public ExtractChangesCommandHandler(IEventSerializer eventSerializer, ILogger<ExtractChangesCommandHandler> logger, SourceContext source)
{
_eventSerializer = eventSerializer;
_logger = logger;
_source = source;
}

public async Task<IReadOnlyCollection<DomainEvent>> Handle(ExtractChangesCommand _, CancellationToken cancellationToken)
{
EventEntity[] events = await _source.Events.AsNoTracking().ToArrayAsync(cancellationToken);
_logger.LogInformation("Extracted {EventCount} {EventText} from source database.", events.Length, events.Length > 1 ? "events" : "event");

return events.Select(_eventSerializer.Deserialize).ToArray().AsReadOnly();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using Logitar.EventSourcing;
using Logitar.EventSourcing.Infrastructure;
using MediatR;

namespace Faktur.Relocation.Worker.Commands;

internal record LoadChangesCommand(IEnumerable<DomainEvent> Changes) : IRequest;

internal class LoadChangesCommandHandler : IRequestHandler<LoadChangesCommand>
{
private readonly IEventBus _bus;
private readonly ILogger<LoadChangesCommandHandler> _logger;

public LoadChangesCommandHandler(IEventBus bus, ILogger<LoadChangesCommandHandler> logger)
{
_bus = bus;
_logger = logger;
}

public async Task Handle(LoadChangesCommand command, CancellationToken cancellationToken)
{
// TODO(fpion): upsert events

int count = command.Changes.Count();
int index = 0;
double percentage;
foreach (DomainEvent change in command.Changes)
{
await _bus.PublishAsync(change, cancellationToken);

index++;
percentage = index / (double)count;
_logger.LogInformation(
"[{Index}/{Count}] ({Percentage}) Handled '{EventType}'.",
index,
count,
percentage.ToString("P2"),
change.GetType());
}
}
}
28 changes: 28 additions & 0 deletions backend/tools/Faktur.Relocation.Worker/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.

# This stage is used when running from VS in fast mode (Default for Debug configuration)
FROM mcr.microsoft.com/dotnet/runtime:8.0 AS base
USER app
WORKDIR /app


# This stage is used to build the service project
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["tools/Faktur.Relocation.Worker/Faktur.Relocation.Worker.csproj", "tools/Faktur.Relocation.Worker/"]
RUN dotnet restore "./tools/Faktur.Relocation.Worker/Faktur.Relocation.Worker.csproj"
COPY . .
WORKDIR "/src/tools/Faktur.Relocation.Worker"
RUN dotnet build "./Faktur.Relocation.Worker.csproj" -c $BUILD_CONFIGURATION -o /app/build

# This stage is used to publish the service project to be copied to the final stage
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./Faktur.Relocation.Worker.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false

# This stage is used in production or when running from VS in regular mode (Default when not using the Debug configuration)
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "Faktur.Relocation.Worker.dll"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<Project Sdk="Microsoft.NET.Sdk.Worker">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>dotnet-Faktur.Relocation.Worker-134ea5cb-41d2-4c77-9686-2f758cb3cdec</UserSecretsId>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<DockerfileContext>..\..</DockerfileContext>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
</PropertyGroup>

<ItemGroup>
<Content Remove="secrets.sample.json" />
</ItemGroup>

<ItemGroup>
<None Include="secrets.sample.json" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Logitar" Version="6.3.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Faktur.EntityFrameworkCore.PostgreSQL\Faktur.EntityFrameworkCore.PostgreSQL.csproj" />
<ProjectReference Include="..\..\src\Faktur.EntityFrameworkCore.SqlServer\Faktur.EntityFrameworkCore.SqlServer.csproj" />
</ItemGroup>

<ItemGroup>
<Using Include="System.Diagnostics" />
<Using Include="System.Reflection" />
</ItemGroup>
</Project>
15 changes: 15 additions & 0 deletions backend/tools/Faktur.Relocation.Worker/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace Faktur.Relocation.Worker;

internal class Program
{
private static void Main(string[] args)
{
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

Startup startup = new(builder.Configuration);
startup.ConfigureServices(builder.Services);

IHost host = builder.Build();
host.Run();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"profiles": {
"Faktur.Relocation.Worker": {
"commandName": "Project",
"environmentVariables": {
"DOTNET_ENVIRONMENT": "Development"
},
"dotnetRunMessages": true
},
"Container (Dockerfile)": {
"commandName": "Docker"
}
},
"$schema": "http://json.schemastore.org/launchsettings.json"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Faktur.Infrastructure;

namespace Faktur.Relocation.Worker.Settings;

internal record DatabaseSettings
{
public string ConnectionString { get; set; } = string.Empty;
public DatabaseProvider DatabaseProvider { get; set; }
}
18 changes: 18 additions & 0 deletions backend/tools/Faktur.Relocation.Worker/SourceContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using Logitar.EventSourcing.EntityFrameworkCore.Relational;
using Microsoft.EntityFrameworkCore;

namespace Faktur.Relocation.Worker;

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

internal DbSet<EventEntity> Events { get; private set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfiguration(new EventConfiguration());
}
}
54 changes: 54 additions & 0 deletions backend/tools/Faktur.Relocation.Worker/Startup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using Faktur.EntityFrameworkCore.PostgreSQL;
using Faktur.EntityFrameworkCore.SqlServer;
using Faktur.Infrastructure;
using Faktur.Relocation.Worker.Settings;
using Microsoft.EntityFrameworkCore;

namespace Faktur.Relocation.Worker;

internal class Startup
{
private readonly IConfiguration _configuration;

public Startup(IConfiguration configuration)
{
_configuration = configuration;
}

public void ConfigureServices(IServiceCollection services)
{
services.AddHostedService<Worker>();
services.AddMediatR(config => config.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()));

AddDestination(services);
AddSource(services);
}

private void AddDestination(IServiceCollection services)
{
DatabaseSettings settings = _configuration.GetSection("Destination").Get<DatabaseSettings>() ?? new();
switch (settings.DatabaseProvider)
{
case DatabaseProvider.EntityFrameworkCorePostgreSQL:
services.AddFakturWithEntityFrameworkCorePostgreSQL(settings.ConnectionString);
break;
case DatabaseProvider.EntityFrameworkCoreSqlServer:
services.AddFakturWithEntityFrameworkCoreSqlServer(settings.ConnectionString);
break;
}
}

private void AddSource(IServiceCollection services)
{
DatabaseSettings settings = _configuration.GetSection("Source").Get<DatabaseSettings>() ?? new();
switch (settings.DatabaseProvider)
{
case DatabaseProvider.EntityFrameworkCorePostgreSQL:
services.AddDbContext<SourceContext>(options => options.UseNpgsql(settings.ConnectionString));
break;
case DatabaseProvider.EntityFrameworkCoreSqlServer:
services.AddDbContext<SourceContext>(options => options.UseSqlServer(settings.ConnectionString));
break;
}
}
}
83 changes: 83 additions & 0 deletions backend/tools/Faktur.Relocation.Worker/Worker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using Faktur.Relocation.Worker.Commands;
using Logitar.EventSourcing;
using MediatR;

namespace Faktur.Relocation.Worker;

internal class Worker : BackgroundService
{
private const string ActorIdKey = "ActorId";
private const string GenericErrorMessage = "An unhandled exception occurred.";

private readonly IConfiguration _configuration;
private readonly IHostApplicationLifetime _hostApplicationLifetime;
private readonly ILogger<Worker> _logger;
private readonly IServiceProvider _serviceProvider;

private LogLevel _result = LogLevel.Information; // NOTE(fpion): "Information" means success.

public Worker(IConfiguration configuration, IHostApplicationLifetime hostApplicationLifetime, ILogger<Worker> logger, IServiceProvider serviceProvider)
{
_configuration = configuration;
_hostApplicationLifetime = hostApplicationLifetime;
_logger = logger;
_serviceProvider = serviceProvider;
}

protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
Stopwatch chrono = Stopwatch.StartNew();
_logger.LogInformation("Worker executing at {Timestamp}.", DateTimeOffset.Now);

using IServiceScope scope = _serviceProvider.CreateScope();
IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();

try
{
IReadOnlyCollection<DomainEvent> changes = await mediator.Send(new ExtractChangesCommand(), cancellationToken);

ActorId actorId = GetActorId();
foreach (DomainEvent change in changes)
{
change.ActorId = actorId;
}

await mediator.Send(new LoadChangesCommand(changes), cancellationToken);
}
catch (Exception exception)
{
_logger.LogError(exception, GenericErrorMessage);
_result = LogLevel.Error;

Environment.ExitCode = exception.HResult;
}
finally
{
chrono.Stop();

long seconds = chrono.ElapsedMilliseconds / 1000;
string secondText = seconds <= 1 ? "second" : "seconds";
switch (_result)
{
case LogLevel.Error:
_logger.LogError("Worker failed after {Elapsed}ms ({Seconds} {SecondText}).", chrono.ElapsedMilliseconds, seconds, secondText);
break;
case LogLevel.Warning:
_logger.LogWarning("Worker completed with warnings in {Elapsed}ms ({Seconds} {SecondText}).", chrono.ElapsedMilliseconds, seconds, secondText);
break;
default:
_logger.LogInformation("Worker succeeded in {Elapsed}ms ({Seconds} {SecondText}).", chrono.ElapsedMilliseconds, seconds, secondText);
break;
}

_hostApplicationLifetime.StopApplication();
}
}
private ActorId GetActorId()
{
string? value = _configuration.GetValue<string>(ActorIdKey);
return string.IsNullOrWhiteSpace(value)
? throw new InvalidOperationException($"The configuration '{ActorIdKey}' is required.")
: new(value);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
8 changes: 8 additions & 0 deletions backend/tools/Faktur.Relocation.Worker/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
Loading

0 comments on commit 6cc904b

Please sign in to comment.