From ae81cda631bd96cf4ad0976b72f24663cafa819d Mon Sep 17 00:00:00 2001 From: Arne Dumarey Date: Tue, 14 Jan 2025 12:35:36 +0100 Subject: [PATCH 1/2] refactor(producer): convert to consoleapp --- paket.dependencies | 2 +- paket.lock | 2 +- .../{ApiModule.cs => ProducerModule.cs} | 61 ++++++- .../Infrastructure/Program.cs | 166 +++++++++++++++--- .../Infrastructure/Startup.cs | 164 +---------------- src/ParcelRegistry.Producer/ParcelProducer.cs | 40 +++++ src/ParcelRegistry.Producer/ProducerModule.cs | 70 -------- 7 files changed, 246 insertions(+), 259 deletions(-) rename src/ParcelRegistry.Producer/Infrastructure/Modules/{ApiModule.cs => ProducerModule.cs} (64%) create mode 100644 src/ParcelRegistry.Producer/ParcelProducer.cs delete mode 100644 src/ParcelRegistry.Producer/ProducerModule.cs diff --git a/paket.dependencies b/paket.dependencies index d1bb9719..e0b5984c 100755 --- a/paket.dependencies +++ b/paket.dependencies @@ -75,7 +75,7 @@ nuget Be.Vlaanderen.Basisregisters.ProjectionHandling.Connector.Testing 14.0.0 nuget Be.Vlaanderen.Basisregisters.ProjectionHandling.Testing.Xunit 14.0.0 nuget Be.Vlaanderen.Basisregisters.ProjectionHandling.Syndication 14.0.0 -nuget Be.Vlaanderen.Basisregisters.Projector 15.1.0 +nuget Be.Vlaanderen.Basisregisters.Projector 15.2.0 nuget Be.Vlaanderen.Basisregisters.Crab 4.0.0 diff --git a/paket.lock b/paket.lock index 2823df85..57285ad6 100644 --- a/paket.lock +++ b/paket.lock @@ -380,7 +380,7 @@ NUGET Microsoft.EntityFrameworkCore (>= 8.0.2) Microsoft.Extensions.Logging (>= 8.0) xunit (>= 2.7) - Be.Vlaanderen.Basisregisters.Projector (15.1) + Be.Vlaanderen.Basisregisters.Projector (15.2) Autofac (>= 8.0) Autofac.Extensions.DependencyInjection (>= 9.0) Be.Vlaanderen.Basisregisters.ProjectionHandling.Connector (>= 14.0) diff --git a/src/ParcelRegistry.Producer/Infrastructure/Modules/ApiModule.cs b/src/ParcelRegistry.Producer/Infrastructure/Modules/ProducerModule.cs similarity index 64% rename from src/ParcelRegistry.Producer/Infrastructure/Modules/ApiModule.cs rename to src/ParcelRegistry.Producer/Infrastructure/Modules/ProducerModule.cs index d231a064..86e3a5e6 100644 --- a/src/ParcelRegistry.Producer/Infrastructure/Modules/ApiModule.cs +++ b/src/ParcelRegistry.Producer/Infrastructure/Modules/ProducerModule.cs @@ -8,22 +8,24 @@ namespace ParcelRegistry.Producer.Infrastructure.Modules using Be.Vlaanderen.Basisregisters.EventHandling.Autofac; using Be.Vlaanderen.Basisregisters.MessageHandling.Kafka; using Be.Vlaanderen.Basisregisters.MessageHandling.Kafka.Producer; + using Be.Vlaanderen.Basisregisters.ProjectionHandling.Runner.SqlServer.MigrationExtensions; using Be.Vlaanderen.Basisregisters.ProjectionHandling.SqlStreamStore.Autofac; using Be.Vlaanderen.Basisregisters.Projector; using Be.Vlaanderen.Basisregisters.Projector.ConnectedProjections; using Be.Vlaanderen.Basisregisters.Projector.Modules; + using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using ParcelRegistry.Infrastructure; - public class ApiModule : Module + public class ProducerModule : Module { private readonly IConfiguration _configuration; private readonly IServiceCollection _services; private readonly ILoggerFactory _loggerFactory; - public ApiModule( + public ProducerModule( IConfiguration configuration, IServiceCollection services, ILoggerFactory loggerFactory) @@ -60,12 +62,26 @@ private void RegisterProjectionSetup(ContainerBuilder builder) private void RegisterProjections(ContainerBuilder builder) { - builder - .RegisterModule( - new ProducerModule( - _configuration, - _services, - _loggerFactory)); + var logger = _loggerFactory.CreateLogger(); + var connectionString = _configuration.GetConnectionString("ProducerProjections"); + + var hasConnectionString = !string.IsNullOrWhiteSpace(connectionString); + if (hasConnectionString) + { + RunOnSqlServer(_services, _loggerFactory, connectionString); + } + else + { + RunInMemoryDb(_services, _loggerFactory, logger); + } + + logger.LogInformation( + "Added {Context} to services:" + + Environment.NewLine + + "\tSchema: {Schema}" + + Environment.NewLine + + "\tTableName: {TableName}", + nameof(ProducerContext), Schema.Producer, MigrationTables.Producer); var connectedProjectionSettings = ConnectedProjectionSettings.Configure(x => { @@ -102,5 +118,34 @@ private void RegisterProjections(ContainerBuilder builder) return new ProducerMigrateProjections(new Producer(producerOptions)); }, connectedProjectionSettings); } + + private static void RunOnSqlServer( + IServiceCollection services, + ILoggerFactory loggerFactory, + string producerConnectionString) + { + services + .AddDbContext((_, options) => options + .UseLoggerFactory(loggerFactory) + .UseSqlServer(producerConnectionString, sqlServerOptions => + { + sqlServerOptions.EnableRetryOnFailure(); + sqlServerOptions.MigrationsHistoryTable(MigrationTables.Producer, Schema.Producer); + }) + .UseExtendedSqlServerMigrations()); + } + + private static void RunInMemoryDb( + IServiceCollection services, + ILoggerFactory loggerFactory, + ILogger logger) + { + services + .AddDbContext(options => options + .UseLoggerFactory(loggerFactory) + .UseInMemoryDatabase(Guid.NewGuid().ToString(), sqlServerOptions => { })); + + logger.LogWarning("Running InMemory for {Context}!", nameof(ProducerContext)); + } } } diff --git a/src/ParcelRegistry.Producer/Infrastructure/Program.cs b/src/ParcelRegistry.Producer/Infrastructure/Program.cs index 4f8b02b3..29f0f626 100644 --- a/src/ParcelRegistry.Producer/Infrastructure/Program.cs +++ b/src/ParcelRegistry.Producer/Infrastructure/Program.cs @@ -1,40 +1,158 @@ namespace ParcelRegistry.Producer.Infrastructure { - using Be.Vlaanderen.Basisregisters.Api; + using System; + using System.IO; + using System.Linq; + using System.Threading.Tasks; + using Autofac; + using Autofac.Extensions.DependencyInjection; using Be.Vlaanderen.Basisregisters.Aws.DistributedMutex; + using Destructurama; using Microsoft.AspNetCore.Hosting; + using Microsoft.Extensions.Configuration; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Hosting; + using Microsoft.Extensions.Logging; + using Modules; + using Serilog; + using Serilog.Debugging; + using Serilog.Extensions.Logging; public class Program { protected Program() { } - - public static void Main(string[] args) - => Run(new ProgramOptions + + public static async Task Main(string[] args) + { + AppDomain.CurrentDomain.FirstChanceException += (_, eventArgs) => + Log.Debug( + eventArgs.Exception, + "FirstChanceException event raised in {AppDomain}.", + AppDomain.CurrentDomain.FriendlyName); + + AppDomain.CurrentDomain.UnhandledException += (_, eventArgs) => + Log.Fatal((Exception)eventArgs.ExceptionObject, "Encountered a fatal exception, exiting program."); + + Log.Information("Initializing ParcelRegistry.Producer"); + + var host = new HostBuilder() + .ConfigureAppConfiguration((_, configurationBuilder) => { - Hosting = - { - HttpPort = 7014 - }, - Logging = - { - WriteTextToConsole = false, - WriteJsonToConsole = false - }, - Runtime = + configurationBuilder + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: false) + .AddJsonFile($"appsettings.{Environment.MachineName.ToLowerInvariant()}.json", optional: true, reloadOnChange: false) + .AddEnvironmentVariables() + .AddCommandLine(args); + }) + .ConfigureLogging((hostContext, loggingBuilder) => + { + SelfLog.Enable(Console.WriteLine); + + Log.Logger = new LoggerConfiguration() //NOSONAR logging configuration is safe + .ReadFrom.Configuration(hostContext.Configuration) + .Enrich.FromLogContext() + .Enrich.WithMachineName() + .Enrich.WithThreadId() + .Enrich.WithEnvironmentUserName() + .Destructure.JsonNetTypes() + .CreateLogger(); + + loggingBuilder.ClearProviders(); + loggingBuilder.AddSerilog(Log.Logger); + }) + .ConfigureServices((hostContext, services) => + { + var healthChecksBuilder = services.AddHealthChecks(); + var connectionStrings = hostContext.Configuration + .GetSection("ConnectionStrings") + .GetChildren(); + + foreach (var connectionString in connectionStrings) { - CommandLineArgs = args - }, - MiddlewareHooks = + healthChecksBuilder.AddSqlServer( + connectionString.Value, + name: $"sqlserver-{connectionString.Key.ToLowerInvariant()}", + tags: new[] { "db", "sql", "sqlserver" }); + } + + healthChecksBuilder.AddDbContextCheck( + $"dbcontext-{nameof(ProducerContext).ToLowerInvariant()}", + tags: new[] { "db", "sql", "sqlserver" }); + + var origins = hostContext.Configuration + .GetSection("Cors") + .GetChildren() + .Select(c => c.Value) + .ToArray(); + + foreach (var origin in origins) { - ConfigureDistributedLock = - configuration => DistributedLockOptions.LoadFromConfiguration(configuration) + services.AddCors(options => + { + options.AddDefaultPolicy(builder => + { + builder.WithOrigins(origin); + }); + }); } - }); + }) + .UseServiceProviderFactory(new AutofacServiceProviderFactory()) + .ConfigureContainer((hostContext, builder) => + { + var services = new ServiceCollection(); + var loggerFactory = new SerilogLoggerFactory(Log.Logger); + + builder.RegisterModule(new ProducerModule(hostContext.Configuration, services, loggerFactory)); + + builder + .RegisterType() + .As() + .SingleInstance(); + + builder.Populate(services); + }) + .ConfigureWebHostDefaults(webHostBuilder => + webHostBuilder + .UseStartup() + .UseKestrel()) + .UseConsoleLifetime() + .Build(); + + Log.Information("Starting ParcelRegistry.Producer"); + + var logger = host.Services.GetRequiredService>(); + var configuration = host.Services.GetRequiredService(); + + try + { + await DistributedLock.RunAsync( + async () => { await host.RunAsync().ConfigureAwait(false); }, + DistributedLockOptions.LoadFromConfiguration(configuration), + logger) + .ConfigureAwait(false); + } + catch (AggregateException aggregateException) + { + foreach (var innerException in aggregateException.InnerExceptions) + { + logger.LogCritical(innerException, "Encountered a fatal exception, exiting program."); + } + } + catch (Exception e) + { + logger.LogCritical(e, "Encountered a fatal exception, exiting program."); + Log.CloseAndFlush(); - private static void Run(ProgramOptions options) - => new WebHostBuilder() - .UseDefaultForApi(options) - .RunWithLock(); + // Allow some time for flushing before shutdown. + await Task.Delay(500, default); + throw; + } + finally + { + logger.LogInformation("Stopping..."); + } + } } } diff --git a/src/ParcelRegistry.Producer/Infrastructure/Startup.cs b/src/ParcelRegistry.Producer/Infrastructure/Startup.cs index c5628235..d7dad21d 100644 --- a/src/ParcelRegistry.Producer/Infrastructure/Startup.cs +++ b/src/ParcelRegistry.Producer/Infrastructure/Startup.cs @@ -1,170 +1,24 @@ namespace ParcelRegistry.Producer.Infrastructure { - using System; - using System.Linq; - using System.Reflection; - using Asp.Versioning.ApiExplorer; - using Autofac; - using Autofac.Extensions.DependencyInjection; - using Be.Vlaanderen.Basisregisters.Api; + using Be.Vlaanderen.Basisregisters.AspNetCore.Mvc.Formatters.Json; using Be.Vlaanderen.Basisregisters.Projector; - using Configuration; using Microsoft.AspNetCore.Builder; - using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; - using Microsoft.Extensions.Diagnostics.HealthChecks; - using Microsoft.Extensions.Hosting; - using Microsoft.Extensions.Logging; - using Microsoft.OpenApi.Models; - using Modules; - using ParcelRegistry.Infrastructure.Modules; + using Newtonsoft.Json; - /// Represents the startup process for the application. public class Startup { - private const string DatabaseTag = "db"; - - private IContainer _applicationContainer; - - private readonly IConfiguration _configuration; - private readonly ILoggerFactory _loggerFactory; - - public Startup( - IConfiguration configuration, - ILoggerFactory loggerFactory) - { - _configuration = configuration; - _loggerFactory = loggerFactory; - } - - /// Configures services for the application. - /// The collection of services to configure the application with. - public IServiceProvider ConfigureServices(IServiceCollection services) - { - var baseUrl = _configuration.GetValue("BaseUrl"); - var baseUrlForExceptions = baseUrl.EndsWith("/") - ? baseUrl.Substring(0, baseUrl.Length - 1) - : baseUrl; - - services - .ConfigureDefaultForApi( - new StartupConfigureOptions - { - Cors = - { - Origins = _configuration - .GetSection("Cors") - .GetChildren() - .Select(c => c.Value) - .ToArray() - }, - Server = - { - BaseUrl = baseUrlForExceptions - }, - Swagger = - { - ApiInfo = (provider, description) => new OpenApiInfo - { - Version = description.ApiVersion.ToString(), - Title = "Basisregisters Vlaanderen Parcel Registry API", - Description = GetApiLeadingText(description), - Contact = new OpenApiContact - { - Name = "Digitaal Vlaanderen", - Email = "digitaal.vlaanderen@vlaanderen.be", - Url = new Uri("https://legacy.basisregisters.vlaanderen") - } - }, - XmlCommentPaths = new[] { typeof(Startup).GetTypeInfo().Assembly.GetName().Name } - }, - MiddlewareHooks = - { - FluentValidation = fv => fv.RegisterValidatorsFromAssemblyContaining(), - - AfterHealthChecks = health => - { - var connectionStrings = _configuration - .GetSection("ConnectionStrings") - .GetChildren(); - - foreach (var connectionString in connectionStrings) - { - health.AddSqlServer( - connectionString.Value, - name: $"sqlserver-{connectionString.Key.ToLowerInvariant()}", - tags: new[] { DatabaseTag, "sql", "sqlserver" }); - } - - health.AddDbContextCheck( - $"dbcontext-{nameof(ProducerContext).ToLowerInvariant()}", - tags: new[] { DatabaseTag, "sql", "sqlserver" }); - } - } - }); - - var containerBuilder = new ContainerBuilder(); - containerBuilder.RegisterModule(new LoggingModule(_configuration, services)); - containerBuilder.RegisterModule(new ApiModule(_configuration, services, _loggerFactory)); - _applicationContainer = containerBuilder.Build(); - - return new AutofacServiceProvider(_applicationContainer); - } - - public void Configure( - IServiceProvider serviceProvider, - IApplicationBuilder app, - IWebHostEnvironment env, - IHostApplicationLifetime appLifetime, - ILoggerFactory loggerFactory, - IApiVersionDescriptionProvider apiVersionProvider, - HealthCheckService healthCheckService) + public void Configure(IApplicationBuilder app) { - StartupHelpers.CheckDatabases(healthCheckService, DatabaseTag, loggerFactory).GetAwaiter().GetResult(); + app.UseRouting(); + app.UseCors(); - app - .UseDefaultForApi(new StartupUseOptions - { - Common = - { - ApplicationContainer = _applicationContainer, - ServiceProvider = serviceProvider, - HostingEnvironment = env, - ApplicationLifetime = appLifetime, - LoggerFactory = loggerFactory - }, - Api = - { - VersionProvider = apiVersionProvider, - Info = groupName => $"Basisregisters Vlaanderen - Parcel Registry API {groupName}", - CSharpClientOptions = - { - ClassName = "ParcelRegistryProducer", - Namespace = "Be.Vlaanderen.Basisregisters" - }, - TypeScriptClientOptions = - { - ClassName = "ParcelRegistryProducer" - } - }, - MiddlewareHooks = - { - AfterMiddleware = x => x.UseMiddleware() - } - }) + app.UseHealthChecks("/health"); - .UseProjectionsManager(new ProjectionsManagerOptions - { - Common = - { - ServiceProvider = serviceProvider, - ApplicationLifetime = appLifetime - } - }); + var configuration = app.ApplicationServices.GetRequiredService(); + var baseUri = configuration.GetValue("BaseUrl").TrimEnd('/'); + app.UseProjectorEndpoints(baseUri, new JsonSerializerSettings().ConfigureDefaultForApi()); } - - private static string GetApiLeadingText(ApiVersionDescription description) - => $"Momenteel leest u de documentatie voor versie {description.ApiVersion} van de Basisregisters Vlaanderen Parcel Registry Producer API{string.Format(description.IsDeprecated ? ", **deze API versie is niet meer ondersteund * *." : ".")}"; } } diff --git a/src/ParcelRegistry.Producer/ParcelProducer.cs b/src/ParcelRegistry.Producer/ParcelProducer.cs new file mode 100644 index 00000000..f1a4815d --- /dev/null +++ b/src/ParcelRegistry.Producer/ParcelProducer.cs @@ -0,0 +1,40 @@ +namespace ParcelRegistry.Producer +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using Be.Vlaanderen.Basisregisters.Projector.ConnectedProjections; + using Microsoft.Extensions.Hosting; + using Microsoft.Extensions.Logging; + + public class ParcelProducer : BackgroundService + { + private readonly IHostApplicationLifetime _hostApplicationLifetime; + private readonly ILogger _logger; + private readonly IConnectedProjectionsManager _projectionManager; + + public ParcelProducer( + IHostApplicationLifetime hostApplicationLifetime, + IConnectedProjectionsManager projectionManager, + ILogger logger) + { + _hostApplicationLifetime = hostApplicationLifetime; + _projectionManager = projectionManager; + _logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + try + { + await _projectionManager.Start(stoppingToken); + } + catch (Exception exception) + { + _logger.LogCritical(exception, $"Critical error occured in {nameof(ParcelProducer)}."); + _hostApplicationLifetime.StopApplication(); + throw; + } + } + } +} diff --git a/src/ParcelRegistry.Producer/ProducerModule.cs b/src/ParcelRegistry.Producer/ProducerModule.cs deleted file mode 100644 index 0e9b9902..00000000 --- a/src/ParcelRegistry.Producer/ProducerModule.cs +++ /dev/null @@ -1,70 +0,0 @@ -namespace ParcelRegistry.Producer -{ - using System; - using Autofac; - using Be.Vlaanderen.Basisregisters.ProjectionHandling.Runner.SqlServer.MigrationExtensions; - using Microsoft.EntityFrameworkCore; - using Microsoft.Extensions.Configuration; - using Microsoft.Extensions.DependencyInjection; - using Microsoft.Extensions.Logging; - using ParcelRegistry.Infrastructure; - - public class ProducerModule : Module - { - public ProducerModule( - IConfiguration configuration, - IServiceCollection services, - ILoggerFactory loggerFactory) - { - var logger = loggerFactory.CreateLogger(); - var connectionString = configuration.GetConnectionString("ProducerProjections"); - - var hasConnectionString = !string.IsNullOrWhiteSpace(connectionString); - if (hasConnectionString) - { - RunOnSqlServer(services, loggerFactory, connectionString); - } - else - { - RunInMemoryDb(services, loggerFactory, logger); - } - - logger.LogInformation( - "Added {Context} to services:" + - Environment.NewLine + - "\tSchema: {Schema}" + - Environment.NewLine + - "\tTableName: {TableName}", - nameof(ProducerContext), Schema.Producer, MigrationTables.Producer); - } - - private static void RunOnSqlServer( - IServiceCollection services, - ILoggerFactory loggerFactory, - string producerConnectionString) - { - services - .AddDbContext((_, options) => options - .UseLoggerFactory(loggerFactory) - .UseSqlServer(producerConnectionString, sqlServerOptions => - { - sqlServerOptions.EnableRetryOnFailure(); - sqlServerOptions.MigrationsHistoryTable(MigrationTables.Producer, Schema.Producer); - }) - .UseExtendedSqlServerMigrations()); - } - - private static void RunInMemoryDb( - IServiceCollection services, - ILoggerFactory loggerFactory, - ILogger logger) - { - services - .AddDbContext(options => options - .UseLoggerFactory(loggerFactory) - .UseInMemoryDatabase(Guid.NewGuid().ToString(), sqlServerOptions => { })); - - logger.LogWarning("Running InMemory for {Context}!", nameof(ProducerContext)); - } - } -} From 9ef62b9c0bc4e45b977c65189e9fac588d7e8753 Mon Sep 17 00:00:00 2001 From: Arne Dumarey Date: Tue, 14 Jan 2025 13:06:54 +0100 Subject: [PATCH 2/2] refactor(snapshot): convert to consoleapp --- .../{ApiModule.cs => ProducerModule.cs} | 61 +++++- .../Infrastructure/Program.cs | 168 +++++++++++++++-- .../Infrastructure/Startup.cs | 178 +----------------- ...rcelRegistry.Producer.Snapshot.Oslo.csproj | 3 - .../ProducerModule.cs | 70 ------- .../Projections/ProjectionsController.cs | 24 --- .../SnapshotProducer.cs | 41 ++++ .../Projections/ProjectionsController.cs | 24 --- 8 files changed, 252 insertions(+), 317 deletions(-) rename src/ParcelRegistry.Producer.Snapshot.Oslo/Infrastructure/Modules/{ApiModule.cs => ProducerModule.cs} (73%) delete mode 100644 src/ParcelRegistry.Producer.Snapshot.Oslo/ProducerModule.cs delete mode 100644 src/ParcelRegistry.Producer.Snapshot.Oslo/Projections/ProjectionsController.cs create mode 100644 src/ParcelRegistry.Producer.Snapshot.Oslo/SnapshotProducer.cs delete mode 100644 src/ParcelRegistry.Producer/Projections/ProjectionsController.cs diff --git a/src/ParcelRegistry.Producer.Snapshot.Oslo/Infrastructure/Modules/ApiModule.cs b/src/ParcelRegistry.Producer.Snapshot.Oslo/Infrastructure/Modules/ProducerModule.cs similarity index 73% rename from src/ParcelRegistry.Producer.Snapshot.Oslo/Infrastructure/Modules/ApiModule.cs rename to src/ParcelRegistry.Producer.Snapshot.Oslo/Infrastructure/Modules/ProducerModule.cs index fc4bfafc..b9bab76d 100644 --- a/src/ParcelRegistry.Producer.Snapshot.Oslo/Infrastructure/Modules/ApiModule.cs +++ b/src/ParcelRegistry.Producer.Snapshot.Oslo/Infrastructure/Modules/ProducerModule.cs @@ -11,23 +11,25 @@ namespace ParcelRegistry.Producer.Snapshot.Oslo.Infrastructure.Modules using Be.Vlaanderen.Basisregisters.GrAr.Oslo.SnapshotProducer; using Be.Vlaanderen.Basisregisters.MessageHandling.Kafka; using Be.Vlaanderen.Basisregisters.MessageHandling.Kafka.Producer; + using Be.Vlaanderen.Basisregisters.ProjectionHandling.Runner.SqlServer.MigrationExtensions; using Be.Vlaanderen.Basisregisters.ProjectionHandling.SqlStreamStore.Autofac; using Be.Vlaanderen.Basisregisters.Projector; using Be.Vlaanderen.Basisregisters.Projector.ConnectedProjections; using Be.Vlaanderen.Basisregisters.Projector.Modules; + using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using NodaTime; using ParcelRegistry.Infrastructure; - public class ApiModule : Module + public class ProducerModule : Module { private readonly IConfiguration _configuration; private readonly IServiceCollection _services; private readonly ILoggerFactory _loggerFactory; - public ApiModule( + public ProducerModule( IConfiguration configuration, IServiceCollection services, ILoggerFactory loggerFactory) @@ -69,12 +71,26 @@ private void RegisterProjectionSetup(ContainerBuilder builder) private void RegisterProjections(ContainerBuilder builder) { - builder - .RegisterModule( - new ProducerModule( - _configuration, - _services, - _loggerFactory)); + var logger = _loggerFactory.CreateLogger(); + var connectionString = _configuration.GetConnectionString("ProducerSnapshotProjections"); + + var hasConnectionString = !string.IsNullOrWhiteSpace(connectionString); + if (hasConnectionString) + { + RunOnSqlServer(_services, _loggerFactory, connectionString); + } + else + { + RunInMemoryDb(_services, _loggerFactory, logger); + } + + logger.LogInformation( + "Added {Context} to services:" + + Environment.NewLine + + "\tSchema: {Schema}" + + Environment.NewLine + + "\tTableName: {TableName}", + nameof(ProducerContext), Schema.ProducerSnapshotOslo, MigrationTables.ProducerSnapshotOslo); var connectedProjectionSettings = ConnectedProjectionSettings.Configure(x => { @@ -149,5 +165,34 @@ private ProducerOptions CreateProducerOptions() return producerOptions; } + + private static void RunOnSqlServer( + IServiceCollection services, + ILoggerFactory loggerFactory, + string producerConnectionString) + { + services + .AddDbContext((_, options) => options + .UseLoggerFactory(loggerFactory) + .UseSqlServer(producerConnectionString, sqlServerOptions => + { + sqlServerOptions.EnableRetryOnFailure(); + sqlServerOptions.MigrationsHistoryTable(MigrationTables.ProducerSnapshotOslo, Schema.ProducerSnapshotOslo); + }) + .UseExtendedSqlServerMigrations()); + } + + private static void RunInMemoryDb( + IServiceCollection services, + ILoggerFactory loggerFactory, + ILogger logger) + { + services + .AddDbContext(options => options + .UseLoggerFactory(loggerFactory) + .UseInMemoryDatabase(Guid.NewGuid().ToString(), sqlServerOptions => { })); + + logger.LogWarning("Running InMemory for {Context}!", nameof(ProducerContext)); + } } } diff --git a/src/ParcelRegistry.Producer.Snapshot.Oslo/Infrastructure/Program.cs b/src/ParcelRegistry.Producer.Snapshot.Oslo/Infrastructure/Program.cs index 1d1519d9..6a88953f 100644 --- a/src/ParcelRegistry.Producer.Snapshot.Oslo/Infrastructure/Program.cs +++ b/src/ParcelRegistry.Producer.Snapshot.Oslo/Infrastructure/Program.cs @@ -1,39 +1,167 @@ namespace ParcelRegistry.Producer.Snapshot.Oslo.Infrastructure { + using System; + using System.IO; + using System.Linq; + using System.Threading.Tasks; + using Autofac; + using Autofac.Extensions.DependencyInjection; using Be.Vlaanderen.Basisregisters.Api; using Be.Vlaanderen.Basisregisters.Aws.DistributedMutex; + using Destructurama; using Microsoft.AspNetCore.Hosting; + using Microsoft.Extensions.Configuration; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Hosting; + using Microsoft.Extensions.Logging; + using Modules; + using Serilog; + using Serilog.Debugging; + using Serilog.Extensions.Logging; public sealed class Program { private Program() { } - public static void Main(string[] args) - => Run(new ProgramOptions + public static async Task Main(string[] args) + { + AppDomain.CurrentDomain.FirstChanceException += (_, eventArgs) => + Log.Debug( + eventArgs.Exception, + "FirstChanceException event raised in {AppDomain}.", + AppDomain.CurrentDomain.FriendlyName); + + AppDomain.CurrentDomain.UnhandledException += (_, eventArgs) => + Log.Fatal((Exception)eventArgs.ExceptionObject, "Encountered a fatal exception, exiting program."); + + Log.Information("Initializing ParcelRegistry.Producer.Snapshot.Oslo"); + + var host = new HostBuilder() + .ConfigureAppConfiguration((_, configurationBuilder) => { - Hosting = - { - HttpPort = 7016 - }, - Logging = + configurationBuilder + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: false) + .AddJsonFile($"appsettings.{Environment.MachineName.ToLowerInvariant()}.json", optional: true, reloadOnChange: false) + .AddEnvironmentVariables() + .AddCommandLine(args); + }) + .ConfigureLogging((hostContext, loggingBuilder) => + { + SelfLog.Enable(Console.WriteLine); + Log.Logger = new LoggerConfiguration() //NOSONAR logging configuration is safe + .ReadFrom.Configuration(hostContext.Configuration) + .Enrich.FromLogContext() + .Enrich.WithMachineName() + .Enrich.WithThreadId() + .Enrich.WithEnvironmentUserName() + .Destructure.JsonNetTypes() + .CreateLogger(); + loggingBuilder.ClearProviders(); + loggingBuilder.AddSerilog(Log.Logger); + }) + .ConfigureServices((hostContext, services) => + { + var healthChecksBuilder = services.AddHealthChecks(); + var connectionStrings = hostContext.Configuration + .GetSection("ConnectionStrings") + .GetChildren(); + + foreach (var connectionString in connectionStrings + .Where(x => !x.Value.Contains("host", StringComparison.OrdinalIgnoreCase))) { - WriteTextToConsole = false, - WriteJsonToConsole = false - }, - Runtime = + healthChecksBuilder.AddSqlServer( + connectionString.Value, + name: $"sqlserver-{connectionString.Key.ToLowerInvariant()}", + tags: new[] { "db", "sql", "sqlserver" }); + } + + foreach (var connectionString in connectionStrings + .Where(x => x.Value.Contains("host", StringComparison.OrdinalIgnoreCase))) { - CommandLineArgs = args - }, - MiddlewareHooks = + healthChecksBuilder.AddNpgSql( + connectionString.Value, + name: $"npgsql-{connectionString.Key.ToLowerInvariant()}", + tags: new[] { "db", "sql", "npgsql" }); + } + + healthChecksBuilder.AddDbContextCheck( + $"dbcontext-{nameof(ProducerContext).ToLowerInvariant()}", + tags: new[] { "db", "sql", "sqlserver" }); + + var origins = hostContext.Configuration + .GetSection("Cors") + .GetChildren() + .Select(c => c.Value) + .ToArray(); + + foreach (var origin in origins) { - ConfigureDistributedLock = DistributedLockOptions.LoadFromConfiguration + services.AddCors(options => + { + options.AddDefaultPolicy(builder => + { + builder.WithOrigins(origin); + }); + }); } - }); + }) + .UseServiceProviderFactory(new AutofacServiceProviderFactory()) + .ConfigureContainer((hostContext, builder) => + { + var services = new ServiceCollection(); + var loggerFactory = new SerilogLoggerFactory(Log.Logger); + + builder.RegisterModule(new ProducerModule(hostContext.Configuration, services, loggerFactory)); + + builder + .RegisterType() + .As() + .SingleInstance(); + + builder.Populate(services); + }) + .ConfigureWebHostDefaults(webHostBuilder => + webHostBuilder + .UseStartup() + .UseKestrel()) + .UseConsoleLifetime() + .Build(); + + Log.Information("Starting ParcelRegistry.Producer.Snapshot.Oslo"); + + var logger = host.Services.GetRequiredService>(); + var configuration = host.Services.GetRequiredService(); + + try + { + await DistributedLock.RunAsync( + async () => { await host.RunAsync().ConfigureAwait(false); }, + DistributedLockOptions.LoadFromConfiguration(configuration), + logger) + .ConfigureAwait(false); + } + catch (AggregateException aggregateException) + { + foreach (var innerException in aggregateException.InnerExceptions) + { + logger.LogCritical(innerException, "Encountered a fatal exception, exiting program."); + } + } + catch (Exception e) + { + logger.LogCritical(e, "Encountered a fatal exception, exiting program."); + Log.CloseAndFlush(); - private static void Run(ProgramOptions options) - => new WebHostBuilder() - .UseDefaultForApi(options) - .RunWithLock(); + // Allow some time for flushing before shutdown. + await Task.Delay(500, default); + throw; + } + finally + { + logger.LogInformation("Stopping..."); + } + } } } diff --git a/src/ParcelRegistry.Producer.Snapshot.Oslo/Infrastructure/Startup.cs b/src/ParcelRegistry.Producer.Snapshot.Oslo/Infrastructure/Startup.cs index 038ab40a..c2785a31 100644 --- a/src/ParcelRegistry.Producer.Snapshot.Oslo/Infrastructure/Startup.cs +++ b/src/ParcelRegistry.Producer.Snapshot.Oslo/Infrastructure/Startup.cs @@ -1,182 +1,24 @@ namespace ParcelRegistry.Producer.Snapshot.Oslo.Infrastructure { - using System; - using System.Linq; - using System.Reflection; - using Asp.Versioning.ApiExplorer; - using Autofac; - using Autofac.Extensions.DependencyInjection; - using Be.Vlaanderen.Basisregisters.Api; - using Be.Vlaanderen.Basisregisters.GrAr.Oslo.SnapshotProducer; + using Be.Vlaanderen.Basisregisters.AspNetCore.Mvc.Formatters.Json; using Be.Vlaanderen.Basisregisters.Projector; - using Configuration; using Microsoft.AspNetCore.Builder; - using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; - using Microsoft.Extensions.Diagnostics.HealthChecks; - using Microsoft.Extensions.Hosting; - using Microsoft.Extensions.Logging; - using Microsoft.OpenApi.Models; - using Modules; - using ParcelRegistry.Infrastructure.Modules; + using Newtonsoft.Json; - /// Represents the startup process for the application. - public class Startup + public sealed class Startup { - private const string DatabaseTag = "db"; - - private IContainer _applicationContainer; - - private readonly IConfiguration _configuration; - private readonly ILoggerFactory _loggerFactory; - - public Startup( - IConfiguration configuration, - ILoggerFactory loggerFactory) - { - _configuration = configuration; - _loggerFactory = loggerFactory; - } - - /// Configures services for the application. - /// The collection of services to configure the application with. - public IServiceProvider ConfigureServices(IServiceCollection services) + public void Configure(IApplicationBuilder app) { - var baseUrl = _configuration.GetValue("BaseUrl"); - var baseUrlForExceptions = baseUrl.EndsWith("/") - ? baseUrl.Substring(0, baseUrl.Length - 1) - : baseUrl; - - services - .ConfigureDefaultForApi( - new StartupConfigureOptions - { - Cors = - { - Origins = _configuration - .GetSection("Cors") - .GetChildren() - .Select(c => c.Value) - .ToArray() - }, - Server = - { - BaseUrl = baseUrlForExceptions - }, - Swagger = - { - ApiInfo = (provider, description) => new OpenApiInfo - { - Version = description.ApiVersion.ToString(), - Title = "Basisregisters Vlaanderen Parcel Registry API", - Description = GetApiLeadingText(description), - Contact = new OpenApiContact - { - Name = "Digitaal Vlaanderen", - Email = "digitaal.vlaanderen@vlaanderen.be", - Url = new Uri("https://legacy.basisregisters.vlaanderen") - } - }, - XmlCommentPaths = new[] { typeof(Startup).GetTypeInfo().Assembly.GetName().Name } - }, - MiddlewareHooks = - { - FluentValidation = fv => fv.RegisterValidatorsFromAssemblyContaining(), - - AfterHealthChecks = health => - { - var connectionStrings = _configuration - .GetSection("ConnectionStrings") - .GetChildren() - .ToArray(); - - foreach (var connectionString in connectionStrings - .Where(x => !x.Value!.Contains("host", StringComparison.OrdinalIgnoreCase))) - { - health.AddSqlServer( - connectionString.Value!, - name: $"sqlserver-{connectionString.Key.ToLowerInvariant()}", - tags: [ DatabaseTag, "sql", "sqlserver" ]); - } - foreach (var connectionString in connectionStrings - .Where(x => x.Value!.Contains("host", StringComparison.OrdinalIgnoreCase))) - { - health.AddNpgSql( - connectionString.Value!, - name: $"npgsql-{connectionString.Key.ToLowerInvariant()}", - tags: [ DatabaseTag, "sql", "npgsql" ]); - } + app.UseRouting(); + app.UseCors(); - health.AddDbContextCheck( - $"dbcontext-{nameof(ProducerContext).ToLowerInvariant()}", - tags: new[] { DatabaseTag, "sql", "sqlserver" }); - } - } - }) - .AddOsloProxy(_configuration["OsloApiUrl"]); + app.UseHealthChecks("/health"); - var containerBuilder = new ContainerBuilder(); - containerBuilder.RegisterModule(new LoggingModule(_configuration, services)); - containerBuilder.RegisterModule(new ApiModule(_configuration, services, _loggerFactory)); - _applicationContainer = containerBuilder.Build(); - - return new AutofacServiceProvider(_applicationContainer); + var configuration = app.ApplicationServices.GetRequiredService(); + var baseUri = configuration.GetValue("BaseUrl").TrimEnd('/'); + app.UseProjectorEndpoints(baseUri, new JsonSerializerSettings().ConfigureDefaultForApi()); } - - public void Configure( - IServiceProvider serviceProvider, - IApplicationBuilder app, - IWebHostEnvironment env, - IHostApplicationLifetime appLifetime, - ILoggerFactory loggerFactory, - IApiVersionDescriptionProvider apiVersionProvider, - HealthCheckService healthCheckService) - { - StartupHelpers.CheckDatabases(healthCheckService, DatabaseTag, loggerFactory).GetAwaiter().GetResult(); - - app - .UseDefaultForApi(new StartupUseOptions - { - Common = - { - ApplicationContainer = _applicationContainer, - ServiceProvider = serviceProvider, - HostingEnvironment = env, - ApplicationLifetime = appLifetime, - LoggerFactory = loggerFactory - }, - Api = - { - VersionProvider = apiVersionProvider, - Info = groupName => $"Basisregisters Vlaanderen - Parcel Registry API {groupName}", - CSharpClientOptions = - { - ClassName = "ParcelRegistryProducer", - Namespace = "Be.Vlaanderen.Basisregisters" - }, - TypeScriptClientOptions = - { - ClassName = "ParcelRegistryProducer" - } - }, - MiddlewareHooks = - { - AfterMiddleware = x => x.UseMiddleware() - } - }) - - .UseProjectionsManager(new ProjectionsManagerOptions - { - Common = - { - ServiceProvider = serviceProvider, - ApplicationLifetime = appLifetime - } - }); - } - - private static string GetApiLeadingText(ApiVersionDescription description) - => $"Momenteel leest u de documentatie voor versie {description.ApiVersion} van de Basisregisters Vlaanderen Parcel Registry Producer Snapshot Oslo API{string.Format(description.IsDeprecated ? ", **deze API versie is niet meer ondersteund * *." : ".")}"; } } diff --git a/src/ParcelRegistry.Producer.Snapshot.Oslo/ParcelRegistry.Producer.Snapshot.Oslo.csproj b/src/ParcelRegistry.Producer.Snapshot.Oslo/ParcelRegistry.Producer.Snapshot.Oslo.csproj index 9e7388cf..21f742c1 100644 --- a/src/ParcelRegistry.Producer.Snapshot.Oslo/ParcelRegistry.Producer.Snapshot.Oslo.csproj +++ b/src/ParcelRegistry.Producer.Snapshot.Oslo/ParcelRegistry.Producer.Snapshot.Oslo.csproj @@ -2,8 +2,6 @@ - - false true false @@ -17,7 +15,6 @@ - diff --git a/src/ParcelRegistry.Producer.Snapshot.Oslo/ProducerModule.cs b/src/ParcelRegistry.Producer.Snapshot.Oslo/ProducerModule.cs deleted file mode 100644 index 998883ed..00000000 --- a/src/ParcelRegistry.Producer.Snapshot.Oslo/ProducerModule.cs +++ /dev/null @@ -1,70 +0,0 @@ -namespace ParcelRegistry.Producer.Snapshot.Oslo -{ - using System; - using Autofac; - using Be.Vlaanderen.Basisregisters.ProjectionHandling.Runner.SqlServer.MigrationExtensions; - using Microsoft.EntityFrameworkCore; - using Microsoft.Extensions.Configuration; - using Microsoft.Extensions.DependencyInjection; - using Microsoft.Extensions.Logging; - using ParcelRegistry.Infrastructure; - - public class ProducerModule : Module - { - public ProducerModule( - IConfiguration configuration, - IServiceCollection services, - ILoggerFactory loggerFactory) - { - var logger = loggerFactory.CreateLogger(); - var connectionString = configuration.GetConnectionString("ProducerSnapshotProjections"); - - var hasConnectionString = !string.IsNullOrWhiteSpace(connectionString); - if (hasConnectionString) - { - RunOnSqlServer(services, loggerFactory, connectionString); - } - else - { - RunInMemoryDb(services, loggerFactory, logger); - } - - logger.LogInformation( - "Added {Context} to services:" + - Environment.NewLine + - "\tSchema: {Schema}" + - Environment.NewLine + - "\tTableName: {TableName}", - nameof(ProducerContext), Schema.ProducerSnapshotOslo, MigrationTables.ProducerSnapshotOslo); - } - - private static void RunOnSqlServer( - IServiceCollection services, - ILoggerFactory loggerFactory, - string producerConnectionString) - { - services - .AddDbContext((_, options) => options - .UseLoggerFactory(loggerFactory) - .UseSqlServer(producerConnectionString, sqlServerOptions => - { - sqlServerOptions.EnableRetryOnFailure(); - sqlServerOptions.MigrationsHistoryTable(MigrationTables.ProducerSnapshotOslo, Schema.ProducerSnapshotOslo); - }) - .UseExtendedSqlServerMigrations()); - } - - private static void RunInMemoryDb( - IServiceCollection services, - ILoggerFactory loggerFactory, - ILogger logger) - { - services - .AddDbContext(options => options - .UseLoggerFactory(loggerFactory) - .UseInMemoryDatabase(Guid.NewGuid().ToString(), sqlServerOptions => { })); - - logger.LogWarning("Running InMemory for {Context}!", nameof(ProducerContext)); - } - } -} diff --git a/src/ParcelRegistry.Producer.Snapshot.Oslo/Projections/ProjectionsController.cs b/src/ParcelRegistry.Producer.Snapshot.Oslo/Projections/ProjectionsController.cs deleted file mode 100644 index 3f1c0494..00000000 --- a/src/ParcelRegistry.Producer.Snapshot.Oslo/Projections/ProjectionsController.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace ParcelRegistry.Producer.Snapshot.Oslo.Projections -{ - using Asp.Versioning; - using Be.Vlaanderen.Basisregisters.Api; - using Be.Vlaanderen.Basisregisters.Projector.ConnectedProjections; - using Be.Vlaanderen.Basisregisters.Projector.Controllers; - using Microsoft.Extensions.Configuration; - using ParcelRegistry.Infrastructure; - - [ApiVersion("1.0")] - [ApiRoute("projections")] - public class ProjectionsController : DefaultProjectorController - { - public ProjectionsController( - IConnectedProjectionsManager connectedProjectionsManager, - IConfiguration configuration) - : base( - connectedProjectionsManager, - configuration.GetValue("BaseUrl")) - { - RegisterConnectionString(Schema.ProducerSnapshotOslo, configuration.GetConnectionString("ProducerSnapshotProjections")); - } - } -} diff --git a/src/ParcelRegistry.Producer.Snapshot.Oslo/SnapshotProducer.cs b/src/ParcelRegistry.Producer.Snapshot.Oslo/SnapshotProducer.cs new file mode 100644 index 00000000..8adf5d00 --- /dev/null +++ b/src/ParcelRegistry.Producer.Snapshot.Oslo/SnapshotProducer.cs @@ -0,0 +1,41 @@ +namespace ParcelRegistry.Producer.Snapshot.Oslo +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using Be.Vlaanderen.Basisregisters.Projector.ConnectedProjections; + using Microsoft.Extensions.Hosting; + using Microsoft.Extensions.Logging; + + public sealed class SnapshotProducer : BackgroundService + { + private readonly IConnectedProjectionsManager _projectionsManager; + private readonly IHostApplicationLifetime _hostApplicationLifetime; + private readonly ILogger _logger; + + public SnapshotProducer( + IConnectedProjectionsManager projectionsManager, + IHostApplicationLifetime hostApplicationLifetime, + ILoggerFactory loggerFactory) + { + _projectionsManager = projectionsManager; + _hostApplicationLifetime = hostApplicationLifetime; + _logger = loggerFactory.CreateLogger(); + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + try + { + _logger.LogInformation("Starting snapshot projections"); + await _projectionsManager.Start(stoppingToken); + } + catch (Exception exception) + { + _logger.LogCritical(exception, $"An error occurred while starting the {nameof(SnapshotProducer)}."); + _hostApplicationLifetime.StopApplication(); + throw; + } + } + } +} diff --git a/src/ParcelRegistry.Producer/Projections/ProjectionsController.cs b/src/ParcelRegistry.Producer/Projections/ProjectionsController.cs deleted file mode 100644 index 775ff9b5..00000000 --- a/src/ParcelRegistry.Producer/Projections/ProjectionsController.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace ParcelRegistry.Producer.Projections -{ - using Asp.Versioning; - using Be.Vlaanderen.Basisregisters.Api; - using Be.Vlaanderen.Basisregisters.Projector.ConnectedProjections; - using Be.Vlaanderen.Basisregisters.Projector.Controllers; - using Microsoft.Extensions.Configuration; - using ParcelRegistry.Infrastructure; - - [ApiVersion("1.0")] - [ApiRoute("projections")] - public class ProjectionsController : DefaultProjectorController - { - public ProjectionsController( - IConnectedProjectionsManager connectedProjectionsManager, - IConfiguration configuration) - : base( - connectedProjectionsManager, - configuration.GetValue("BaseUrl")) - { - RegisterConnectionString(Schema.Producer, configuration.GetConnectionString("ProducerProjections")); - } - } -}