diff --git a/README.md b/README.md index 791d325..bd6f274 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ While `worldometers` is only displaying recent data, this API is providing histo The API is hosted on azure using an app-service. All times returned from the server are in UTC. Api base url: -https://app-covid-19-statistics-api.azurewebsites.net +https://api.alsami-covid19-statistics.dev/ A swagger definition can be found [here](https://api.alsami-covid19-statistics.dev/swagger/index.html) for testing the API. diff --git a/src/Covid19Api.Endpoints.Rest/V1/CountryStatisticsController.cs b/src/Covid19Api.Endpoints.Rest/V1/CountryStatisticsController.cs index 92769a3..70ca87c 100644 --- a/src/Covid19Api.Endpoints.Rest/V1/CountryStatisticsController.cs +++ b/src/Covid19Api.Endpoints.Rest/V1/CountryStatisticsController.cs @@ -22,7 +22,7 @@ public CountryStatisticsController(IMediator mediator) [HttpGet] public Task> LoadLatestAsync() => - this.mediator.Send(new LoadLatestCountriesStatisticsQuery()); + this.mediator.Send(new LoadCurrentStatisticsForCountyQuery()); [HttpGet("history")] public Task> LoadHistoryAsync() @@ -34,7 +34,7 @@ public Task> LoadVaryHistoryAsync( [HttpGet("{country}")] public Task LoadLatestForCountryAsync(string country) => - this.mediator.Send(new LoadLatestStatisticsForCountryQuery(country)); + this.mediator.Send(new LoadCurrentStatisticsForCountryQuery(country)); [HttpGet("{country}/history")] public Task> LoadHistoryForCountryAsync(string country) => diff --git a/src/Covid19Api.Mongo.Migrator/Migrations/000001_CountryAggregatesMigration.cs b/src/Covid19Api.Mongo.Migrator/Migrations/000001_CountryAggregatesMigration.cs index cbf7721..16b37bd 100644 --- a/src/Covid19Api.Mongo.Migrator/Migrations/000001_CountryAggregatesMigration.cs +++ b/src/Covid19Api.Mongo.Migrator/Migrations/000001_CountryAggregatesMigration.cs @@ -38,7 +38,7 @@ protected override async Task ExecuteAsync() if (end.Year < next.Year) break; - var loadCountriesStatisticsQuery = new LoadLatestCountriesStatisticsQuery(); + var loadCountriesStatisticsQuery = new LoadCurrentStatisticsForCountyQuery(); var countries = (await this.mediator.Send(loadCountriesStatisticsQuery)) .Select(country => country.Country).ToList(); diff --git a/src/Covid19Api.Repositories.Abstractions/ICountryStatisticsReadRepository.cs b/src/Covid19Api.Repositories.Abstractions/ICountryStatisticsReadRepository.cs index 447e1f1..e8e4031 100644 --- a/src/Covid19Api.Repositories.Abstractions/ICountryStatisticsReadRepository.cs +++ b/src/Covid19Api.Repositories.Abstractions/ICountryStatisticsReadRepository.cs @@ -7,12 +7,13 @@ namespace Covid19Api.Repositories.Abstractions { public interface ICountryStatisticsReadRepository { - Task MostRecentAsync(string country); Task> HistoricalAsync(DateTime minFetchedAt); Task> HistoricalAsync(DateTime minFetchedAt, string country); - Task> HistoricalForDayAsync(DateTime minFetchedAt, string country); - Task FindInRangeAsync(string country, DateTime inclusiveStart, DateTime exclusiveEnd); + + Task LoadCurrentAsync(string country); + + Task> LoadCurrentAsync(); } } \ No newline at end of file diff --git a/src/Covid19Api.Repositories.Abstractions/IGlobalStatisticsReadRepository.cs b/src/Covid19Api.Repositories.Abstractions/IGlobalStatisticsReadRepository.cs index 4980211..173059f 100644 --- a/src/Covid19Api.Repositories.Abstractions/IGlobalStatisticsReadRepository.cs +++ b/src/Covid19Api.Repositories.Abstractions/IGlobalStatisticsReadRepository.cs @@ -7,8 +7,9 @@ namespace Covid19Api.Repositories.Abstractions { public interface IGlobalStatisticsReadRepository { + Task LoadCurrentAsync(); + Task> HistoricalAsync(DateTime minFetchedAt); - Task> HistoricalForDayAsync(DateTime minFetchedAt); Task FindInRangeAsync(DateTime inclusiveStart, DateTime inclusiveEnd); } } \ No newline at end of file diff --git a/src/Covid19Api.Repositories/CountryStatisticsReadRepository.cs b/src/Covid19Api.Repositories/CountryStatisticsReadRepository.cs index 69f17e1..1bb1682 100644 --- a/src/Covid19Api.Repositories/CountryStatisticsReadRepository.cs +++ b/src/Covid19Api.Repositories/CountryStatisticsReadRepository.cs @@ -21,27 +21,6 @@ public CountryStatisticsReadRepository(Covid19ApiDbContext context) this.context = context; } - public async Task MostRecentAsync(string country) - { - var collection = this.GetCollection(); - - var sort = Builders.Sort - .Descending(nameof(CountryStatistic.TotalCases)) - .Descending(nameof(CountryStatistic.FetchedAt)); - - // ReSharper disable once SpecifyStringComparison - var cursor = await collection.FindAsync(statistics => - statistics.FetchedAt >= DateTime.UtcNow.Date.AddDays(-1) && - statistics.Country.ToLower() == country.ToLower() && - statistics.Key == EntityKeys.CountryStatistics, - new FindOptions - { - Sort = sort, - }); - - return await cursor.FirstOrDefaultAsync(); - } - public async Task> HistoricalAsync(DateTime minFetchedAt) { var collection = this.GetCollection(); @@ -90,18 +69,6 @@ public async Task> HistoricalAsync(DateTime minFet return await cursor.ToListAsync(); } - public async Task> HistoricalForDayAsync(DateTime minFetchedAt, string country) - { - var collection = this.GetCollection(); - - var cursor = await collection.FindAsync( - statistics => statistics.FetchedAt >= minFetchedAt && - statistics.Country.ToLowerInvariant() == country.ToLowerInvariant() && - statistics.Key == EntityKeys.CountryStatistics); - - return await cursor.ToListAsync(); - } - public async Task FindInRangeAsync(string country, DateTime inclusiveStart, DateTime exclusiveEnd) { @@ -135,6 +102,40 @@ public async Task> HistoricalForDayAsync(DateTime return await cursor.FirstOrDefaultAsync(); } + public Task LoadCurrentAsync(string country) + { + var keyFilter = + Builders.Filter.Where(statistics => + statistics.Key == EntityKeys.CountryStatistics); + + var countryFilter = + Builders.Filter.Where(statistic => statistic.Country.ToLower() == country.ToLower()); + + var filter = keyFilter & countryFilter; + + return this.GetCollection() + .Find(filter) + .SortByDescending(statistic => statistic.FetchedAt) + .Limit(1) + .SingleAsync(); + } + + public async Task> LoadCurrentAsync() + { + var collection = this.GetCollection(); + + var cursor = await collection + .Find(statistic => statistic.FetchedAt >= DateTime.UtcNow.Date.AddDays(-1) && statistic.Key == EntityKeys.CountryStatistics) + .SortByDescending(statistic => statistic.TotalCases) + .ToListAsync(); + + var onlyLatestEntries = cursor + .GroupBy(countryStats => countryStats.Country) + .SelectMany(grouping => grouping.Take(1)); + + return onlyLatestEntries; + } + private IMongoCollection GetCollection() => this.context.Database.GetCollection(CollectionNames.CountryStatistics); } diff --git a/src/Covid19Api.Repositories/GlobalStatisticsReadRepository.cs b/src/Covid19Api.Repositories/GlobalStatisticsReadRepository.cs index 35114f0..1024971 100644 --- a/src/Covid19Api.Repositories/GlobalStatisticsReadRepository.cs +++ b/src/Covid19Api.Repositories/GlobalStatisticsReadRepository.cs @@ -19,6 +19,15 @@ public GlobalStatisticsReadRepository(Covid19ApiDbContext context) this.context = context; } + public Task LoadCurrentAsync() + { + var collection = this.GetCollection(); + + var keyFilter = Builders.Filter.Where(global => global.Key == EntityKeys.GlobalStatistics); + + return collection.Find(keyFilter).SortByDescending(statistics => statistics.FetchedAt).Limit(1).SingleAsync(); + } + public async Task> HistoricalAsync(DateTime minFetchedAt) { var collection = this.GetCollection(); @@ -38,19 +47,6 @@ public async Task> HistoricalAsync(DateTime minFet return await cursor.ToListAsync(); } - public async Task> HistoricalForDayAsync(DateTime minFetchedAt) - { - var collection = this.GetCollection(); - - var cursor = await collection.FindAsync( - globalStatistics => globalStatistics.FetchedAt >= minFetchedAt && - globalStatistics.Key == EntityKeys.GlobalStatistics); - - var all = await cursor.ToListAsync(); - - return all.OrderBy(entry => entry.FetchedAt); - } - public async Task FindInRangeAsync(DateTime inclusiveStart, DateTime inclusiveEnd) { var collection = this.GetCollection(); diff --git a/src/Covid19Api.UseCases.Abstractions/Queries/CountryStatistics/LoadLatestStatisticsForCountryQuery.cs b/src/Covid19Api.UseCases.Abstractions/Queries/CountryStatistics/LoadCurrentStatisticsForCountryQuery.cs similarity index 60% rename from src/Covid19Api.UseCases.Abstractions/Queries/CountryStatistics/LoadLatestStatisticsForCountryQuery.cs rename to src/Covid19Api.UseCases.Abstractions/Queries/CountryStatistics/LoadCurrentStatisticsForCountryQuery.cs index 8b1bd81..c5cc970 100644 --- a/src/Covid19Api.UseCases.Abstractions/Queries/CountryStatistics/LoadLatestStatisticsForCountryQuery.cs +++ b/src/Covid19Api.UseCases.Abstractions/Queries/CountryStatistics/LoadCurrentStatisticsForCountryQuery.cs @@ -6,10 +6,10 @@ namespace Covid19Api.UseCases.Abstractions.Queries.CountryStatistics { - public sealed record LoadLatestStatisticsForCountryQuery(string Country) : ICacheableRequest, IRequest + public sealed record LoadCurrentStatisticsForCountryQuery(string Country) : ICacheableRequest, IRequest { public CacheConfiguration GetCacheConfiguration() => - new CacheConfiguration($"{nameof(LoadLatestStatisticsForCountryQuery)}_{this.Country}", + new CacheConfiguration($"{nameof(LoadCurrentStatisticsForCountryQuery)}_{this.Country}", TimeSpan.FromMinutes(30)); } } \ No newline at end of file diff --git a/src/Covid19Api.UseCases.Abstractions/Queries/CountryStatistics/LoadLatestCountriesStatisticsQuery.cs b/src/Covid19Api.UseCases.Abstractions/Queries/CountryStatistics/LoadCurrentStatisticsForCountyQuery.cs similarity index 59% rename from src/Covid19Api.UseCases.Abstractions/Queries/CountryStatistics/LoadLatestCountriesStatisticsQuery.cs rename to src/Covid19Api.UseCases.Abstractions/Queries/CountryStatistics/LoadCurrentStatisticsForCountyQuery.cs index 54cd8b8..63c8048 100644 --- a/src/Covid19Api.UseCases.Abstractions/Queries/CountryStatistics/LoadLatestCountriesStatisticsQuery.cs +++ b/src/Covid19Api.UseCases.Abstractions/Queries/CountryStatistics/LoadCurrentStatisticsForCountyQuery.cs @@ -7,9 +7,9 @@ namespace Covid19Api.UseCases.Abstractions.Queries.CountryStatistics { - public sealed record LoadLatestCountriesStatisticsQuery : ICacheableRequest, IRequest> + public sealed record LoadCurrentStatisticsForCountyQuery : ICacheableRequest, IRequest> { public CacheConfiguration GetCacheConfiguration() => - new CacheConfiguration(nameof(LoadLatestCountriesStatisticsQuery), TimeSpan.FromMinutes(30)); + new CacheConfiguration(nameof(LoadCurrentStatisticsForCountyQuery), TimeSpan.FromMinutes(30)); } } \ No newline at end of file diff --git a/src/Covid19Api.UseCases/Queries/CountryStatistics/LoadCurrentStatisticsForCountryQueryHandler.cs b/src/Covid19Api.UseCases/Queries/CountryStatistics/LoadCurrentStatisticsForCountryQueryHandler.cs new file mode 100644 index 0000000..e86f6ca --- /dev/null +++ b/src/Covid19Api.UseCases/Queries/CountryStatistics/LoadCurrentStatisticsForCountryQueryHandler.cs @@ -0,0 +1,37 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AutoMapper; +using Covid19Api.Presentation.Response; +using Covid19Api.Repositories.Abstractions; +using Covid19Api.Services.Abstractions.Loader; +using Covid19Api.UseCases.Abstractions.Queries.CountryStatistics; +using Covid19Api.UseCases.Filter; +using MediatR; + +namespace Covid19Api.UseCases.Queries.CountryStatistics +{ + public class + LoadCurrentStatisticsForCountryQueryHandler : IRequestHandler + { + private readonly IMapper mapper; + private readonly ICountryStatisticsReadRepository countryStatisticsReadRepository; + + public LoadCurrentStatisticsForCountryQueryHandler(IMapper mapper, ICountryStatisticsReadRepository countryStatisticsReadRepository) + { + this.mapper = mapper; + this.countryStatisticsReadRepository = countryStatisticsReadRepository; + } + + + public async Task Handle(LoadCurrentStatisticsForCountryQuery request, + CancellationToken cancellationToken) + { + var current = await this.countryStatisticsReadRepository.LoadCurrentAsync(request.Country); + + return this.mapper.Map(current); + } + } +} \ No newline at end of file diff --git a/src/Covid19Api.UseCases/Queries/CountryStatistics/LoadCurrentStatisticsForCountyQueryHandler.cs b/src/Covid19Api.UseCases/Queries/CountryStatistics/LoadCurrentStatisticsForCountyQueryHandler.cs new file mode 100644 index 0000000..5b0a44d --- /dev/null +++ b/src/Covid19Api.UseCases/Queries/CountryStatistics/LoadCurrentStatisticsForCountyQueryHandler.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AutoMapper; +using Covid19Api.Presentation.Response; +using Covid19Api.Repositories.Abstractions; +using Covid19Api.UseCases.Abstractions.Queries.CountryStatistics; +using MediatR; + +namespace Covid19Api.UseCases.Queries.CountryStatistics +{ + public class LoadCurrentStatisticsForCountyQueryHandler : IRequestHandler> + { + private readonly IMapper mapper; + private readonly ICountryStatisticsReadRepository countryStatisticsReadRepository; + + + public LoadCurrentStatisticsForCountyQueryHandler(IMapper mapper, ICountryStatisticsReadRepository countryStatisticsReadRepository) + { + this.mapper = mapper; + this.countryStatisticsReadRepository = countryStatisticsReadRepository; + } + + public async Task> Handle(LoadCurrentStatisticsForCountyQuery request, + CancellationToken cancellationToken) + { + var current = await this.countryStatisticsReadRepository.LoadCurrentAsync(); + + return this.mapper.Map>(current).OrderByDescending(country => country!.TotalCases); + } + } +} \ No newline at end of file diff --git a/src/Covid19Api.UseCases/Queries/CountryStatistics/LoadLatestCountriesStatisticsQueryHandler.cs b/src/Covid19Api.UseCases/Queries/CountryStatistics/LoadLatestCountriesStatisticsQueryHandler.cs deleted file mode 100644 index c47ff70..0000000 --- a/src/Covid19Api.UseCases/Queries/CountryStatistics/LoadLatestCountriesStatisticsQueryHandler.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using AutoMapper; -using Covid19Api.Presentation.Response; -using Covid19Api.Services.Abstractions.Loader; -using Covid19Api.UseCases.Abstractions.Queries.CountryStatistics; -using Covid19Api.UseCases.Filter; -using MediatR; - -namespace Covid19Api.UseCases.Queries.CountryStatistics -{ - public class - LoadLatestCountriesStatisticsQueryHandler : IRequestHandler> - { - private readonly IMapper mapper; - private readonly ICountryStatisticsLoader countryStatisticsLoader; - - public LoadLatestCountriesStatisticsQueryHandler(IMapper mapper, - ICountryStatisticsLoader countryStatisticsLoader) - { - this.mapper = mapper; - this.countryStatisticsLoader = countryStatisticsLoader; - } - - public async Task> Handle(LoadLatestCountriesStatisticsQuery request, - CancellationToken cancellationToken) - { - var fetchedAt = DateTime.UtcNow; - - var countries = await this.countryStatisticsLoader - .ParseAsync(fetchedAt, CountryStatsFilter.ValidOnly.Value); - - return this.mapper.Map>(countries) - .OrderByDescending(country => country!.TotalCases); - } - } -} \ No newline at end of file diff --git a/src/Covid19Api.UseCases/Queries/CountryStatistics/LoadLatestCountryStatisticsForCountryQueryHandler.cs b/src/Covid19Api.UseCases/Queries/CountryStatistics/LoadLatestCountryStatisticsForCountryQueryHandler.cs deleted file mode 100644 index d1858f1..0000000 --- a/src/Covid19Api.UseCases/Queries/CountryStatistics/LoadLatestCountryStatisticsForCountryQueryHandler.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using AutoMapper; -using Covid19Api.Presentation.Response; -using Covid19Api.Services.Abstractions.Loader; -using Covid19Api.UseCases.Abstractions.Queries.CountryStatistics; -using Covid19Api.UseCases.Filter; -using MediatR; - -namespace Covid19Api.UseCases.Queries.CountryStatistics -{ - public class - LoadLatestCountryStatisticsForCountryQueryHandler : IRequestHandler - { - private readonly IMapper mapper; - private readonly IHtmlDocumentLoader htmlDocumentLoader; - private readonly ICountryStatisticsLoader countryStatisticsLoader; - - public LoadLatestCountryStatisticsForCountryQueryHandler(IMapper mapper, IHtmlDocumentLoader htmlDocumentLoader, - ICountryStatisticsLoader countryStatisticsLoader) - { - this.mapper = mapper; - this.htmlDocumentLoader = htmlDocumentLoader; - this.countryStatisticsLoader = countryStatisticsLoader; - } - - public async Task Handle(LoadLatestStatisticsForCountryQuery request, - CancellationToken cancellationToken) - { - var fetchedAt = DateTime.UtcNow; - - var countries = - await this.countryStatisticsLoader.ParseAsync(fetchedAt, CountryStatsFilter.ValidOnly.Value); - - var wanted = countries - .SingleOrDefault(stats => - string.Equals(stats!.Country, request.Country, StringComparison.InvariantCultureIgnoreCase)); - - return this.mapper.Map(wanted); - } - } -} \ No newline at end of file diff --git a/src/Covid19Api.UseCases/Queries/GlobalStatistics/LoadLatestGlobalStatisticsQueryHandler.cs b/src/Covid19Api.UseCases/Queries/GlobalStatistics/LoadLatestGlobalStatisticsQueryHandler.cs index 2cff422..cfd8d45 100644 --- a/src/Covid19Api.UseCases/Queries/GlobalStatistics/LoadLatestGlobalStatisticsQueryHandler.cs +++ b/src/Covid19Api.UseCases/Queries/GlobalStatistics/LoadLatestGlobalStatisticsQueryHandler.cs @@ -1,9 +1,8 @@ -using System; using System.Threading; using System.Threading.Tasks; using AutoMapper; using Covid19Api.Presentation.Response; -using Covid19Api.Services.Abstractions.Loader; +using Covid19Api.Repositories.Abstractions; using Covid19Api.UseCases.Abstractions.Queries.GlobalStatistics; using MediatR; @@ -12,26 +11,22 @@ namespace Covid19Api.UseCases.Queries.GlobalStatistics public class LoadLatestGlobalStatisticsQueryHandler : IRequestHandler { - private readonly IHtmlDocumentLoader htmlDocumentLoader; private readonly IMapper mapper; - private readonly IGlobalStatisticsLoader globalStatisticsLoader; + private readonly IGlobalStatisticsReadRepository globalStatisticsReadRepository; - public LoadLatestGlobalStatisticsQueryHandler(IHtmlDocumentLoader htmlDocumentLoader, IMapper mapper, - IGlobalStatisticsLoader globalStatisticsLoader) + public LoadLatestGlobalStatisticsQueryHandler(IMapper mapper, IGlobalStatisticsReadRepository globalStatisticsReadRepository) { - this.htmlDocumentLoader = htmlDocumentLoader; this.mapper = mapper; - this.globalStatisticsLoader = globalStatisticsLoader; + this.globalStatisticsReadRepository = globalStatisticsReadRepository; } + public async Task Handle(LoadLatestGlobalStatisticsQuery request, CancellationToken cancellationToken) { - var fetchedAt = DateTime.UtcNow; - - var latest = await this.globalStatisticsLoader.ParseAsync(fetchedAt); + var current = await this.globalStatisticsReadRepository.LoadCurrentAsync(); - return this.mapper.Map(latest); + return this.mapper.Map(current); } } } \ No newline at end of file diff --git a/src/Covid19Api.Worker/CountryStatisticsAggregateWorker.cs b/src/Covid19Api.Worker/CountryStatisticsAggregateWorker.cs index 282fc5a..4ab7be3 100644 --- a/src/Covid19Api.Worker/CountryStatisticsAggregateWorker.cs +++ b/src/Covid19Api.Worker/CountryStatisticsAggregateWorker.cs @@ -46,7 +46,7 @@ private async Task ExecuteAggregationAsync(DateTime nextRun, CancellationToken s { using var scope = this.serviceProvider.CreateScope(); var mediator = scope.ServiceProvider.GetRequiredService(); - var query = new LoadLatestCountriesStatisticsQuery(); + var query = new LoadCurrentStatisticsForCountyQuery(); 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);