Skip to content

Commit

Permalink
Implement WeatherForecast Application layer.
Browse files Browse the repository at this point in the history
Add Forecasts via WeatherForecast service.
  • Loading branch information
Alejandro Del Rincón López committed Sep 28, 2023
1 parent 8057c25 commit 5f04e73
Show file tree
Hide file tree
Showing 17 changed files with 358 additions and 26 deletions.
23 changes: 23 additions & 0 deletions BlazorApp1.Application.Unit.Tests/AutoMoqDataAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using AutoFixture;
using AutoFixture.AutoMoq;

namespace BlazorApp1.Application.Unit.Tests;

public class AutoMoqDataAttribute : AutoDataAttribute
{
public AutoMoqDataAttribute() : base(Factory())
{
}

private static Func<IFixture> Factory()
{
return () =>
{
var fixture = new Fixture();
fixture.Customize<DateOnly>(composer => composer.FromFactory<DateTime>(DateOnly.FromDateTime));
return fixture.Customize(new AutoMoqCustomization());
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="AutoFixture.AutoMoq" Version="4.18.0" />
<PackageReference Include="AutoFixture.Xunit2" Version="4.18.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageReference Include="Moq" Version="4.20.69" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.2.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\BlazorApp1.Application\BlazorApp1.Application.csproj" />
</ItemGroup>

</Project>
5 changes: 5 additions & 0 deletions BlazorApp1.Application.Unit.Tests/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
global using AutoFixture.Xunit2;
global using Moq;
global using System.Threading;
global using System.Threading.Tasks;
global using Xunit;
85 changes: 85 additions & 0 deletions BlazorApp1.Application.Unit.Tests/WeatherForecastServiceTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@

using AutoMapper;
using BlazorApp1.Application.Queue;
using BlazorApp1.Data.Abstractions.Repositories;
using BlazorApp1.Domain;
using BlazorApp1.Server.Abstractions.Contracts;
using FluentAssertions.Execution;

namespace BlazorApp1.Application.Unit.Tests;

public class WeatherForecastServiceTests
{
[Theory, AutoMoqData]
public async Task AddForecast_ShouldAddForecastToRepositoryAndQueueBackgroundTask(
[Frozen] Mock<IWeatherForecastRepository> weatherForecastRepositoryMock,
[Frozen] Mock<IBackgroundTaskQueue> backgroundTaskQueueMock,
[Frozen] Mock<IMapper> mapperMock,
WeatherForecastDto weatherForecastDto,
WeatherForecast weatherForecast,
WeatherForecastService sut)
{
// Arrange
mapperMock.Setup(m => m.Map<WeatherForecast>(weatherForecastDto)).Returns(weatherForecast);

// Act
await sut.AddForecast(weatherForecastDto);

// Assert
weatherForecastRepositoryMock.Verify(r => r.AddWeatherForecast(weatherForecast), Times.Once);
backgroundTaskQueueMock.Verify(q => q.QueueBackgroundWorkItemAsync(It.IsAny<Func<IServiceProvider, CancellationToken, ValueTask>>()), Times.Once);
}

[Theory, AutoMoqData]
public async Task ProcessWeatherSummary_WhenExistingForecast_ShouldUpdateSummaryToRepository(
[Frozen] Mock<IWeatherForecastRepository> weatherForecastRepositoryMock,
[Frozen] Mock<IWeatherForecastSummaryRepository> weatherForecastSummaryRepositoryMock,
WeatherForecast[] weatherForecasts,
WeatherForecastSummary weatherForecastSummary,
WeatherForecastService sut)
{
// Arrange
weatherForecastRepositoryMock.Setup(r => r.GetAllForecasts()).ReturnsAsync(weatherForecasts);
weatherForecastSummaryRepositoryMock.Setup(r => r.GetForecastSummaryByDate(It.IsAny<DateOnly>())).ReturnsAsync(weatherForecastSummary);

// Act
await sut.ProcessWeatherSummary(weatherForecasts.First(), CancellationToken.None);

// Assert
using var scope = new AssertionScope();

weatherForecastSummaryRepositoryMock.Verify(r => r.GetForecastSummaryByDate(DateOnly.FromDateTime(weatherForecasts.First().Date)), Times.Once);
weatherForecastSummaryRepositoryMock.Verify(r => r.UpdateWeatherForecastSummary(It.IsAny<WeatherForecastSummary>()), Times.Once);
weatherForecastSummaryRepositoryMock.VerifyNoOtherCalls();
}

[Theory, AutoMoqData]
public async Task ProcessWeatherSummary_WhenNoExistingForecast_ShouldAddSummaryToRepository(
[Frozen] Mock<IWeatherForecastRepository> weatherForecastRepositoryMock,
[Frozen] Mock<IWeatherForecastSummaryRepository> weatherForecastSummaryRepositoryMock,
List<WeatherForecast> weatherForecasts,
WeatherForecast weatherForecast,
WeatherForecastService sut)
{
// Arrange
weatherForecast.Date = weatherForecasts.First().Date;
weatherForecasts.Add(weatherForecast);

weatherForecastRepositoryMock.Setup(r => r.GetAllForecasts()).ReturnsAsync(weatherForecasts);
weatherForecastSummaryRepositoryMock.Setup(r => r.GetForecastSummaryByDate(It.IsAny<DateOnly>())).ReturnsAsync((WeatherForecastSummary)null);

// Act
await sut.ProcessWeatherSummary(weatherForecast, CancellationToken.None);

// Assert
using var scope = new AssertionScope();

weatherForecastSummaryRepositoryMock.Verify(r => r.GetForecastSummaryByDate(DateOnly.FromDateTime(weatherForecast.Date)), Times.Once);

var sameDateWeatherforecasts = weatherForecasts.Where(x => DateOnly.FromDateTime(x.Date) == DateOnly.FromDateTime(weatherForecast.Date));
int tempAcg = (int)Math.Round(sameDateWeatherforecasts.Average(x => x.TemperatureC), 0);

weatherForecastSummaryRepositoryMock.Verify(r => r.AddWeatherForecastSummary(It.Is<WeatherForecastSummary>(s => s.Date == DateOnly.FromDateTime(weatherForecast.Date) && s.TemperatureC == tempAcg)), Times.Once);
weatherForecastSummaryRepositoryMock.VerifyNoOtherCalls();
}
}
18 changes: 18 additions & 0 deletions BlazorApp1.Application/BlazorApp1.Application.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="AutoMapper" Version="12.0.1" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\BlazorApp1.Server.Abstractions\BlazorApp1.Server.Abstractions.csproj" />
<ProjectReference Include="..\BlazorApp1\Shared\BlazorApp1.Data.csproj" />
</ItemGroup>

</Project>
25 changes: 25 additions & 0 deletions BlazorApp1.Application/DependencyInjectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using BlazorApp1.Application.Queue;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace BlazorApp1.Application;

public static class DependencyInjectionExtensions
{
public static IServiceCollection AddWeatherForecastApplicationLayer(this IServiceCollection services, IConfiguration configuration)
{
services.AddSingleton<IBackgroundTaskQueue>(_ =>
{
if (!int.TryParse(configuration["QueueCapacity"],
out int queueCapacity))
{
queueCapacity = 100;
}
return new DefaultBackgroundTaskQueue(queueCapacity);
});
services.AddScoped<IWeatherForecastService, WeatherForecastService>();

return services;
}
}
9 changes: 9 additions & 0 deletions BlazorApp1.Application/IWeatherForecastService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using BlazorApp1.Domain;
using BlazorApp1.Server.Abstractions.Contracts;

namespace BlazorApp1.Application;
public interface IWeatherForecastService
{
Task AddForecast(WeatherForecastDto weatherForecast, CancellationToken cancellationToken = default);
ValueTask ProcessWeatherSummary(WeatherForecast forecast, CancellationToken cancellationToken);
}
75 changes: 75 additions & 0 deletions BlazorApp1.Application/WeatherForecastService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using AutoMapper;
using BlazorApp1.Application.Queue;
using BlazorApp1.Data.Abstractions.Repositories;
using BlazorApp1.Domain;
using BlazorApp1.Server.Abstractions.Contracts;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace BlazorApp1.Application;

public class WeatherForecastService : IWeatherForecastService
{
private readonly IWeatherForecastRepository _weatherForecastRepository;
private readonly IWeatherForecastSummaryRepository _weatherForecastSummaryRepository;
private readonly IMapper _mapper;
private readonly IBackgroundTaskQueue _backgroundTaskQueue;
private readonly ILogger<WeatherForecastService> _logger;

public WeatherForecastService(IWeatherForecastRepository weatherForecastRepository,
IWeatherForecastSummaryRepository weatherForecastSummaryRepository,
IMapper mapper,
IBackgroundTaskQueue backgroundTaskQueue,
ILogger<WeatherForecastService> logger)
{
_weatherForecastRepository = weatherForecastRepository;
_weatherForecastSummaryRepository = weatherForecastSummaryRepository;
_mapper = mapper;
_backgroundTaskQueue = backgroundTaskQueue;
_logger = logger;
}

public async Task AddForecast(WeatherForecastDto weatherForecast, CancellationToken cancellationToken = default)
{
var forecast = _mapper.Map<WeatherForecast>(weatherForecast);

await _weatherForecastRepository.AddWeatherForecast(forecast);

var tasktoDo = _backgroundTaskQueue.QueueBackgroundWorkItemAsync(ProcessWeatherSummary(forecast));
}

private static Func<IServiceProvider, CancellationToken, ValueTask> ProcessWeatherSummary(WeatherForecast forecast)
{
return (serviceProvider, cancellationToken) =>
{
var service = serviceProvider.GetRequiredService<IWeatherForecastService>();
return service.ProcessWeatherSummary(forecast, cancellationToken);
};
}

public async ValueTask ProcessWeatherSummary(WeatherForecast forecast, CancellationToken cancellationToken)
{
//TODO: Move to a new RepoMethod.
var forecasts = await _weatherForecastRepository.GetAllForecasts();
forecasts = forecasts.Where(dbForecast => DateOnly.FromDateTime(forecast.Date) == DateOnly.FromDateTime(dbForecast.Date));

var forecastSummary = new WeatherForecastSummary
{
Date = DateOnly.FromDateTime(forecast.Date),
TemperatureC = (int)Math.Round(forecasts.Average(x => x.TemperatureC), 0)
};

var existingSummary = await _weatherForecastSummaryRepository.GetForecastSummaryByDate(forecastSummary.Date);
if (existingSummary != null)
{
existingSummary.TemperatureC = forecastSummary.TemperatureC;
await _weatherForecastSummaryRepository.UpdateWeatherForecastSummary(existingSummary);
}
else
{
await _weatherForecastSummaryRepository.AddWeatherForecastSummary(forecastSummary);
}

_logger.LogInformation($"Processing ForecastSummary and added {forecast.Date}");
}
}
10 changes: 9 additions & 1 deletion BlazorApp1.Data/WeatherForecastSummary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,15 @@

namespace BlazorApp1.Domain;

public record WeatherForecastSummary([property: Key][property: DatabaseGenerated(DatabaseGeneratedOption.Identity)] int Id, DateOnly Date, int TemperatureC)
public class WeatherForecastSummary
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }

public DateOnly Date { get; set; }

public int TemperatureC { get; set; }

public int TemperatureF => 32 + (int)Math.Round(TemperatureC / 0.5556, 0);
}
37 changes: 20 additions & 17 deletions BlazorApp1.Server.Unit.Tests/WeatherForecastControllerTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using AutoMapper;
using BlazorApp1.Application;
using BlazorApp1.Data.Abstractions.Repositories;
using BlazorApp1.Domain;
using BlazorApp1.Server.Abstractions.Contracts;
Expand All @@ -13,31 +14,33 @@ public class WeatherForecastControllerTests
{
private readonly Mock<ILogger<WeatherForecastController>> _loggerMock;
private readonly Mock<IWeatherForecastRepository> _weatherForecastRepositoryMock;
private readonly Mock<IWeatherForecastService> _weatherForecastServiceMock;
private readonly IMapper _mapper;

public WeatherForecastControllerTests()
{
_loggerMock = new Mock<ILogger<WeatherForecastController>>();
_weatherForecastRepositoryMock = new Mock<IWeatherForecastRepository>();
_weatherForecastServiceMock = new Mock<IWeatherForecastService>();

var services = new ServiceCollection();
services.AddAutoMapper(typeof(WeatherForecastController).Assembly);
var servicesCollection = services.BuildServiceProvider();
_mapper = servicesCollection.GetRequiredService<IMapper>();
}

[Fact]
public async Task Get_ShouldReturnAllForecasts()
{
// Arrange
var forecasts = new List<WeatherForecast>
[Fact]
public async Task Get_ShouldReturnAllForecasts()
{
// Arrange
var forecasts = new List<WeatherForecast>
{
new WeatherForecast { Date = DateTime.Today, TemperatureC = 20, Summary = "Sunny" },
new WeatherForecast { Date = DateTime.Today.AddDays(1), TemperatureC = 15, Summary = "Cloudy" }
};
_weatherForecastRepositoryMock.Setup(x => x.GetAllForecasts()).ReturnsAsync(forecasts);
_weatherForecastRepositoryMock.Setup(x => x.GetAllForecasts()).ReturnsAsync(forecasts);

var controller = new WeatherForecastController(_loggerMock.Object, _weatherForecastRepositoryMock.Object, _mapper);
var controller = new WeatherForecastController(_loggerMock.Object, _weatherForecastRepositoryMock.Object, _mapper, _weatherForecastServiceMock.Object);

// Act
var result = await controller.Get();
Expand All @@ -54,7 +57,7 @@ public async Task Get_ShouldReturnEmptyListWhenNoForecasts()
var forecasts = new List<WeatherForecast>();
_weatherForecastRepositoryMock.Setup(x => x.GetAllForecasts()).ReturnsAsync(forecasts);

var controller = new WeatherForecastController(_loggerMock.Object, _weatherForecastRepositoryMock.Object, _mapper);
var controller = new WeatherForecastController(_loggerMock.Object, _weatherForecastRepositoryMock.Object, _mapper, _weatherForecastServiceMock.Object);

// Act
var result = await controller.Get();
Expand All @@ -71,7 +74,7 @@ public async Task Get_ShouldReturnExceptionWhenException()
var forecasts = new List<WeatherForecast>();
_weatherForecastRepositoryMock.Setup(x => x.GetAllForecasts()).Throws(new Exception());

var controller = new WeatherForecastController(_loggerMock.Object, _weatherForecastRepositoryMock.Object, _mapper);
var controller = new WeatherForecastController(_loggerMock.Object, _weatherForecastRepositoryMock.Object, _mapper, _weatherForecastServiceMock.Object);

// Act
var result = async () => await controller.Get();
Expand All @@ -81,21 +84,21 @@ public async Task Get_ShouldReturnExceptionWhenException()
_weatherForecastRepositoryMock.Verify(x => x.GetAllForecasts(), Times.Once);
}

[Fact]
public async Task AddForecast_ShouldCallRepository()
{
// Arrange
var forecast = new WeatherForecast { Date = DateTime.Today, TemperatureC = 25, Summary = "Hot" };
_weatherForecastRepositoryMock.Setup(x => x.AddWeatherForecast(forecast)).Returns(Task.CompletedTask);
[Fact]
public async Task AddForecast_ShouldCallRepository()
{
// Arrange
var forecast = new WeatherForecast { Date = DateTime.Today, TemperatureC = 25, Summary = "Hot" };
_weatherForecastRepositoryMock.Setup(x => x.AddWeatherForecast(forecast)).Returns(Task.CompletedTask);

var controller = new WeatherForecastController(_loggerMock.Object, _weatherForecastRepositoryMock.Object, _mapper);
var controller = new WeatherForecastController(_loggerMock.Object, _weatherForecastRepositoryMock.Object, _mapper, _weatherForecastServiceMock.Object);

// Act
var forecastDto = new WeatherForecastDto { Date = DateTime.Today, TemperatureC = 25, Summary = "Hot" };

await controller.AddForecast(forecastDto);

// Assert
_weatherForecastRepositoryMock.Verify(x => x.AddWeatherForecast(It.IsAny<WeatherForecast>()), Times.Once);
_weatherForecastServiceMock.Verify(x => x.AddForecast(forecastDto, It.IsAny<CancellationToken>()), Times.Once);
}
}
Loading

0 comments on commit 5f04e73

Please sign in to comment.