From 18b961c5112b4259702bc395fcc77a19310fb447 Mon Sep 17 00:00:00 2001 From: alsami Date: Wed, 21 Oct 2020 20:36:06 +0200 Subject: [PATCH] feat: add country-statistics aggregates --- .../CountryStatisticsAggregateProfile.cs | 18 ++++ .../Extensions/ContainerBuilderExtensions.cs | 8 ++ .../CountryAggregatesStartConfiguration.cs | 9 ++ .../000000_GlobalAggregatesMigration.cs | 1 + .../000001_CountryAggregatesMigration.cs | 71 ++++++++++++++ src/Covid19Api.Mongo.Migrator/Program.cs | 8 +- .../appsettings.json | 6 +- ...lobalStatisticAggregateUpdateDefinition.cs | 10 +- ...untryStatisticAggregateUpdateDefinition.cs | 92 +++++++++++++++++++ .../appsettings.json | 2 +- src/Covid19Api.Mongo/CollectionNames.cs | 3 +- .../Response/CountryStatisticsAggregateDto.cs | 37 ++++++++ .../ICountryStatisticsAggregatesRepository.cs | 12 +++ .../ICountryStatisticsRepository.cs | 2 + .../CountryStatisticsAggregatesRepository.cs | 55 +++++++++++ .../CountryStatisticsRepository.cs | 24 +++++ .../GlobalStatisticsAggregatesRepository.cs | 2 +- .../AggregateCountryStatisticsCommand.cs | 21 +++++ .../Queries/LoadCountryStatisticsAggregate.cs | 20 ++++ ...ggregateCountryStatisticsCommandHandler.cs | 66 +++++++++++++ ...dCountryStatisticsAggregateQueryHandler.cs | 39 ++++++++ ...adLatestCountriesStatisticsQueryHandler.cs | 4 +- .../CountryStatisticsAggregateWorker.cs | 58 ++++++++++++ 23 files changed, 555 insertions(+), 13 deletions(-) create mode 100644 src/Covid19Api.AutoMapper/CountryStatisticsAggregateProfile.cs create mode 100644 src/Covid19Api.Mongo.Migrator/Configuration/CountryAggregatesStartConfiguration.cs create mode 100644 src/Covid19Api.Mongo.Migrator/Migrations/000001_CountryAggregatesMigration.cs create mode 100644 src/Covid19Api.Mongo.Scaffolder/Updates/000003_CountryStatisticAggregateUpdateDefinition.cs create mode 100644 src/Covid19Api.Presentation/Response/CountryStatisticsAggregateDto.cs create mode 100644 src/Covid19Api.Repositories.Abstractions/ICountryStatisticsAggregatesRepository.cs create mode 100644 src/Covid19Api.Repositories/CountryStatisticsAggregatesRepository.cs create mode 100644 src/Covid19Api.UseCases.Abstractions/Commands/AggregateCountryStatisticsCommand.cs create mode 100644 src/Covid19Api.UseCases.Abstractions/Queries/LoadCountryStatisticsAggregate.cs create mode 100644 src/Covid19Api.UseCases/Commands/AggregateCountryStatisticsCommandHandler.cs create mode 100644 src/Covid19Api.UseCases/Queries/LoadCountryStatisticsAggregateQueryHandler.cs create mode 100644 src/Covid19Api.Worker/CountryStatisticsAggregateWorker.cs diff --git a/src/Covid19Api.AutoMapper/CountryStatisticsAggregateProfile.cs b/src/Covid19Api.AutoMapper/CountryStatisticsAggregateProfile.cs new file mode 100644 index 0000000..3555377 --- /dev/null +++ b/src/Covid19Api.AutoMapper/CountryStatisticsAggregateProfile.cs @@ -0,0 +1,18 @@ +using AutoMapper; +using Covid19Api.Domain; +using Covid19Api.Presentation.Response; + +namespace Covid19Api.AutoMapper +{ + public class CountryStatisticsAggregateProfile : Profile + { + public CountryStatisticsAggregateProfile() + { + this.CreateMap() + .ConvertUsing(source => new CountryStatisticsAggregateDto(source.Id, source.Country, source.CountryCode, + source.Total, source.New, source.Deaths, source.NewDeaths, source.Recovered, source.Active, + source.Month, + source.Year)); + } + } +} \ No newline at end of file diff --git a/src/Covid19Api.IoC/Extensions/ContainerBuilderExtensions.cs b/src/Covid19Api.IoC/Extensions/ContainerBuilderExtensions.cs index 3c3e0f6..18d9f39 100644 --- a/src/Covid19Api.IoC/Extensions/ContainerBuilderExtensions.cs +++ b/src/Covid19Api.IoC/Extensions/ContainerBuilderExtensions.cs @@ -33,6 +33,10 @@ public static ContainerBuilder RegisterRepositories(this ContainerBuilder builde .As() .InstancePerLifetimeScope(); + builder.RegisterType() + .As() + .InstancePerLifetimeScope(); + builder.RegisterModule(new Covid19ApiDbContextModule(hostEnvironment, configuration)); return builder; @@ -81,6 +85,10 @@ public static ContainerBuilder RegisterWorker(this ContainerBuilder builder) builder.RegisterType() .As() .InstancePerDependency(); + + builder.RegisterType() + .As() + .InstancePerDependency(); return builder; } diff --git a/src/Covid19Api.Mongo.Migrator/Configuration/CountryAggregatesStartConfiguration.cs b/src/Covid19Api.Mongo.Migrator/Configuration/CountryAggregatesStartConfiguration.cs new file mode 100644 index 0000000..e2f297d --- /dev/null +++ b/src/Covid19Api.Mongo.Migrator/Configuration/CountryAggregatesStartConfiguration.cs @@ -0,0 +1,9 @@ +namespace Covid19Api.Mongo.Migrator.Configuration +{ + public class CountryAggregatesStartConfiguration + { + public int Month { get; set; } = 9; + + public int Year { get; set; } = 2020; + } +} \ No newline at end of file diff --git a/src/Covid19Api.Mongo.Migrator/Migrations/000000_GlobalAggregatesMigration.cs b/src/Covid19Api.Mongo.Migrator/Migrations/000000_GlobalAggregatesMigration.cs index 889907a..773badf 100644 --- a/src/Covid19Api.Mongo.Migrator/Migrations/000000_GlobalAggregatesMigration.cs +++ b/src/Covid19Api.Mongo.Migrator/Migrations/000000_GlobalAggregatesMigration.cs @@ -10,6 +10,7 @@ namespace Covid19Api.Mongo.Migrator.Migrations { + // ReSharper disable once UnusedType.Global public class GlobalAggregatesMigration : DatabaseMigration { private readonly GlobalAggregatesStartConfiguration options; diff --git a/src/Covid19Api.Mongo.Migrator/Migrations/000001_CountryAggregatesMigration.cs b/src/Covid19Api.Mongo.Migrator/Migrations/000001_CountryAggregatesMigration.cs new file mode 100644 index 0000000..39145ea --- /dev/null +++ b/src/Covid19Api.Mongo.Migrator/Migrations/000001_CountryAggregatesMigration.cs @@ -0,0 +1,71 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Covid19Api.Mongo.Migrator.Abstractions; +using Covid19Api.Mongo.Migrator.Configuration; +using Covid19Api.UseCases.Abstractions.Commands; +using Covid19Api.UseCases.Abstractions.Queries; +using MediatR; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Covid19Api.Mongo.Migrator.Migrations +{ + // ReSharper disable once UnusedType.Global + public class CountryAggregatesMigration : DatabaseMigration + { + private readonly CountryAggregatesStartConfiguration options; + private readonly IMediator mediator; + + // ReSharper disable once SuggestBaseTypeForParameter + public CountryAggregatesMigration(ILogger logger, + IOptions options, IMediator mediator) : base(logger) + { + this.mediator = mediator; + this.options = options?.Value ?? throw new ArgumentNullException(nameof(options)); + } + + public override int Number => 1; + protected override string Name => nameof(CountryAggregatesMigration); + + protected override async Task ExecuteAsync() + { + var next = new DateTime(this.options.Year, this.options.Month, 1, 0, 0, 0, DateTimeKind.Utc); + var end = DateTime.UtcNow.Date; + + while (true) + { + if (next.Month > end.Month && next.Year >= end.Year) + break; + + var loadCountriesStatisticsQuery = new LoadLatestCountriesStatisticsQuery(); + var countries = (await this.mediator.Send(loadCountriesStatisticsQuery)) + .Select(country => country.Country).ToList(); + + foreach (var country in countries.ToList()) + { + var query = new LoadCountryStatisticsAggregate(country, next.Month, next.Year); + var aggregate = await this.mediator.Send(query); + + await Task.Delay(50); + if (aggregate is null) continue; + + countries.Remove(country); + } + + if (!countries.Any()) + { + next = next.AddMonths(1); + await Task.Delay(100); + continue; + } + + var command = new AggregateCountryStatisticsCommand(countries.ToArray(), next.Month, next.Year); + await this.mediator.Send(command); + + next = next.AddMonths(1); + await Task.Delay(100); + } + } + } +} \ No newline at end of file diff --git a/src/Covid19Api.Mongo.Migrator/Program.cs b/src/Covid19Api.Mongo.Migrator/Program.cs index a6d9cc7..492ee54 100644 --- a/src/Covid19Api.Mongo.Migrator/Program.cs +++ b/src/Covid19Api.Mongo.Migrator/Program.cs @@ -29,7 +29,7 @@ public static async Task Main(string[] args) foreach (var databaseMigration in migrations.OrderBy(migration => migration.Number)) await databaseMigration.ExecuteUpdateAsync(); - + await host.StopAsync(); } @@ -45,8 +45,14 @@ private static IHost CreateHost(string[] args) private static void ConfigureServices(HostBuilderContext hostBuilderContext, IServiceCollection services) { services.AddOptions(); + + services.AddHttpClient(); + services.Configure(options => hostBuilderContext.Configuration.GetSection(nameof(GlobalAggregatesStartConfiguration)).Bind(options)); + + services.Configure(options => + hostBuilderContext.Configuration.GetSection(nameof(CountryAggregatesStartConfiguration)).Bind(options)); } private static void ConfigureLogger(HostBuilderContext context, LoggerConfiguration loggerConfiguration) diff --git a/src/Covid19Api.Mongo.Migrator/appsettings.json b/src/Covid19Api.Mongo.Migrator/appsettings.json index 2dd93e3..bbb72bc 100644 --- a/src/Covid19Api.Mongo.Migrator/appsettings.json +++ b/src/Covid19Api.Mongo.Migrator/appsettings.json @@ -1,6 +1,6 @@ { "Serilog": { - "MinimumLevel": "Information", + "MinimumLevel": "Debug", "WriteTo": [ { "Name": "Console", @@ -17,5 +17,9 @@ "GlobalAggregatesStartConfiguration": { "Month": 9, "Year": 2020 + }, + "CountryAggregatesStartConfiguration": { + "Month": 9, + "Year": 2020 } } diff --git a/src/Covid19Api.Mongo.Scaffolder/Updates/000002_GlobalStatisticAggregateUpdateDefinition.cs b/src/Covid19Api.Mongo.Scaffolder/Updates/000002_GlobalStatisticAggregateUpdateDefinition.cs index b6a07b9..1834d41 100644 --- a/src/Covid19Api.Mongo.Scaffolder/Updates/000002_GlobalStatisticAggregateUpdateDefinition.cs +++ b/src/Covid19Api.Mongo.Scaffolder/Updates/000002_GlobalStatisticAggregateUpdateDefinition.cs @@ -24,7 +24,7 @@ public GlobalStatisticAggregateUpdateDefinition(ILogger .IndexKeys @@ -32,7 +32,7 @@ await this.databaseContext.Database.CreateCollectionIfNotExistsAsync(CollectionN var monthIndexModel = new CreateIndexModel(monthIndex, new CreateIndexOptions { - Name = $"{CollectionNames.GlobalStatisticsAggregate}_month_descending" + Name = $"{CollectionNames.GlobalStatisticsAggregates}_month_descending" }); var yearIndex = Builders @@ -41,7 +41,7 @@ await this.databaseContext.Database.CreateCollectionIfNotExistsAsync(CollectionN var yearIndexModel = new CreateIndexModel(yearIndex, new CreateIndexOptions { - Name = $"{CollectionNames.GlobalStatisticsAggregate}_year_descending" + Name = $"{CollectionNames.GlobalStatisticsAggregates}_year_descending" }); var yearMonthIndex = Builders @@ -51,13 +51,13 @@ await this.databaseContext.Database.CreateCollectionIfNotExistsAsync(CollectionN var yearMonthIndexModel = new CreateIndexModel(yearMonthIndex, new CreateIndexOptions { - Name = $"{CollectionNames.GlobalStatisticsAggregate}_year_month", + Name = $"{CollectionNames.GlobalStatisticsAggregates}_year_month", Unique = true }); var collection = this.databaseContext.Database.GetCollection(CollectionNames - .GlobalStatisticsAggregate); + .GlobalStatisticsAggregates); await collection.Indexes.CreateManyAsync(new[] { diff --git a/src/Covid19Api.Mongo.Scaffolder/Updates/000003_CountryStatisticAggregateUpdateDefinition.cs b/src/Covid19Api.Mongo.Scaffolder/Updates/000003_CountryStatisticAggregateUpdateDefinition.cs new file mode 100644 index 0000000..19d7d40 --- /dev/null +++ b/src/Covid19Api.Mongo.Scaffolder/Updates/000003_CountryStatisticAggregateUpdateDefinition.cs @@ -0,0 +1,92 @@ +using System.Threading.Tasks; +using Covid19Api.Domain; +using Covid19Api.Mongo.Scaffolder.Abstractions; +using Covid19Api.Mongo.Scaffolder.Extensions; +using Microsoft.Extensions.Logging; +using MongoDB.Driver; + +namespace Covid19Api.Mongo.Scaffolder.Updates +{ + // ReSharper disable once UnusedType.Global + public class CountryStatisticAggregateUpdateDefinition : DatabaseUpdateDefinition + { + private readonly Covid19ApiDbContext databaseContext; + + // ReSharper disable once SuggestBaseTypeForParameter + public CountryStatisticAggregateUpdateDefinition(ILogger logger, + Covid19ApiDbContext databaseContext) : base(logger) + { + this.databaseContext = databaseContext; + } + + public override int Version => 3; + + protected override async Task ExecuteAsync() + { + await this.databaseContext.Database.CreateCollectionIfNotExistsAsync(CollectionNames + .CountryStatisticsAggregates); + + var collection = + this.databaseContext.Database.GetCollection(CollectionNames + .CountryStatisticsAggregates); + + var countryIndex = Builders + .IndexKeys + .Ascending(statistics => statistics.Country); + + var countryIndexModel = new CreateIndexModel(countryIndex, new CreateIndexOptions + { + Name = $"{CollectionNames.CountryStatisticsAggregates}_country" + }); + + await collection.Indexes.CreateOneAsync(countryIndexModel); + + var monthIndex = Builders + .IndexKeys + .Descending(statistics => statistics.Month); + + var monthIndexModel = new CreateIndexModel(monthIndex, new CreateIndexOptions + { + Name = $"{CollectionNames.CountryStatisticsAggregates}_month_descending" + }); + + await collection.Indexes.CreateOneAsync(monthIndexModel); + + var yearIndex = Builders + .IndexKeys + .Descending(statistics => statistics.Year); + + var yearIndexModel = new CreateIndexModel(yearIndex, new CreateIndexOptions + { + Name = $"{CollectionNames.CountryStatisticsAggregates}_year_descending" + }); + + await collection.Indexes.CreateOneAsync(yearIndexModel); + + var yearMonthIndex = Builders + .IndexKeys + .Combine(yearIndex, monthIndex); + + var yearMonthIndexModel = new CreateIndexModel(yearMonthIndex, + new CreateIndexOptions + { + Name = $"{CollectionNames.CountryStatisticsAggregates}_year_month", + }); + + await collection.Indexes.CreateOneAsync(yearMonthIndexModel); + + var countryYearMonthIndex = Builders + .IndexKeys + .Combine(countryIndex, yearIndex, monthIndex); + + var countryYearMonthIndexModel = new CreateIndexModel(countryYearMonthIndex, + new CreateIndexOptions + { + Name = $"{CollectionNames.CountryStatisticsAggregates}_country_year_month", + Unique = true + }); + + await collection.Indexes.CreateOneAsync(countryYearMonthIndexModel); + } + } +} \ No newline at end of file diff --git a/src/Covid19Api.Mongo.Scaffolder/appsettings.json b/src/Covid19Api.Mongo.Scaffolder/appsettings.json index a56fe89..24902c9 100644 --- a/src/Covid19Api.Mongo.Scaffolder/appsettings.json +++ b/src/Covid19Api.Mongo.Scaffolder/appsettings.json @@ -1,6 +1,6 @@ { "Serilog": { - "MinimumLevel": "Information", + "MinimumLevel": "Debug", "WriteTo": [ { "Name": "Console", diff --git a/src/Covid19Api.Mongo/CollectionNames.cs b/src/Covid19Api.Mongo/CollectionNames.cs index a129575..5aeffcd 100644 --- a/src/Covid19Api.Mongo/CollectionNames.cs +++ b/src/Covid19Api.Mongo/CollectionNames.cs @@ -3,8 +3,9 @@ namespace Covid19Api.Mongo public static class CollectionNames { public const string GlobalStatistics = "globalStatistics"; - public const string GlobalStatisticsAggregate = "globalStatisticsAggregate"; + public const string GlobalStatisticsAggregates = "globalStatisticsAggregates"; public const string CountryStatistics = "countryStatistics"; + public const string CountryStatisticsAggregates = "countryStatisticsAggregates"; } } \ No newline at end of file diff --git a/src/Covid19Api.Presentation/Response/CountryStatisticsAggregateDto.cs b/src/Covid19Api.Presentation/Response/CountryStatisticsAggregateDto.cs new file mode 100644 index 0000000..d4b33b5 --- /dev/null +++ b/src/Covid19Api.Presentation/Response/CountryStatisticsAggregateDto.cs @@ -0,0 +1,37 @@ +using System; + +namespace Covid19Api.Presentation.Response +{ + public class CountryStatisticsAggregateDto + { + public CountryStatisticsAggregateDto(Guid id, string country, string? countryCode, int total, int @new, + int deaths, + int newDeaths, + int recovered, int active, int month, int year) + { + this.Id = id; + this.Country = country; + this.CountryCode = countryCode; + this.Total = total; + this.New = @new; + this.Deaths = deaths; + this.NewDeaths = newDeaths; + this.Recovered = recovered; + this.Active = active; + this.Month = month; + this.Year = year; + } + + public Guid Id { get; set; } + public string Country { get; set; } + public string? CountryCode { get; set; } + public int Total { get; set; } + public int New { get; set; } + public int Deaths { get; set; } + public int NewDeaths { get; set; } + public int Recovered { get; set; } + public int Active { get; set; } + public int Month { get; set; } + public int Year { get; set; } + } +} \ No newline at end of file diff --git a/src/Covid19Api.Repositories.Abstractions/ICountryStatisticsAggregatesRepository.cs b/src/Covid19Api.Repositories.Abstractions/ICountryStatisticsAggregatesRepository.cs new file mode 100644 index 0000000..68dd8cb --- /dev/null +++ b/src/Covid19Api.Repositories.Abstractions/ICountryStatisticsAggregatesRepository.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; +using Covid19Api.Domain; + +namespace Covid19Api.Repositories.Abstractions +{ + public interface ICountryStatisticsAggregatesRepository + { + Task StoreAsync(CountryStatisticsAggregate countryStatisticsAggregate); + + Task FindAsync(string country, int month, int year); + } +} \ No newline at end of file diff --git a/src/Covid19Api.Repositories.Abstractions/ICountryStatisticsRepository.cs b/src/Covid19Api.Repositories.Abstractions/ICountryStatisticsRepository.cs index bc0c5bd..e05a912 100644 --- a/src/Covid19Api.Repositories.Abstractions/ICountryStatisticsRepository.cs +++ b/src/Covid19Api.Repositories.Abstractions/ICountryStatisticsRepository.cs @@ -11,6 +11,8 @@ public interface ICountryStatisticsRepository Task> HistoricalAsync(DateTime minFetchedAt); Task> HistoricalAsync(DateTime minFetchedAt, string country); Task> HistoricalForDayAsync(DateTime minFetchedAt, string country); + Task> HistoricalInRangeAsync(string country, DateTime inclusiveStart, + DateTime exclusiveEnd); Task StoreManyAsync(IEnumerable countryStats); } } \ No newline at end of file diff --git a/src/Covid19Api.Repositories/CountryStatisticsAggregatesRepository.cs b/src/Covid19Api.Repositories/CountryStatisticsAggregatesRepository.cs new file mode 100644 index 0000000..ec6803e --- /dev/null +++ b/src/Covid19Api.Repositories/CountryStatisticsAggregatesRepository.cs @@ -0,0 +1,55 @@ +using System.Threading.Tasks; +using Covid19Api.Domain; +using Covid19Api.Mongo; +using Covid19Api.Repositories.Abstractions; +using MongoDB.Driver; + +namespace Covid19Api.Repositories +{ + public class CountryStatisticsAggregatesRepository : ICountryStatisticsAggregatesRepository + { + private readonly Covid19ApiDbContext context; + + public CountryStatisticsAggregatesRepository(Covid19ApiDbContext context) + { + this.context = context; + } + + public Task StoreAsync(CountryStatisticsAggregate countryStatisticsAggregate) + { + var collection = this.GetCollection(); + + return collection.ReplaceOneAsync(existing => existing.Id == countryStatisticsAggregate.Id, + countryStatisticsAggregate, new ReplaceOptions + { + IsUpsert = true + }); + } + + public async Task FindAsync(string country, int month, int year) + { + // ReSharper disable once SpecifyStringComparison + var countryFilter = + Builders.Filter.Where(statistics => + statistics.Country.ToLower() == country.ToLower()); + + var monthFilter = + Builders.Filter.Where(statistics => statistics.Month == month); + + var yearFilter = + Builders.Filter.Where(statistics => statistics.Year == year); + + var filter = countryFilter & monthFilter & yearFilter; + + var collection = this.GetCollection(); + + var cursor = await collection.FindAsync(filter); + + return await cursor.SingleOrDefaultAsync(); + } + + private IMongoCollection GetCollection() + => this.context.Database.GetCollection(CollectionNames + .CountryStatisticsAggregates); + } +} \ No newline at end of file diff --git a/src/Covid19Api.Repositories/CountryStatisticsRepository.cs b/src/Covid19Api.Repositories/CountryStatisticsRepository.cs index a8e163d..61d972a 100644 --- a/src/Covid19Api.Repositories/CountryStatisticsRepository.cs +++ b/src/Covid19Api.Repositories/CountryStatisticsRepository.cs @@ -98,6 +98,30 @@ public async Task> HistoricalForDayAsync(DateTime return await cursor.ToListAsync(); } + public async Task> HistoricalInRangeAsync(string country, DateTime inclusiveStart, + DateTime exclusiveEnd) + { + var collection = this.GetCollection(); + + var countryFilter = + Builders.Filter.Where( + statistics => statistics.Country.ToLower() == country.ToLower()); + + var startFilter = + Builders.Filter.Where( + statistics => statistics.FetchedAt >= inclusiveStart); + + var endFilter = + Builders.Filter.Where( + statistics => statistics.FetchedAt <= exclusiveEnd); + + var filter = countryFilter & startFilter & endFilter; + + var cursor = await collection.FindAsync(filter); + + return await cursor.ToListAsync(); + } + public async Task StoreManyAsync(IEnumerable countryStats) { var collection = this.GetCollection(); diff --git a/src/Covid19Api.Repositories/GlobalStatisticsAggregatesRepository.cs b/src/Covid19Api.Repositories/GlobalStatisticsAggregatesRepository.cs index e9b63d8..5392caf 100644 --- a/src/Covid19Api.Repositories/GlobalStatisticsAggregatesRepository.cs +++ b/src/Covid19Api.Repositories/GlobalStatisticsAggregatesRepository.cs @@ -40,6 +40,6 @@ public Task StoreAsync(GlobalStatisticsAggregate globalStatisticsAggregate) private IMongoCollection GetCollection() => this.context.Database.GetCollection(CollectionNames - .GlobalStatisticsAggregate); + .GlobalStatisticsAggregates); } } \ No newline at end of file diff --git a/src/Covid19Api.UseCases.Abstractions/Commands/AggregateCountryStatisticsCommand.cs b/src/Covid19Api.UseCases.Abstractions/Commands/AggregateCountryStatisticsCommand.cs new file mode 100644 index 0000000..9fe9a61 --- /dev/null +++ b/src/Covid19Api.UseCases.Abstractions/Commands/AggregateCountryStatisticsCommand.cs @@ -0,0 +1,21 @@ +using System; +using MediatR; + +namespace Covid19Api.UseCases.Abstractions.Commands +{ + public class AggregateCountryStatisticsCommand : IRequest + { + public AggregateCountryStatisticsCommand(string[] countries, int month, int year) + { + this.Countries = countries ?? throw new ArgumentNullException(nameof(countries)); + this.Month = month; + this.Year = year; + } + + public string[] Countries { get; } + + public int Month { get; } + + public int Year { get; } + } +} \ No newline at end of file diff --git a/src/Covid19Api.UseCases.Abstractions/Queries/LoadCountryStatisticsAggregate.cs b/src/Covid19Api.UseCases.Abstractions/Queries/LoadCountryStatisticsAggregate.cs new file mode 100644 index 0000000..7df342a --- /dev/null +++ b/src/Covid19Api.UseCases.Abstractions/Queries/LoadCountryStatisticsAggregate.cs @@ -0,0 +1,20 @@ +using Covid19Api.Presentation.Response; +using MediatR; + +namespace Covid19Api.UseCases.Abstractions.Queries +{ + public class LoadCountryStatisticsAggregate : IRequest + { + public LoadCountryStatisticsAggregate(string country, int month, int year) + { + this.Country = country; + this.Month = month; + this.Year = year; + } + + public string Country { get; } + public int Month { get; } + + public int Year { get; } + } +} \ No newline at end of file diff --git a/src/Covid19Api.UseCases/Commands/AggregateCountryStatisticsCommandHandler.cs b/src/Covid19Api.UseCases/Commands/AggregateCountryStatisticsCommandHandler.cs new file mode 100644 index 0000000..be8095b --- /dev/null +++ b/src/Covid19Api.UseCases/Commands/AggregateCountryStatisticsCommandHandler.cs @@ -0,0 +1,66 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Covid19Api.Domain; +using Covid19Api.Repositories.Abstractions; +using Covid19Api.UseCases.Abstractions.Commands; +using Covid19Api.UseCases.Extensions; +using MediatR; +using Microsoft.Extensions.Logging; + +namespace Covid19Api.UseCases.Commands +{ + public class AggregateCountryStatisticsCommandHandler : IRequestHandler + { + private readonly ILogger logger; + private readonly ICountryStatisticsRepository countryStatisticsRepository; + private readonly ICountryStatisticsAggregatesRepository countryStatisticsAggregatesRepository; + + public AggregateCountryStatisticsCommandHandler(ILogger logger, ICountryStatisticsRepository countryStatisticsRepository, ICountryStatisticsAggregatesRepository countryStatisticsAggregatesRepository) + { + this.logger = logger; + this.countryStatisticsRepository = countryStatisticsRepository; + this.countryStatisticsAggregatesRepository = countryStatisticsAggregatesRepository; + } + + public async Task Handle(AggregateCountryStatisticsCommand request, CancellationToken cancellationToken) + { + this.logger.LogInformation("Aggregating statistics for countries"); + this.logger.LogDebug("Countries that are being aggregated: {countries}", + string.Join(", ", request.Countries)); + var start = new DateTime(request.Year, request.Month, 1, 0, 0, 0, DateTimeKind.Utc); + var end = start.MonthsEnd(); + + foreach (var country in request.Countries) + { + await AggregateCountryAsync(country, start, end); + await Task.Delay(TimeSpan.FromMilliseconds(50), cancellationToken); + } + + this.logger.LogInformation("Aggregated statistics for countries"); + + return Unit.Value; + } + + private async Task AggregateCountryAsync(string country, DateTime start, DateTime end) + { + var statisticsInRange = await this.countryStatisticsRepository.HistoricalInRangeAsync(country, start, end); + + if (!statisticsInRange.Any()) return; + + var aggregate = new CountryStatisticsAggregate(country, statisticsInRange.First().CountryCode, + statisticsInRange.Sum(statistics => statistics.TotalCases), + statisticsInRange.Sum(statistics => statistics.NewCases), + statisticsInRange.Sum(statistics => statistics.TotalDeaths), + statisticsInRange.Sum(statistics => statistics.NewDeaths), + statisticsInRange.Sum(statistics => statistics.RecoveredCases), + statisticsInRange.Sum(statistics => statistics.ActiveCases), + start.Month, + start.Year + ); + + await this.countryStatisticsAggregatesRepository.StoreAsync(aggregate); + } + } +} \ No newline at end of file diff --git a/src/Covid19Api.UseCases/Queries/LoadCountryStatisticsAggregateQueryHandler.cs b/src/Covid19Api.UseCases/Queries/LoadCountryStatisticsAggregateQueryHandler.cs new file mode 100644 index 0000000..6e24351 --- /dev/null +++ b/src/Covid19Api.UseCases/Queries/LoadCountryStatisticsAggregateQueryHandler.cs @@ -0,0 +1,39 @@ +using System.Threading; +using System.Threading.Tasks; +using AutoMapper; +using Covid19Api.Domain; +using Covid19Api.Presentation.Response; +using Covid19Api.Repositories.Abstractions; +using Covid19Api.UseCases.Abstractions.Queries; +using MediatR; + +namespace Covid19Api.UseCases.Queries +{ + public class + LoadCountryStatisticsAggregateQueryHandler : IRequestHandler + { + private readonly IMapper mapper; + private readonly ICountryStatisticsAggregatesRepository countryStatisticsAggregatesRepository; + + + public LoadCountryStatisticsAggregateQueryHandler(IMapper mapper, + ICountryStatisticsAggregatesRepository countryStatisticsAggregatesRepository) + { + this.mapper = mapper; + this.countryStatisticsAggregatesRepository = countryStatisticsAggregatesRepository; + } + + public async Task Handle(LoadCountryStatisticsAggregate request, + CancellationToken cancellationToken) + { + var aggregate = + await this.countryStatisticsAggregatesRepository.FindAsync(request.Country, request.Month, + request.Year); + + return aggregate is null + ? null + : this.mapper.Map(aggregate); + } + } +} \ No newline at end of file diff --git a/src/Covid19Api.UseCases/Queries/LoadLatestCountriesStatisticsQueryHandler.cs b/src/Covid19Api.UseCases/Queries/LoadLatestCountriesStatisticsQueryHandler.cs index 0a6c0ca..f06a0ce 100644 --- a/src/Covid19Api.UseCases/Queries/LoadLatestCountriesStatisticsQueryHandler.cs +++ b/src/Covid19Api.UseCases/Queries/LoadLatestCountriesStatisticsQueryHandler.cs @@ -17,14 +17,12 @@ public class IEnumerable> { private readonly IMapper mapper; - private readonly IHtmlDocumentLoader htmlDocumentLoader; private readonly ICountryStatisticsLoader countryStatisticsLoader; - public LoadLatestCountriesStatisticsQueryHandler(IMapper mapper, IHtmlDocumentLoader htmlDocumentLoader, + public LoadLatestCountriesStatisticsQueryHandler(IMapper mapper, ICountryStatisticsLoader countryStatisticsLoader) { this.mapper = mapper; - this.htmlDocumentLoader = htmlDocumentLoader; this.countryStatisticsLoader = countryStatisticsLoader; } diff --git a/src/Covid19Api.Worker/CountryStatisticsAggregateWorker.cs b/src/Covid19Api.Worker/CountryStatisticsAggregateWorker.cs new file mode 100644 index 0000000..161b44e --- /dev/null +++ b/src/Covid19Api.Worker/CountryStatisticsAggregateWorker.cs @@ -0,0 +1,58 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Covid19Api.UseCases.Abstractions.Commands; +using Covid19Api.UseCases.Abstractions.Queries; +using MediatR; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Covid19Api.Worker +{ + public class CountryStatisticsAggregateWorker : BackgroundService + { + private readonly ILogger logger; + private readonly IServiceProvider serviceProvider; + + public CountryStatisticsAggregateWorker(ILogger logger, + IServiceProvider serviceProvider) + { + this.logger = logger; + this.serviceProvider = serviceProvider; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + var nextRun = DateTime.UtcNow; + while (!stoppingToken.IsCancellationRequested) + { + if (nextRun <= DateTime.UtcNow) + { + await this.ExecuteAggregationAsync(nextRun, stoppingToken); + nextRun = nextRun.AddHours(12); + } + + await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken); + } + } + + private async Task ExecuteAggregationAsync(DateTime nextRun, CancellationToken stoppingToken) + { + try + { + using var scope = this.serviceProvider.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + var query = new LoadLatestCountriesStatisticsQuery(); + var countries = (await mediator.Send(query, stoppingToken)).Select(country => country.Country); + var command = new AggregateCountryStatisticsCommand(countries.ToArray(), nextRun.Month, nextRun.Year); + await mediator.Send(command, stoppingToken); + } + catch (Exception e) + { + this.logger.LogCritical(e, "Error while aggregating global-statistics"); + } + } + } +} \ No newline at end of file