Skip to content

Commit

Permalink
feature flag implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
Gramli committed Nov 24, 2024
1 parent bd23433 commit 0cf2e2a
Show file tree
Hide file tree
Showing 21 changed files with 156 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public async Task GetForecastWeather_Failed()
var getForecastWeatherQuery = new GetForecastWeatherQuery(1, 1);

_getForecastWeatherQueryValidatorMock.Setup(x => x.Validate(It.IsAny<GetForecastWeatherQuery>())).Returns(new RequestValidationResult { IsValid = true });
_weatherServiceMock.Setup(x => x.GetForecastWeather(It.IsAny<LocationDto>(), It.IsAny<CancellationToken>())).ReturnsAsync(Result.Fail(errorMessage));
_weatherServiceMock.Setup(x => x.GetSixteenDayForecastWeather(It.IsAny<LocationDto>(), It.IsAny<CancellationToken>())).ReturnsAsync(Result.Fail(errorMessage));
//Act
var result = await _uut.HandleAsync(getForecastWeatherQuery, CancellationToken.None);

Expand All @@ -69,7 +69,7 @@ public async Task GetForecastWeather_Failed()
Assert.Equal(ErrorMessages.ExternalApiError, result.Errors.Single());
Assert.Null(result.Data);
_getForecastWeatherQueryValidatorMock.Verify(x => x.Validate(It.Is<GetForecastWeatherQuery>(y => y.Equals(getForecastWeatherQuery))), Times.Once);
_weatherServiceMock.Verify(x => x.GetForecastWeather(It.Is<LocationDto>(y => y.Equals(getForecastWeatherQuery.Location)), It.IsAny<CancellationToken>()), Times.Once);
_weatherServiceMock.Verify(x => x.GetSixteenDayForecastWeather(It.Is<LocationDto>(y => y.Equals(getForecastWeatherQuery.Location)), It.IsAny<CancellationToken>()), Times.Once);
_loggerMock.VerifyLog(LogLevel.Error, LogEvents.ForecastWeathersGet, errorMessage, Times.Once());
}

Expand All @@ -81,7 +81,7 @@ public async Task GetForecastWeather_ValidationFailed()
var forecastWeather = new ForecastWeatherDto();

_getForecastWeatherQueryValidatorMock.Setup(x => x.Validate(It.IsAny<GetForecastWeatherQuery>())).Returns(new RequestValidationResult { IsValid = true });
_weatherServiceMock.Setup(x => x.GetForecastWeather(It.IsAny<LocationDto>(), It.IsAny<CancellationToken>())).ReturnsAsync(Result.Ok(forecastWeather));
_weatherServiceMock.Setup(x => x.GetSixteenDayForecastWeather(It.IsAny<LocationDto>(), It.IsAny<CancellationToken>())).ReturnsAsync(Result.Ok(forecastWeather));
_forecastWeatherValidatorMock.Setup(x => x.Validate(It.IsAny<ForecastWeatherDto>())).Returns(new RequestValidationResult { IsValid = false });

//Act
Expand All @@ -92,7 +92,7 @@ public async Task GetForecastWeather_ValidationFailed()
Assert.Single(result.Errors);
Assert.Null(result.Data);
_getForecastWeatherQueryValidatorMock.Verify(x => x.Validate(It.Is<GetForecastWeatherQuery>(y => y.Equals(getForecastWeatherQuery))), Times.Once);
_weatherServiceMock.Verify(x => x.GetForecastWeather(It.Is<LocationDto>(y => y.Equals(getForecastWeatherQuery.Location)), It.IsAny<CancellationToken>()), Times.Once);
_weatherServiceMock.Verify(x => x.GetSixteenDayForecastWeather(It.Is<LocationDto>(y => y.Equals(getForecastWeatherQuery.Location)), It.IsAny<CancellationToken>()), Times.Once);
_forecastWeatherValidatorMock.Verify(x => x.Validate(It.Is<ForecastWeatherDto>(y => y.Equals(forecastWeather))), Times.Once);
_loggerMock.VerifyLog(LogLevel.Error, LogEvents.ForecastWeathersValidation, Times.Once());
}
Expand All @@ -105,7 +105,7 @@ public async Task Success()
var forecastWeather = new ForecastWeatherDto();

