diff --git a/.assets/api.png b/.assets/api.png index bedabcbc2..729ebfd46 100644 Binary files a/.assets/api.png and b/.assets/api.png differ diff --git a/.assets/core.png b/.assets/core.png index f4932e1cd..c1d2e5008 100644 Binary files a/.assets/core.png and b/.assets/core.png differ diff --git a/.github/workflows/docker-push.yml b/.github/workflows/docker-push.yml index 5583b2471..dc580f9be 100644 --- a/.github/workflows/docker-push.yml +++ b/.github/workflows/docker-push.yml @@ -87,6 +87,9 @@ jobs: uses: docker/build-push-action@v5 with: push: true + build-args: | + VERSION=${{ env.VERSION }} + SHA=${{ github.sha }} labels: ${{ steps.meta.outputs.labels }} tags: | ${{ env.BASE_TAG }}:${{ env.VERSION }} diff --git a/.vscode/launch.json b/.vscode/launch.json index 11b7d2e2d..9f9708e36 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,26 +1,27 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "Launch API", - "type": "coreclr", - "request": "launch", - "preLaunchTask": "build", - "program": "${workspaceFolder}/src/HappyCode.NetCoreBoilerplate.Api/bin/Debug/netcoreapp3.1/HappyCode.NetCoreBoilerplate.Api.dll", - "args": [], - "cwd": "${workspaceFolder}/src/HappyCode.NetCoreBoilerplate.Api", - "stopAtEntry": false, - "serverReadyAction": { - "action": "openExternally", - "pattern": "^\\s*Now listening on:\\s+(https?://\\S+)", - }, - "env": { - "ASPNETCORE_ENVIRONMENT": "Development", - "ASPNETCORE_URLS": "http://*:5000", - } - } - ] -} +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Debug API", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/src/HappyCode.NetCoreBoilerplate.Api/bin/Debug/net8.0/HappyCode.NetCoreBoilerplate.Api.dll", + "args": [], + "cwd": "${workspaceFolder}/src/HappyCode.NetCoreBoilerplate.Api", + "stopAtEntry": false, + "serverReadyAction": { + "action": "openExternally", + "pattern": "\\bNow listening on:\\s+(https?://\\S+)", + "uriFormat": "%s/swagger" + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development", + "ASPNETCORE_URLS": "http://localhost:5000", + } + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index fb2c872e1..a19d84a07 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -7,7 +7,9 @@ "type": "process", "args": [ "build", - "${workspaceFolder}/src/HappyCode.NetCoreBoilerplate.Api/HappyCode.NetCoreBoilerplate.Api.csproj" + "${workspaceFolder}/src/HappyCode.NetCoreBoilerplate.Api/HappyCode.NetCoreBoilerplate.Api.csproj", + "-c", + "Debug", ], "problemMatcher": "$msCompile" } diff --git a/Directory.Build.props b/Directory.Build.props index 9f4186974..10d365a7e 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -6,7 +6,7 @@ Łukasz Kurzyniec - Copyright © happy+code Łukasz Kurzyniec 2022 + Copyright © happy+code Łukasz Kurzyniec 2024 2.0.0 diff --git a/README.md b/README.md index 0acc64347..a15d3593f 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,7 @@ After successful start of the solution in any of above option, check useful endp * swagger - * health check - +* version - ### Standalone @@ -183,6 +184,10 @@ Generally it is totally up to you! But in case you do not have any plan, You can * Configurations * `Serilog` configuration place - [SerilogConfigurator.cs](src/HappyCode.NetCoreBoilerplate.Api/Infrastructure/Configurations/SerilogConfigurator.cs) * `Swagger` registration place - [SwaggerRegistration.cs](src/HappyCode.NetCoreBoilerplate.Api/Infrastructure/Registrations/SwaggerRegistration.cs) + * Feature flag documentation filter - [FeatureFlagSwaggerDocumentFilter.cs](src/HappyCode.NetCoreBoilerplate.Api/Infrastructure/Filters/FeatureFlagSwaggerDocumentFilter.cs) + * Security requirement operation filter - [SecurityRequirementSwaggerOperationFilter.cs](src/HappyCode.NetCoreBoilerplate.Api/Infrastructure/Filters/SecurityRequirementSwaggerOperationFilter.cs) +* Logging + * Custom enricher to have version properties in logs - [VersionEnricher.cs](src/HappyCode.NetCoreBoilerplate.Api/Infrastructure/Logging/VersionEnricher.cs) * Simple exemplary API controllers - [EmployeesController.cs](src/HappyCode.NetCoreBoilerplate.Api/Controllers/EmployeesController.cs), [CarsController.cs](src/HappyCode.NetCoreBoilerplate.Api/Controllers/CarsController.cs), [PingsController.cs](src/HappyCode.NetCoreBoilerplate.Api/Controllers/PingsController.cs) * Example of BackgroundService - [PingWebsiteBackgroundService.cs](src/HappyCode.NetCoreBoilerplate.Api/BackgroundServices/PingWebsiteBackgroundService.cs) @@ -199,6 +204,8 @@ Generally it is totally up to you! But in case you do not have any plan, You can * DbContexts * MySQL DbContext - [EmployeesContext.cs](src/HappyCode.NetCoreBoilerplate.Core/EmployeesContext.cs) * MsSQL DbContext - [CarsContext.cs](src/HappyCode.NetCoreBoilerplate.Core/CarsContext.cs) +* Providers + * Version provider - [VersionProvider.cs](src/HappyCode.NetCoreBoilerplate.Core/Providers/VersionProvider.cs) * Core registrations - [CoreRegistrations.cs](src/HappyCode.NetCoreBoilerplate.Core/Registrations/CoreRegistrations.cs) * Exemplary MySQL repository - [EmployeeRepository.cs](src/HappyCode.NetCoreBoilerplate.Core/Repositories/EmployeeRepository.cs) * Exemplary MsSQL service - [CarService.cs](src/HappyCode.NetCoreBoilerplate.Core/Services/CarService.cs) @@ -224,7 +231,8 @@ Generally it is totally up to you! But in case you do not have any plan, You can * Fixture with TestServer - [TestServerClientFixture.cs](test/HappyCode.NetCoreBoilerplate.Api.IntegrationTests/Infrastructure/TestServerClientFixture.cs) * TestStartup with InMemory databases - [TestStartup.cs](test/HappyCode.NetCoreBoilerplate.Api.IntegrationTests/Infrastructure/TestStartup.cs) * Simple data feeders - [EmployeeContextDataFeeder.cs](test/HappyCode.NetCoreBoilerplate.Api.IntegrationTests/Infrastructure/DataFeeders/EmployeeContextDataFeeder.cs), [CarsContextDataFeeder.cs](test/HappyCode.NetCoreBoilerplate.Api.IntegrationTests/Infrastructure/DataFeeders/CarsContextDataFeeder.cs) -* Exemplary tests - [EmployeesTests.cs](test/HappyCode.NetCoreBoilerplate.Api.IntegrationTests/EmployeesTests.cs), [CarsTests.cs](test/HappyCode.NetCoreBoilerplate.Api.IntegrationTests/CarsTests.cs) + * Fakes - [FakePingService.cs](test/HappyCode.NetCoreBoilerplate.Api.IntegrationTests/Infrastructure/Fakes/FakePingService.cs) +* Exemplary tests - [EmployeesTests.cs](test/HappyCode.NetCoreBoilerplate.Api.IntegrationTests/EmployeesTests.cs), [CarsTests.cs](test/HappyCode.NetCoreBoilerplate.Api.IntegrationTests/CarsTests.cs), [PingsTests.cs](test/HappyCode.NetCoreBoilerplate.Api.IntegrationTests/PingsTests.cs) ![HappyCode.NetCoreBoilerplate.Api.IntegrationTests](.assets/itests.png "HappyCode.NetCoreBoilerplate.Api.IntegrationTests") @@ -232,13 +240,18 @@ Generally it is totally up to you! But in case you do not have any plan, You can [HappyCode.NetCoreBoilerplate.Api.UnitTests](test/HappyCode.NetCoreBoilerplate.Api.UnitTests) -* Exemplary tests - [EmployeesControllerTests.cs](test/HappyCode.NetCoreBoilerplate.Api.UnitTests/Controllers/EmployeesControllerTests.cs) -* Unit tests of `ApiKeyAuthorizationFilter.cs` - [ApiKeyAuthorizationFilterTests.cs](test/HappyCode.NetCoreBoilerplate.Api.UnitTests/Infrastructure/Filters/ApiKeyAuthorizationFilterTests.cs) +* Exemplary tests - [EmployeesControllerTests.cs](test/HappyCode.NetCoreBoilerplate.Api.UnitTests/Controllers/EmployeesControllerTests.cs), [CarsControllerTests.cs](test/HappyCode.NetCoreBoilerplate.Api.UnitTests/Controllers/CarsControllerTests.cs), [PingsControllerTests.cs](test/HappyCode.NetCoreBoilerplate.Api.UnitTests/Controllers/PingsControllerTests.cs) +* API Infrastructure Unit tests + * [ApiKeyAuthorizationFilterTests.cs](test/HappyCode.NetCoreBoilerplate.Api.UnitTests/Infrastructure/Filters/ApiKeyAuthorizationFilterTests.cs) + * [ValidateModelStateFilterTests.cs](test/HappyCode.NetCoreBoilerplate.Api.UnitTests/Infrastructure/Filters/ValidateModelStateFilterTests.cs) + * [VersionEnricherTests.cs](test/HappyCode.NetCoreBoilerplate.Api.UnitTests/Infrastructure/Logging/VersionEnricherTests.cs) [HappyCode.NetCoreBoilerplate.Core.UnitTests](test/HappyCode.NetCoreBoilerplate.Core.UnitTests) * Extension methods to mock `DbSet` faster - [EnumerableExtensions.cs](test/HappyCode.NetCoreBoilerplate.Core.UnitTests/Extensions/EnumerableExtensions.cs) * Exemplary tests - [EmployeeRepositoryTests.cs](test/HappyCode.NetCoreBoilerplate.Core.UnitTests/Repositories/EmployeeRepositoryTests.cs), [CarServiceTests.cs](test/HappyCode.NetCoreBoilerplate.Core.UnitTests/Services/CarServiceTests.cs) +* Providers tests + * [VersionProviderTests.cs](test/HappyCode.NetCoreBoilerplate.Core.UnitTests/Providers/VersionProviderTests.cs) with [HappyCode.NetCoreBoilerplate.Core.UnitTests.runsettings](test/HappyCode.NetCoreBoilerplate.Core.UnitTests/HappyCode.NetCoreBoilerplate.Core.UnitTests.runsettings) ![HappyCode.NetCoreBoilerplate.Core.UnitTests](.assets/utests.png "HappyCode.NetCoreBoilerplate.Core.UnitTests") diff --git a/dockerfile b/dockerfile index 4b895c3e9..0afcf8f44 100644 --- a/dockerfile +++ b/dockerfile @@ -1,3 +1,6 @@ +ARG VERSION=2.0.0 +ARG SHA=none + FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base USER $APP_UID WORKDIR /app @@ -45,6 +48,9 @@ COPY --from=publish /app . ENV DOTNET_NOLOGO=true ENV DOTNET_CLI_TELEMETRY_OPTOUT=true +ENV HC_SHA=${SHA} +ENV HC_VERSION=${VERSION} + HEALTHCHECK --interval=5m --timeout=3s --start-period=10s --retries=1 \ CMD curl --fail http://localhost:8080/healthz/live || exit 1 diff --git a/src/HappyCode.NetCoreBoilerplate.Api/Infrastructure/Configurations/SerilogConfigurator.cs b/src/HappyCode.NetCoreBoilerplate.Api/Infrastructure/Configurations/SerilogConfigurator.cs index 4841757bf..3cee3f35e 100644 --- a/src/HappyCode.NetCoreBoilerplate.Api/Infrastructure/Configurations/SerilogConfigurator.cs +++ b/src/HappyCode.NetCoreBoilerplate.Api/Infrastructure/Configurations/SerilogConfigurator.cs @@ -1,3 +1,4 @@ +using HappyCode.NetCoreBoilerplate.Api.Infrastructure.Logging; using Microsoft.Extensions.Configuration; using Serilog; using Serilog.Core; @@ -11,6 +12,7 @@ public static Logger CreateLogger() var configuration = LoadAppConfiguration(); return new LoggerConfiguration() .ReadFrom.Configuration(configuration) + .Enrich.With(new VersionEnricher(new ())) .CreateLogger(); } diff --git a/src/HappyCode.NetCoreBoilerplate.Api/Infrastructure/Logging/VersionEnricher.cs b/src/HappyCode.NetCoreBoilerplate.Api/Infrastructure/Logging/VersionEnricher.cs new file mode 100644 index 000000000..c6577909a --- /dev/null +++ b/src/HappyCode.NetCoreBoilerplate.Api/Infrastructure/Logging/VersionEnricher.cs @@ -0,0 +1,23 @@ +using HappyCode.NetCoreBoilerplate.Core.Providers; +using Serilog.Core; +using Serilog.Events; + +namespace HappyCode.NetCoreBoilerplate.Api.Infrastructure.Logging; + +public class VersionEnricher : ILogEventEnricher +{ + private readonly VersionProvider _versionProvider; + + public VersionEnricher(VersionProvider versionProvider) + { + _versionProvider = versionProvider; + } + + public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) + { + foreach (var item in _versionProvider.VersionEntries) + { + logEvent.AddPropertyIfAbsent(new LogEventProperty(item.Key, new ScalarValue(item.Value))); + } + } +} diff --git a/src/HappyCode.NetCoreBoilerplate.Api/Properties/launchSettings.json b/src/HappyCode.NetCoreBoilerplate.Api/Properties/launchSettings.json index 113d07f1e..013ad0082 100644 --- a/src/HappyCode.NetCoreBoilerplate.Api/Properties/launchSettings.json +++ b/src/HappyCode.NetCoreBoilerplate.Api/Properties/launchSettings.json @@ -6,8 +6,10 @@ "launchUrl": "swagger", "environmentVariables": { "ASPNETCORE_URLS": "http://localhost:5000", - "ASPNETCORE_ENVIRONMENT": "Development" + "ASPNETCORE_ENVIRONMENT": "Development", + "HC_SHA": "2a6ba3cac8416214cfa84eb1f9092f130427479f", + "HC_VERSION": "2.0.0" } } } -} \ No newline at end of file +} diff --git a/src/HappyCode.NetCoreBoilerplate.Api/Startup.cs b/src/HappyCode.NetCoreBoilerplate.Api/Startup.cs index f3a679ab2..7b042d044 100644 --- a/src/HappyCode.NetCoreBoilerplate.Api/Startup.cs +++ b/src/HappyCode.NetCoreBoilerplate.Api/Startup.cs @@ -5,12 +5,14 @@ using HappyCode.NetCoreBoilerplate.Api.Infrastructure.Registrations; using HappyCode.NetCoreBoilerplate.BooksModule; using HappyCode.NetCoreBoilerplate.Core; +using HappyCode.NetCoreBoilerplate.Core.Providers; using HappyCode.NetCoreBoilerplate.Core.Registrations; using HappyCode.NetCoreBoilerplate.Core.Settings; using HealthChecks.UI.Client; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -91,6 +93,9 @@ public virtual void Configure(IApplicationBuilder app, IWebHostEnvironment env) ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse, }).ShortCircuit(); + endpoints.MapGet("/version", (VersionProvider provider) => provider.VersionEntries) + .ExcludeFromDescription(); + endpoints.MapControllers(); endpoints.MapBooksModule(); }); diff --git a/src/HappyCode.NetCoreBoilerplate.Core/Providers/VersionProvider.cs b/src/HappyCode.NetCoreBoilerplate.Core/Providers/VersionProvider.cs new file mode 100644 index 000000000..01edd0532 --- /dev/null +++ b/src/HappyCode.NetCoreBoilerplate.Core/Providers/VersionProvider.cs @@ -0,0 +1,24 @@ +using System.Collections; +using System.Linq; + +namespace HappyCode.NetCoreBoilerplate.Core.Providers; + +public class VersionProvider +{ + private const string PREFIX = "HC_"; + + private readonly Lazy> _versionEntries = new(GetVersionEntries); + + private static Dictionary GetVersionEntries() + { + var variables = Environment.GetEnvironmentVariables() + .Cast() + .Where(x => x.Key.ToString().StartsWith(PREFIX)) + .ToDictionary( + x => x.Key.ToString().Remove(0, PREFIX.Length), + y => y.Value.ToString()); + return variables; + } + + public Dictionary VersionEntries => _versionEntries.Value; +} diff --git a/src/HappyCode.NetCoreBoilerplate.Core/Registrations/CoreRegistrations.cs b/src/HappyCode.NetCoreBoilerplate.Core/Registrations/CoreRegistrations.cs index 8090327a4..7566d3eb9 100644 --- a/src/HappyCode.NetCoreBoilerplate.Core/Registrations/CoreRegistrations.cs +++ b/src/HappyCode.NetCoreBoilerplate.Core/Registrations/CoreRegistrations.cs @@ -1,3 +1,4 @@ +using HappyCode.NetCoreBoilerplate.Core.Providers; using HappyCode.NetCoreBoilerplate.Core.Repositories; using HappyCode.NetCoreBoilerplate.Core.Services; using Microsoft.Extensions.DependencyInjection; @@ -10,6 +11,7 @@ public static IServiceCollection AddCoreComponents(this IServiceCollection servi { services.AddTransient(); services.AddScoped(); + services.AddSingleton(); return services; } diff --git a/test/HappyCode.NetCoreBoilerplate.Api.UnitTests/HappyCode.NetCoreBoilerplate.Api.UnitTests.csproj b/test/HappyCode.NetCoreBoilerplate.Api.UnitTests/HappyCode.NetCoreBoilerplate.Api.UnitTests.csproj index 494d46fa2..bb37c6d69 100644 --- a/test/HappyCode.NetCoreBoilerplate.Api.UnitTests/HappyCode.NetCoreBoilerplate.Api.UnitTests.csproj +++ b/test/HappyCode.NetCoreBoilerplate.Api.UnitTests/HappyCode.NetCoreBoilerplate.Api.UnitTests.csproj @@ -1,4 +1,8 @@ + + $(MSBuildProjectDirectory)\HappyCode.NetCoreBoilerplate.Api.UnitTests.runsettings + + diff --git a/test/HappyCode.NetCoreBoilerplate.Api.UnitTests/HappyCode.NetCoreBoilerplate.Api.UnitTests.runsettings b/test/HappyCode.NetCoreBoilerplate.Api.UnitTests/HappyCode.NetCoreBoilerplate.Api.UnitTests.runsettings new file mode 100644 index 000000000..3f297f311 --- /dev/null +++ b/test/HappyCode.NetCoreBoilerplate.Api.UnitTests/HappyCode.NetCoreBoilerplate.Api.UnitTests.runsettings @@ -0,0 +1,10 @@ + + + + + TEST_VALUE + 36b90293 + 9.9.9 + + + diff --git a/test/HappyCode.NetCoreBoilerplate.Api.UnitTests/Infrastructure/Logging/VersionEnricherTests.cs b/test/HappyCode.NetCoreBoilerplate.Api.UnitTests/Infrastructure/Logging/VersionEnricherTests.cs new file mode 100644 index 000000000..ecf1fc0b1 --- /dev/null +++ b/test/HappyCode.NetCoreBoilerplate.Api.UnitTests/Infrastructure/Logging/VersionEnricherTests.cs @@ -0,0 +1,38 @@ +using System; +using FluentAssertions; +using HappyCode.NetCoreBoilerplate.Api.Infrastructure.Logging; +using HappyCode.NetCoreBoilerplate.Core.Providers; +using Serilog.Events; +using Xunit; + +namespace HappyCode.NetCoreBoilerplate.Api.UnitTests.Infrastructure.Logging; + +public class VersionEnricherTests +{ + private readonly VersionEnricher _sut; + + public VersionEnricherTests() + { + _sut = new VersionEnricher(new VersionProvider()); + } + + [Fact] + public void Properties_should_be_available() + { + // Arrange + var logEvent = GetEmptyLogEvent(); + + // Act + _sut.Enrich(logEvent, null); + + // Assert + logEvent.Properties["SHA"].ToString().Should().Contain("36b90293"); + logEvent.Properties["VERSION"].ToString().Should().Contain("9.9.9"); + } + + private static LogEvent GetEmptyLogEvent() + { + return new LogEvent(DateTimeOffset.UtcNow, LogEventLevel.Verbose, null, + new MessageTemplate(Guid.NewGuid().ToString(), []), []); + } +} diff --git a/test/HappyCode.NetCoreBoilerplate.Core.UnitTests/HappyCode.NetCoreBoilerplate.Core.UnitTests.csproj b/test/HappyCode.NetCoreBoilerplate.Core.UnitTests/HappyCode.NetCoreBoilerplate.Core.UnitTests.csproj index 8e26e805e..c32eb0b3b 100644 --- a/test/HappyCode.NetCoreBoilerplate.Core.UnitTests/HappyCode.NetCoreBoilerplate.Core.UnitTests.csproj +++ b/test/HappyCode.NetCoreBoilerplate.Core.UnitTests/HappyCode.NetCoreBoilerplate.Core.UnitTests.csproj @@ -1,4 +1,8 @@ + + $(MSBuildProjectDirectory)\HappyCode.NetCoreBoilerplate.Core.UnitTests.runsettings + + diff --git a/test/HappyCode.NetCoreBoilerplate.Core.UnitTests/HappyCode.NetCoreBoilerplate.Core.UnitTests.runsettings b/test/HappyCode.NetCoreBoilerplate.Core.UnitTests/HappyCode.NetCoreBoilerplate.Core.UnitTests.runsettings new file mode 100644 index 000000000..3f297f311 --- /dev/null +++ b/test/HappyCode.NetCoreBoilerplate.Core.UnitTests/HappyCode.NetCoreBoilerplate.Core.UnitTests.runsettings @@ -0,0 +1,10 @@ + + + + + TEST_VALUE + 36b90293 + 9.9.9 + + + diff --git a/test/HappyCode.NetCoreBoilerplate.Core.UnitTests/Providers/VersionProviderTests.cs b/test/HappyCode.NetCoreBoilerplate.Core.UnitTests/Providers/VersionProviderTests.cs new file mode 100644 index 000000000..a973aee4f --- /dev/null +++ b/test/HappyCode.NetCoreBoilerplate.Core.UnitTests/Providers/VersionProviderTests.cs @@ -0,0 +1,27 @@ +using FluentAssertions; +using HappyCode.NetCoreBoilerplate.Core.Providers; +using Xunit; + +namespace HappyCode.NetCoreBoilerplate.Core.UnitTests.Providers; + +public class VersionProviderTests +{ + private readonly VersionProvider _provider; + + public VersionProviderTests() + { + _provider = new VersionProvider(); + } + + [Fact] + public void Provided_should_returns_expected_values() + { + // act + var result = _provider.VersionEntries; + + // assert + result.Should().NotContainKeys("TEST_ENV", "HC_SHA", "HC_VERSION"); + result.Should().ContainKeys("SHA", "VERSION"); + result.Should().HaveCount(2); + } +}