_getForecastWeatherQueryValidatorMock.Setup(x => x.Validate(It.IsAny<GetForecastWeatherQuery>())).Returns(new RequestValidationResult { IsValid = true });
_weatherServiceMock.Setup(x => x.GetForecastWeather(It.IsAny<LocationDto>(), It.IsAny<CancellationToken>())).ReturnsAsync(Result.Ok(forecastWeather));
_weatherServiceMock.Setup(x => x.GetSixteenDayForecastWeather(It.IsAny<LocationDto>(), It.IsAny<CancellationToken>())).ReturnsAsync(Result.Ok(forecastWeather));
_forecastWeatherValidatorMock.Setup(x => x.Validate(It.IsAny<ForecastWeatherDto>())).Returns(new RequestValidationResult { IsValid = true });

//Act
Expand All @@ -117,7 +117,7 @@ public async Task Success()
Assert.NotNull(result.Data);
Assert.Equal(forecastWeather, result.Data);
_getForecastWeatherQueryValidatorMock.Verify(x => x.Validate(It.Is<GetForecastWeatherQuery>(y => y.Equals(getForecastWeatherQuery))), Times.Once);
_weatherServiceMock.Verify(x => x.GetForecastWeather(It.Is<LocationDto>(y => y.Equals(getForecastWeatherQuery.Location)), It.IsAny<CancellationToken>()), Times.Once);
_weatherServiceMock.Verify(x => x.GetSixteenDayForecastWeather(It.Is<LocationDto>(y => y.Equals(getForecastWeatherQuery.Location)), It.IsAny<CancellationToken>()), Times.Once);
_forecastWeatherValidatorMock.Verify(x => x.Validate(It.Is<ForecastWeatherDto>(y => y.Equals(forecastWeather))), Times.Once);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ public async Task GetForecastWeather_Failed()

_weatherbiClientMock.Setup(x => x.GetSixteenDayForecast(It.IsAny<double>(), It.IsAny<double>(), It.IsAny<CancellationToken>())).ReturnsAsync(Result.Fail(failedMessage));
//Act
var result = await _uut.GetForecastWeather(location, CancellationToken.None);
var result = await _uut.GetSixteenDayForecastWeather(location, CancellationToken.None);
//Assert
Assert.True(result.IsFailed);
Assert.Single(result.Errors);
Expand All @@ -149,7 +149,7 @@ public async Task GetForecastWeather_NullData()

_weatherbiClientMock.Setup(x => x.GetSixteenDayForecast(It.IsAny<double>(), It.IsAny<double>(), It.IsAny<CancellationToken>())).ReturnsAsync(Result.Ok((Wheaterbit.Client.Dtos.ForecastWeatherDto)null));
//Act
var result = await _uut.GetForecastWeather(location, CancellationToken.None);
var result = await _uut.GetSixteenDayForecastWeather(location, CancellationToken.None);
//Assert
Assert.True(result.IsFailed);
Assert.Single(result.Errors);
Expand All @@ -165,7 +165,7 @@ public async Task GetForecastWeather_EmptyData()

_weatherbiClientMock.Setup(x => x.GetSixteenDayForecast(It.IsAny<double>(), It.IsAny<double>(), It.IsAny<CancellationToken>())).ReturnsAsync(Result.Ok(new Wheaterbit.Client.Dtos.ForecastWeatherDto()));
//Act
var result = await _uut.GetForecastWeather(location, CancellationToken.None);
var result = await _uut.GetSixteenDayForecastWeather(location, CancellationToken.None);
//Assert
Assert.True(result.IsFailed);
Assert.Single(result.Errors);
Expand Down Expand Up @@ -193,7 +193,7 @@ public async Task GetForecastWeather_Success()
_mapperMock.Setup(x => x.Map<Domain.Dtos.ForecastWeatherDto>(It.IsAny<Wheaterbit.Client.Dtos.ForecastWeatherDto>())).Returns(mapResult);

//Act
var result = await _uut.GetForecastWeather(location, CancellationToken.None);
var result = await _uut.GetSixteenDayForecastWeather(location, CancellationToken.None);

//Assert
Assert.True(result.IsSuccess);
Expand Down
12 changes: 0 additions & 12 deletions src/Weather.API/Configuration/ContainerConfigurationExtension.cs

This file was deleted.

18 changes: 18 additions & 0 deletions src/Weather.API/Ex.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using SmallApiToolkit.Middleware;
using System.Net;
using System.Runtime.CompilerServices;

namespace Weather.API
{
public class Ex : ExceptionMiddleware
{
public Ex(RequestDelegate next, ILogger<ExceptionMiddleware> logger) : base(next, logger)
{
}

protected override (HttpStatusCode responseCode, string responseMessage) ExtractFromException(Exception generalEx)
{
return base.ExtractFromException(generalEx);
}
}
}
19 changes: 18 additions & 1 deletion src/Weather.API/Program.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using Microsoft.FeatureManagement;
using SmallApiToolkit.Extensions;
using SmallApiToolkit.Middleware;
using Weather.API.Configuration;
using Weather.API.EndpointBuilders;
using Weather.Core.Configuration;
using Weather.Domain.FeatureFlags;
using Weather.Infrastructure.Configuration;

var builder = WebApplication.CreateBuilder(args);
Expand All @@ -13,6 +15,19 @@
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var corsPolicyName = builder.Services.AddCorsByConfiguration(builder.Configuration);

builder.Services.AddFeatureManagement(builder.Configuration.GetSection(FeatureFlagKeys.FeatureFlagsKey));

/*builder.Configuration.AddAzureAppConfiguration(options =>
{
options.Connect("")
.UseFeatureFlags(featureFlagOptions =>
{
featureFlagOptions.SetRefreshInterval(TimeSpan.FromMinutes(1));
});
});*/

var app = builder.Build();

if (app.Environment.IsDevelopment())
Expand All @@ -21,6 +36,8 @@
app.UseSwaggerUI();
}

app.UseCors(corsPolicyName);

app.UseHttpsRedirection();

app.UseMiddleware<ExceptionMiddleware>();
Expand Down
2 changes: 2 additions & 0 deletions src/Weather.API/Weather.API.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

<ItemGroup>
<PackageReference Include="Ardalis.GuardClauses" Version="5.0.0" />
<PackageReference Include="Microsoft.Azure.AppConfiguration.AspNetCore" Version="8.0.0" />
<PackageReference Include="Microsoft.FeatureManagement.AspNetCore" Version="4.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.0" />
<PackageReference Include="SmallApiToolkit" Version="1.0.0.7" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.0.0" />
Expand Down
29 changes: 28 additions & 1 deletion src/Weather.API/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,37 @@
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Weatherbit": {
"BaseUrl": "https://weatherbit-v1-mashape.p.rapidapi.com",
"XRapidAPIKey": "",
"XRapidAPIHost": "weatherbit-v1-mashape.p.rapidapi.com"
},
"UseAzureForFeatureFlags": false,
"FeatureFlags": {
"useFiveDayForecast": true,
"useCelsiusForTemperature": true,
"allowToDeleteFavorites": [
{
"name": "TimeBiggerThan",
"parameters": {
"value": "9:00"
}
},
{
"name": "TimeLessThan",
"parameters": {
"value": "17:00"
}
}
]
},
"CORS": {
"name": "allowLocalhostOrigins",
"urls": [
"http://127.0.0.1:4200",
"http://localhost:4200",
"https://127.0.0.1:4200",
"https://localhost:4200"
]
}
}
3 changes: 2 additions & 1 deletion src/Weather.Core/Abstractions/IWeatherService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ public interface IWeatherService
{
Task<Result<CurrentWeatherDto>> GetCurrentWeather(LocationDto locationDto, CancellationToken cancellationToken);

Task<Result<ForecastWeatherDto>> GetForecastWeather(LocationDto locationDto, CancellationToken cancellationToken);
Task<Result<ForecastWeatherDto>> GetSixteenDayForecastWeather(LocationDto locationDto, CancellationToken cancellationToken);
Task<Result<ForecastWeatherDto>> GetFiveDayForecastWeather(LocationDto locationDto, CancellationToken cancellationToken);
}
}
1 change: 1 addition & 0 deletions src/Weather.Core/Queries/GetFavoritesHandler.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Ardalis.GuardClauses;
using FluentResults;
using Microsoft.Extensions.Logging;
using Microsoft.FeatureManagement;
using SmallApiToolkit.Core.Extensions;
using SmallApiToolkit.Core.RequestHandlers;
using SmallApiToolkit.Core.Response;
Expand Down
15 changes: 13 additions & 2 deletions src/Weather.Core/Queries/GetForecastWeatherHandler.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using Ardalis.GuardClauses;
using FluentResults;
using Microsoft.Extensions.Logging;
using Microsoft.FeatureManagement;
using SmallApiToolkit.Core.Extensions;
using SmallApiToolkit.Core.RequestHandlers;
using SmallApiToolkit.Core.Response;
Expand All @@ -8,6 +10,7 @@
using Weather.Core.Resources;
using Weather.Domain.Dtos;
using Weather.Domain.Extensions;
using Weather.Domain.FeatureFlags;
using Weather.Domain.Logging;
using Weather.Domain.Queries;
using Weather.Domain.Resources;
Expand All @@ -19,20 +22,23 @@ internal sealed class GetForecastWeatherHandler : ValidationHttpRequestHandler<F
private readonly IRequestValidator<ForecastWeatherDto> _forecastWeatherValidator;
private readonly IWeatherService _weatherService;
private readonly ILogger<GetForecastWeatherHandler> _logger;
private readonly IFeatureManager _featureManager;
public GetForecastWeatherHandler(
IRequestValidator<GetForecastWeatherQuery> getForecastWeatherQueryValidator,
IWeatherService weatherService,
IRequestValidator<ForecastWeatherDto> forecastWeatherValidator,
ILogger<GetForecastWeatherHandler> logger)
ILogger<GetForecastWeatherHandler> logger,
IFeatureManager featureManager)
: base(getForecastWeatherQueryValidator)
{
_weatherService = Guard.Against.Null(weatherService);
_forecastWeatherValidator = Guard.Against.Null(forecastWeatherValidator);
_logger = Guard.Against.Null(logger);
_featureManager = Guard.Against.Null(featureManager);
}
protected override async Task<HttpDataResponse<ForecastWeatherDto>> HandleValidRequestAsync(GetForecastWeatherQuery request, CancellationToken cancellationToken)
{
var forecastResult = await _weatherService.GetForecastWeather(request.Location, cancellationToken);
var forecastResult = await GetForecastWeatherAsync(request, cancellationToken);

if(forecastResult.IsFailed)
{
Expand All @@ -49,5 +55,10 @@ protected override async Task<HttpDataResponse<ForecastWeatherDto>> HandleValidR

return HttpDataResponses.AsOK(forecastResult.Value);
}

private async Task<Result<ForecastWeatherDto>> GetForecastWeatherAsync(GetForecastWeatherQuery request, CancellationToken cancellationToken)
=> await _featureManager.IsEnabledAsync(FeatureFlagKeys.UseFiveDayForecast) ?
await _weatherService.GetFiveDayForecastWeather(request.Location, cancellationToken) :
await _weatherService.GetSixteenDayForecastWeather(request.Location, cancellationToken);
}
}
1 change: 1 addition & 0 deletions src/Weather.Core/Weather.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<PackageReference Include="Ardalis.GuardClauses" Version="5.0.0" />
<PackageReference Include="FluentResults" Version="3.16.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
<PackageReference Include="Microsoft.FeatureManagement" Version="4.0.0" />
<PackageReference Include="SmallApiToolkit.Core" Version="1.0.0.7" />
<PackageReference Include="Validot" Version="2.5.0" />
</ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion src/Weather.Domain/Dtos/ForecastWeatherDto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
{
public sealed class ForecastWeatherDto
{
public IReadOnlyCollection<ForecastTemperatureDto> ForecastTemperatures { get; init; } = new List<ForecastTemperatureDto>();
public IReadOnlyCollection<ForecastTemperatureDto> ForecastTemperatures { get; init; } = [];

public string CityName { get; init; } = string.Empty;
}
Expand Down
9 changes: 9 additions & 0 deletions src/Weather.Domain/Extensions/CelsiusExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Weather.Domain.Extensions
{
public static class CelsiusExtensions
{
private const double FahrenheitCelsiusConst = 33.8;
public static double ToCelsius(double fahrenheit)
=> fahrenheit * FahrenheitCelsiusConst;
}
}
9 changes: 9 additions & 0 deletions src/Weather.Domain/FeatureFlags/FeatureFlagKeys.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Weather.Domain.FeatureFlags
{
public static class FeatureFlagKeys
{
public static readonly string FeatureFlagsKey = "FeatureFlags";
public static readonly string UseFiveDayForecast = "useFiveDayForecast";
public static readonly string UseCelsiusForTemperature = "useCelsiusForTemperature";
}
}
10 changes: 8 additions & 2 deletions src/Weather.Infrastructure/Services/WeatherService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,15 @@ public async Task<Result<CurrentWeatherDto>> GetCurrentWeather(LocationDto locat
return _mapper.Map<CurrentWeatherDto>(currentWeatherResult.Value.Data.Single());
}

public async Task<Result<ForecastWeatherDto>> GetForecastWeather(LocationDto locationDto, CancellationToken cancellationToken)
public async Task<Result<ForecastWeatherDto>> GetFiveDayForecastWeather(LocationDto locationDto, CancellationToken cancellationToken)
=> await GetForecastWeather(_weatherbitHttpClient.GetFiveDayForecast(locationDto.Latitude, locationDto.Longitude, cancellationToken));

public async Task<Result<ForecastWeatherDto>> GetSixteenDayForecastWeather(LocationDto locationDto, CancellationToken cancellationToken)
=> await GetForecastWeather(_weatherbitHttpClient.GetSixteenDayForecast(locationDto.Latitude, locationDto.Longitude, cancellationToken));

private async Task<Result<ForecastWeatherDto>> GetForecastWeather(Task<Result<Wheaterbit.Client.Dtos.ForecastWeatherDto>> getData)
{
var forecastWeatherResult = await _weatherbitHttpClient.GetSixteenDayForecast(locationDto.Latitude, locationDto.Longitude, cancellationToken);
var forecastWeatherResult = await getData;
if (forecastWeatherResult.IsFailed)
{
return Result.Fail(forecastWeatherResult.Errors);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ namespace Wheaterbit.Client.Abstractions
public interface IJsonSerializerSettingsFactory
{
JsonSerializerSettings Create();
JsonSerializerSettings CreateWithHoursOnly();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ public interface IWeatherbitHttpClient
{
Task<Result<ForecastWeatherDto>> GetSixteenDayForecast(double latitude, double longitude, CancellationToken cancellationToken);
Task<Result<CurrentWeatherDataDto>> GetCurrentWeather(double latitude, double longitude, CancellationToken cancellationToken);
Task<Result<ForecastWeatherDto>> GetFiveDayForecast(double latitude, double longitude, CancellationToken cancellationToken);
}
}
4 changes: 2 additions & 2 deletions src/Wheaterbit.Client/Dtos/ForecastTemperatureDto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
{
public sealed class ForecastTemperatureDto
{
public double temp { get; init; }
public DateTime datetime { get; init; }
public double? temp { get; init; }
public DateTime? datetime { get; init; }
}
}
4 changes: 2 additions & 2 deletions src/Wheaterbit.Client/Dtos/ForecastWeatherDto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
{
public sealed class ForecastWeatherDto
{
public IReadOnlyCollection<ForecastTemperatureDto> Data { get; init; } = new List<ForecastTemperatureDto>();
public IReadOnlyCollection<ForecastTemperatureDto>? Data { get; init; } = [];

public string city_name { get; init; } = string.Empty;
public string? city_name { get; init; } = string.Empty;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,13 @@ public JsonSerializerSettings Create()
DateFormatString = "yyyy-MM-dd hh:mm"
};
}

public JsonSerializerSettings CreateWithHoursOnly()
{
return new JsonSerializerSettings
{
DateFormatString = "yyyy-MM-dd:HH",
};
}
}
}
Loading

0 comments on commit 0cf2e2a

Please sign in to comment.