.NET 5 Docker for Desktop Git for Windows PowerShell Core
- https://github.com/jbogard/MediatR/wiki
- https://www.c-sharpcorner.com/article/cqrs-mediatr-in-net-5/
- https://jasontaylor.dev/clean-architecture-getting-started/
dotnet new sln --name CompanyName.SampleService
dotnet new classlib --output CompanyName.SampleService.Application
dotnet new classlib --output CompanyName.SampleService.Domain
dotnet new classlib --output CompanyName.SampleService.Infrastructure
dotnet new webapi --output CompanyName.SampleService.WebApi --no-https
dotnet sln add ./CompanyName.SampleService.Application/CompanyName.SampleService.Application.csproj
dotnet sln add ./CompanyName.SampleService.Domain/CompanyName.SampleService.Domain.csproj
dotnet sln add ./CompanyName.SampleService.Infrastructure/CompanyName.SampleService.Infrastructure.csproj
dotnet sln add ./CompanyName.SampleService.WebApi/CompanyName.SampleService.WebApi.csproj
cd ./CompanyName.SampleService.WebApi
dotnet add reference ../CompanyName.SampleService.Infrastructure/CompanyName.SampleService.Infrastructure.csproj
cd ../CompanyName.SampleService.Infrastructure
dotnet add reference ../CompanyName.SampleService.Application/CompanyName.SampleService.Application.csproj
cd ../CompanyName.SampleService.Application
dotnet add reference ../CompanyName.SampleService.Domain/CompanyName.SampleService.Domain.csproj
Add required libraries to the solutions.
Installing MediatR.Extensions.Microsoft.DependencyInjection insite Application project
dotnet add package MediatR.Extensions.Microsoft.DependencyInjection
dotnet add package Microsoft.Extensions.Logging.Abstractions
namespace CompanyName.SampleService.Application
{
using System.Reflection;
using MediatR;
using Microsoft.Extensions.DependencyInjection;
public static class DependencyInjection
{
public static IServiceCollection AddApplication(this IServiceCollection services)
{
services.AddMediatR(Assembly.GetExecutingAssembly());
return services;
}
}
}Install Microsoft.Extensions.Configuration.Abstractions and Microsoft.Extensions.Configuration.Binder insite Infrastructure project
cd ./CompanyName.SampleService.Infrastructure
dotnet add package Microsoft.Extensions.Configuration.Abstractions
dotnet add package Microsoft.Extensions.Configuration.Binder
dotnet add package Microsoft.Extensions.Options.ConfigurationExtensions
namespace CompanyName.SampleService.Infrastructure
{
using System.Reflection;
using MediatR;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
public static class DependencyInjection
{
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
{
services.AddMediatR(Assembly.GetExecutingAssembly());
return services;
}
}
}namespace CompanyName.SampleService.WebApi
{
using System.Reflection;
using CompanyName.SampleService.Application;
using CompanyName.SampleService.Infrastructure;
using MediatR;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.OpenApi.Models;
public class Startup
{
public Startup(IConfiguration configuration) =>
this.Configuration = configuration;
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddMediatR(Assembly.GetExecutingAssembly());
services.AddApplication();
services.AddInfrastructure(this.Configuration);
services.AddControllers();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo
{
Title = "CompanyName.SampleService.WebApi",
Version = "v1",
});
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("v1/swagger.json", "CompanyName.SampleService.WebApi v1"));
}
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
}Add logger infrastructure to the WebApi project
dotnet add package Microsoft.IO.RecyclableMemoryStream
dotnet add package Serilog.AspNetCore
and now add it to Program.cs and Startup.cs file
namespace CompanyName.SampleService.WebApi
{
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Serilog;
public class Program
{
private const int EXIT_FAILURE = 1;
private const int EXIT_SUCCESS = 0;
public static async Task<int> Main(string[] args)
{
var configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.AddEnvironmentVariables()
.Build();
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(configuration)
.CreateLogger();
try
{
Log.Information("Starting host");
await CreateHostBuilder(args).Build().RunAsync();
return EXIT_SUCCESS;
}
catch (Exception exception)
{
Log.Fatal(exception, "Host terminated unexpectedly");
return EXIT_FAILURE;
}
finally
{
Log.CloseAndFlush();
}
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseSerilog()
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
}finally, change appsettings.json file
{
"Serilog": {
"Using": [
"Serilog.Sinks.Console"
],
"MinimumLevel": {
"Default": "Debug",
"Override": {
"Microsoft": "Information",
"System": "Information"
}
},
"Enrich": [
"FromLogContext",
"WithMachineName",
"WithThreadId"
],
"WriteTo": [
{
"Name": "Console",
"Args": {
"formatter": "Serilog.Formatting.Json.JsonFormatter, Serilog"
}
}
]
}
}namespace CompanyName.SampleService.WebApi
{
using System;
using System.Diagnostics.Contracts;
using Microsoft.AspNetCore.Mvc.Filters;
using Serilog;
public sealed class SerilogLoggingActionFilter : IActionFilter
{
private readonly IDiagnosticContext diagnosticContext;
public SerilogLoggingActionFilter(IDiagnosticContext diagnosticContext)
{
this.diagnosticContext = diagnosticContext
?? throw new ArgumentNullException(nameof(diagnosticContext));
}
public void OnActionExecuting(ActionExecutingContext context)
{
Contract.Assert(context != null);
this.diagnosticContext.Set("ActionId", context.ActionDescriptor.Id);
this.diagnosticContext.Set("ActionName", context.ActionDescriptor.DisplayName);
this.diagnosticContext.Set("RouteData", context.ActionDescriptor.RouteValues);
this.diagnosticContext.Set("ValidationState", context.ModelState.IsValid);
}
public void OnActionExecuted(ActionExecutedContext context)
{
}
}
}namespace CompanyName.SampleService.WebApi
{
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.IO;
internal sealed class RequestResponseLoggingMiddleware
{
private readonly ILogger logger;
private readonly RequestDelegate next;
private readonly RecyclableMemoryStreamManager recyclableMemoryStreamManager;
public RequestResponseLoggingMiddleware(RequestDelegate next, ILoggerFactory loggerFactory)
{
this.logger = loggerFactory.CreateLogger<RequestResponseLoggingMiddleware>();
this.next = next;
this.recyclableMemoryStreamManager = new RecyclableMemoryStreamManager();
}
public async Task Invoke(HttpContext context)
{
await this.LogRequest(context).ConfigureAwait(true);
await this.LogResponse(context).ConfigureAwait(true);
}
private async Task LogRequest(HttpContext context)
{
context.Request.EnableBuffering();
await using var requestStream = this.recyclableMemoryStreamManager.GetStream();
await context.Request.Body.CopyToAsync(requestStream).ConfigureAwait(true);
var text = ReadStreamInChunks(requestStream);
if (!string.IsNullOrEmpty(text))
{
this.logger.LogDebug(text);
}
context.Request.Body.Position = 0;
}
private async Task LogResponse(HttpContext context)
{
var originalBodyStream = context.Response.Body;
await using var responseBody = this.recyclableMemoryStreamManager.GetStream();
context.Response.Body = responseBody;
await this.next(context);
context.Response.Body.Seek(0, SeekOrigin.Begin);
var text = await new StreamReader(context.Response.Body).ReadToEndAsync();
context.Response.Body.Seek(0, SeekOrigin.Begin);
this.logger.LogDebug(text);
await responseBody.CopyToAsync(originalBodyStream);
}
private static string ReadStreamInChunks(Stream stream)
{
const int readChunkBufferLength = 4096;
stream.Seek(0, SeekOrigin.Begin);
using var textWriter = new StringWriter();
using var reader = new StreamReader(stream);
var readChunk = new char[readChunkBufferLength];
int readChunkLength;
do
{
readChunkLength = reader.ReadBlock(readChunk, 0, readChunkBufferLength);
textWriter.Write(readChunk, 0, readChunkLength);
} while (readChunkLength > 0);
return textWriter.ToString();
}
}
internal static class RequestResponseLoggingMiddlewareExtensions
{
public static IApplicationBuilder UseRequestResponseLogging(this IApplicationBuilder builder)
{
return builder.UseMiddleware<RequestResponseLoggingMiddleware>();
}
}
}and some changes to Startup.cs file
namespace CompanyName.SampleService.WebApi
{
using System.Reflection;
using CompanyName.SampleService.Application;
using CompanyName.SampleService.Infrastructure;
using MediatR;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.OpenApi.Models;
using Serilog;
public sealed class Startup
{
public Startup(IConfiguration configuration) =>
this.Configuration = configuration;
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddLogging(configure => configure.AddSerilog());
services.AddHealthChecks();
services.AddMediatR(Assembly.GetExecutingAssembly());
services.AddApplication();
services.AddInfrastructure(this.Configuration);
services.AddControllers(options =>
{
options.Filters.Add<SerilogLoggingActionFilter>();
});
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo
{
Title = "CompanyName.SampleService.WebApi",
Version = "v1",
});
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("v1/swagger.json", "CompanyName.SampleService.WebApi v1"));
}
app.UseHealthChecks("/health");
app.UseSerilogRequestLogging(options =>
{
options.EnrichDiagnosticContext = EnrichDiagnosticContext;
});
if (env.IsDevelopment())
{
app.UseRequestResponseLogging();
}
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
private static void EnrichDiagnosticContext(IDiagnosticContext diagnosticContext, HttpContext httpContext)
{
var request = httpContext.Request;
diagnosticContext.Set("Host", request.Host);
diagnosticContext.Set("Protocol", request.Protocol);
diagnosticContext.Set("Scheme", request.Scheme);
foreach (var (name, value) in request.Headers)
{
diagnosticContext.Set(name, value);
}
if (request.QueryString.HasValue)
{
diagnosticContext.Set("QueryString", request.QueryString.Value);
}
diagnosticContext.Set("ContentType", httpContext.Response.ContentType);
var endpoint = httpContext.GetEndpoint();
if (endpoint is { })
{
diagnosticContext.Set("EndpointName", endpoint.DisplayName);
}
}
}
}Install AutoMapper.Extensions.Microsoft.DependencyInjection insite Infrastructure project
cd ./CompanyName.SampleService.Infrastructure
dotnet add package AutoMapper.Extensions.Microsoft.DependencyInjection
namespace CompanyName.SampleService.WebApi.Controllers
{
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using CompanyName.SampleService.Application.Queries;
using CompanyName.SampleService.Application.ViewModels;
using MediatR;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
[ApiController]
[Route("[controller]")]
public sealed class WeatherForecastController : ControllerBase
{
private readonly ILogger<WeatherForecastController> logger;
private readonly IMediator mediator;
public WeatherForecastController(ILogger<WeatherForecastController> logger, IMediator mediator) =>
(this.logger, this.mediator) = (logger, mediator);
[HttpGet]
public async Task<IEnumerable<WeatherForecast>> Get(CancellationToken cancellationToken = default) =>
await this.mediator.Send(new GetWeatherForecasts { }, cancellationToken);
}
}namespace CompanyName.SampleService.Application.Queries
{
using System.Collections.Generic;
using CompanyName.SampleService.Application.ViewModels;
using MediatR;
public sealed record GetWeatherForecasts : IRequest<IReadOnlyList<WeatherForecast>>
{
}
}namespace CompanyName.SampleService.Application.ViewModels
{
using System;
public sealed record WeatherForecast
{
public DateTime Date { get; init; }
public int TemperatureC { get; init; }
public int TemperatureF { get; init; }
public string Summary { get; init; }
}
}Create CompanyName/SampleService/Infrastructure/WeatherForecasts/Interfaces/IWeatherForecastService.cs file
namespace CompanyName.SampleService.Infrastructure.WeatherForecasts.Interfaces
{
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using CompanyName.SampleService.Infrastructure.WeatherForecasts.Models;
internal interface IWeatherForecastService
{
Task<IReadOnlyList<WeatherForecast>> Get(CancellationToken cancellationToken = default);
}
}namespace CompanyName.SampleService.Infrastructure.WeatherForecasts.Models
{
using System;
internal sealed record WeatherForecast
{
public DateTime Date { get; init; }
public int TemperatureC { get; init; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
public string Summary { get; init; }
}
}Create CompanyName/SampleService/Infrastructure/WeatherForecasts/Profiles/WeatherForecastProfile.cs file
namespace CompanyName.SampleService.Infrastructure.WeatherForecasts.Profiles
{
using AutoMapper;
using SourceWeatherForecast = CompanyName.SampleService.Infrastructure.WeatherForecasts.Models.WeatherForecast;
using TargetWeatherForecast = CompanyName.SampleService.Application.ViewModels.WeatherForecast;
internal sealed class WeatherForecastProfile : Profile
{
public WeatherForecastProfile()
{
this.CreateMap<SourceWeatherForecast, TargetWeatherForecast>()
.ForMember(target => target.Date, options => options.MapFrom(source => source.Date))
.ForMember(target => target.Summary, options => options.MapFrom(source => source.Summary))
.ForMember(target => target.TemperatureC, options => options.MapFrom(source => source.TemperatureC))
.ForMember(target => target.TemperatureF, options => options.MapFrom(source => source.TemperatureF))
;
}
}
}More information about how to use AutoMapper you can find on [https://docs.automapper.org/en/latest/Getting-started.html] (https://docs.automapper.org/en/latest/Getting-started.html)
Create CompanyName/SampleService/Infrastructure/WeatherForecasts/QueryHandlers/GetWeatherForecastsHandler.cs file
namespace CompanyName.SampleService.Infrastructure.WeatherForecasts.QueryHandlers
{
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using CompanyName.SampleService.Application.Queries;
using CompanyName.SampleService.Application.ViewModels;
using CompanyName.SampleService.Infrastructure.WeatherForecasts.Interfaces;
using AutoMapper;
using MediatR;
internal sealed class GetWeatherForecastsHandler : IRequestHandler<GetWeatherForecasts, IReadOnlyList<WeatherForecast>>
{
private readonly IMapper mapper;
private readonly IWeatherForecastService service;
public GetWeatherForecastsHandler(IMapper mapper, IWeatherForecastService service) =>
(this.mapper, this.service) = (mapper, service);
public async Task<IReadOnlyList<WeatherForecast>> Handle(GetWeatherForecasts request, CancellationToken cancellationToken)
{
var source = await this.service.Get(cancellationToken);
var result = this.mapper.Map<IReadOnlyList<WeatherForecast>>(source);
return result;
}
}
}Create CompanyName/SampleService/Infrastructure/WeatherForecasts/Services/WeatherForecastService.cs file
namespace CompanyName.SampleService.Infrastructure.WeatherForecasts.Services
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using CompanyName.SampleService.Infrastructure.WeatherForecasts.Interfaces;
using CompanyName.SampleService.Infrastructure.WeatherForecasts.Models;
internal sealed class WeatherForecastService : IWeatherForecastService
{
private static readonly string[] Summaries = new[]
{
"Freezing",
"Bracing",
"Chilly",
"Cool",
"Mild",
"Warm",
"Balmy",
"Hot",
"Sweltering",
"Scorching",
};
public async Task<IReadOnlyList<WeatherForecast>> Get(CancellationToken cancellationToken = default)
{
var rng = new Random();
var result = Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = rng.Next(-20, 55),
Summary = Summaries[rng.Next(Summaries.Length)]
})
.ToList();
return await Task.FromResult(result.AsReadOnly());
}
}
}namespace CompanyName.SampleService.Infrastructure
{
using System.Reflection;
using CompanyName.SampleService.Infrastructure.WeatherForecasts.Interfaces;
using CompanyName.SampleService.Infrastructure.WeatherForecasts.Services;
using MediatR;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
public static class DependencyInjection
{
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
{
services.AddAutoMapper(Assembly.GetExecutingAssembly());
services.AddMediatR(Assembly.GetExecutingAssembly());
services.AddSingleton<IWeatherForecastService, WeatherForecastService>();
return services;
}
}
}From the server site use Commands/Queries/ViewModels from Application project
CompanyName.SampleChildService.Application.Commands
CompanyName.SampleChildService.Application.Queries
CompanyName.SampleChildService.Application.ViewModels
on client use models in Infrastructure project
CompanyName.SampleService.Infrastructure.SampleChildService.Models
and create new folders
CompanyName.SampleService.Infrastructure.SampleChildService.Interfaces
CompanyName.SampleService.Infrastructure.SampleChildService.Profiles
CompanyName.SampleService.Infrastructure.SampleChildService.Services
Install Microsoft.Extensions.Http.Polly and System.Net.Http.Json insite Infrastructure project
cd ./CompanyName.SampleService.Infrastructure
dotnet add package Microsoft.Extensions.Http.Polly
dotnet add package System.Net.Http.Json
Use configuration object for external service
namespace CompanyName.SampleService.Infrastructure.SampleChildService
{
using System;
internal sealed record SampleChildServiceOptions
{
public string SectionName => "SampleChildService";
public Uri BaseAddress { get; init; } = new Uri("about:blank");
public int RetryCount { get; init; } = default;
public int RetrySleepDurationInMilliSeconds { get; set; } = default;
public int TimeoutInMilliSeconds { get; init; } = default;
}
}namespace CompanyName.SampleService.Infrastructure
{
using System;
using System.Net;
using System.Net.Http.Headers;
using System.Reflection;
using CompanyName.SampleService.Infrastructure.SampleChildService;
using CompanyName.SampleService.Infrastructure.SampleChildService.Interfaces;
using CompanyName.SampleService.Infrastructure.SampleChildService.Services;
using CompanyName.SampleService.Infrastructure.WeatherForecasts.Interfaces;
using CompanyName.SampleService.Infrastructure.WeatherForecasts.Services;
using MediatR;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Polly;
using Polly.Extensions.Http;
public static class DependencyInjection
{
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
{
services.AddAutoMapper(Assembly.GetExecutingAssembly());
services.AddMediatR(Assembly.GetExecutingAssembly());
services.AddSingleton<IWeatherForecastService, WeatherForecastService>();
var options = new SampleChildServiceOptions();
configuration.GetSection(options.SectionName).Bind(options);
services.AddHttpClient("SampleChildService", client =>
{
client.BaseAddress = options.BaseAddress;
client.Timeout = TimeSpan.FromMilliseconds(options.TimeoutInMilliSeconds);
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
}).ConfigureHttpMessageHandlerBuilder(builder =>
{
#if DEBUG
builder.PrimaryHandler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (_, _, _, _) => true,
};
#endif
}).AddPolicyHandler(_ =>
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.OrResult(result => result.StatusCode != HttpStatusCode.Accepted)
.WaitAndRetryAsync(
options.RetryCount,
retryAttempt => TimeSpan.FromMilliseconds(options.RetrySleepDurationInMilliSeconds)
);
});
services.AddSingleton<ISampleChildService, SampleChildService>();
return services;
}
}
}Create CompanyName/SampleService/Infrastructure/SampleChildService/Models/SampleChildRequest.cs file
namespace CompanyName.SampleService.Infrastructure.SampleChildService.Models
{
internal sealed record SampleChildRequest
{
}
}Create CompanyName/SampleService/Infrastructure/SampleChildService/Interfaces/ISampleChildService.cs file
namespace CompanyName.SampleService.Infrastructure.SampleChildService.Interfaces
{
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using CompanyName.SampleService.Infrastructure.SampleChildService.Models;
internal interface ISampleChildService
{
Task<IReadOnlyList<SampleChildResponse>> GetAsync(SampleChildRequest request, CancellationToken cancellationToken = default);
}
}Create CompanyName/SampleService/Infrastructure/SampleChildService/Services/SampleChildService.cs file
namespace CompanyName.SampleService.Infrastructure.SampleChildService.Services
{
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading;
using System.Threading.Tasks;
using CompanyName.SampleService.Infrastructure.Extensions;
using CompanyName.SampleService.Infrastructure.SampleChildService.Interfaces;
using CompanyName.SampleService.Infrastructure.SampleChildService.Models;
using Microsoft.Extensions.Logging;
internal sealed class SampleChildService : ISampleChildService
{
private const string ClientName = "SampleChildService";
private readonly HttpClient client;
private readonly ILogger<SampleChildService> logger;
public SampleChildService(IHttpClientFactory httpClientFactory, ILogger<SampleChildService> logger) =>
(this.client, this.logger) = (httpClientFactory.CreateClient(ClientName), logger);
public async Task<IReadOnlyList<SampleChildResponse>> GetAsync(SampleChildRequest request, CancellationToken cancellationToken = default)
{
using var loggerScope = this.logger.BeginPropertyScope(
("BaseAddress", $"{this.client.BaseAddress}")
);
var requestUri = $"{this.client.BaseAddress}Get";
try
{
var response = await this.client.PostAsJsonAsync(requestUri, request);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<IReadOnlyList<SampleChildResponse>>();
return result ?? new List<SampleChildResponse>().AsReadOnly();
}
catch (Exception exception)
{
this.logger.LogWarning(exception, exception.Message);
}
return await Task.FromResult(new List<SampleChildResponse>().AsReadOnly());
}
}
}Create example handler CompanyName/SampleService/Infrastructure/SampleChildService/QueryHandlers/GetSampleChildServiceRecordsHandler.cs file
namespace CompanyName.SampleService.Infrastructure.SampleChildService.QueryHandlers
{
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using CompanyName.SampleService.Application.Queries;
using CompanyName.SampleService.Infrastructure.SampleChildService.Interfaces;
using CompanyName.SampleService.Infrastructure.SampleChildService.Models;
using MediatR;
internal sealed class GetSampleChildServiceRecordsHandler : IRequestHandler<GetSampleChildServiceRecords, IReadOnlyList<string>>
{
private readonly ISampleChildService service;
public GetSampleChildServiceRecordsHandler(ISampleChildService service) =>
(this.service) = (service);
public async Task<IReadOnlyList<string>> Handle(GetSampleChildServiceRecords request, CancellationToken cancellationToken)
{
var sampleChildRequest = new SampleChildRequest();
var sampleChildResponse = await this.service.GetAsync(sampleChildRequest, cancellationToken);
var response = new List<string>();
foreach (var sampleChildResponseEntity in sampleChildResponse)
{
response.Add(string.Empty);
}
return await Task.FromResult(response.AsReadOnly());
}
}
}FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
WORKDIR /src
COPY Verus.InternalVerification.sln Verus.InternalVerification.sln
COPY Verus.InternalVerification.Application/Verus.InternalVerification.Application.csproj Verus.InternalVerification.Application/Verus.InternalVerification.Application.csproj
COPY Verus.InternalVerification.Infrastructure/Verus.InternalVerification.Infrastructure.csproj Verus.InternalVerification.Infrastructure/Verus.InternalVerification.Infrastructure.csproj
COPY Verus.InternalVerification.WebApi/Verus.InternalVerification.WebApi.csproj Verus.InternalVerification.WebApi/Verus.InternalVerification.WebApi.csproj
RUN dotnet restore
COPY . .
RUN dotnet build -c Release -o /app/build
RUN dotnet publish -c Release -o /app/publish
RUN cp ca.crt /app/publish/ca.crt
FROM mcr.microsoft.com/dotnet/aspnet:5.0
WORKDIR /app
COPY --from=build /app/publish .
RUN mkdir -p /usr/local/share/ca-certificates && cp ca.crt /usr/local/share/ca-certificates/ca.crt
RUN update-ca-certificates
RUN groupadd -g 10000 dotnet && useradd -u 10000 -g dotnet -d /app dotnet && chown -R dotnet:dotnet /app
USER dotnet:dotnet
ENV ASPNETCORE_URLS https://*:5443
EXPOSE 5443
ENTRYPOINT ["dotnet", "Verus.InternalVerification.WebApi.dll"]
Line RUN cp ca.crt /app/publish/ca.crt will copy root certificate injected to source code Line RUN mkdir -p /usr/local/share/ca-certificates && cp ca.crt /usr/local/share/ca-certificates/ca.crt copy root certificate from build stage to runtime Line RUN update-ca-certificates update list of root certificates inside container
namespace Verus.InternalVerification.WebApi
{
using System;
using System.Linq;
using System.Security.Authentication;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Serilog;
public class Program
{
private const int EXIT_FAILURE = 1;
private const int EXIT_SUCCESS = 0;
public static async Task<int> Main(string[] args)
{
var configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.Build();
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(configuration)
.CreateLogger();
try
{
Log.Information("Application is starting up...");
await CreateHostBuilder(args).Build().RunAsync();
return EXIT_SUCCESS;
}
catch (Exception exception)
{
Log.Fatal(exception, "Host terminated unexpectedly");
return EXIT_FAILURE;
}
finally
{
Log.CloseAndFlush();
}
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseSerilog()
.ConfigureWebHostDefaults(webBuilder =>
{
#if !DEBUG
webBuilder.UseKestrel(serverOptions =>
{
serverOptions.ConfigureHttpsDefaults(listenOptions =>
{
var cert = Base64ToBase64(Environment.GetEnvironmentVariable("SSL_RSA_CERT") ?? string.Empty);
var key = Base64ToBase64(Environment.GetEnvironmentVariable("SSL_RSA_KEY") ?? string.Empty);
listenOptions.SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13;
listenOptions.ServerCertificate = BuildCertificate(cert, key);
});
});
#endif
webBuilder.UseStartup<Startup>();
});
private static string Base64ToBase64(string source)
{
try
{
var base64 = source.Replace("\\n", string.Empty).Replace("\n", string.Empty); // Remove new line asci or 'new line' from text
var bytes = Convert.FromBase64String(base64); // Convert base64 string to bytes array
var text = Encoding.Default.GetString(bytes); // Convert bytes array to 'original' string
return text.Replace("\\n", Environment.NewLine).Replace("\"", string.Empty); // Replace 'new line' text to new line, remove quotes
}
catch
{
return source;
}
}
private static X509Certificate2 BuildCertificate(string cert, string privateKey)
{
var pfxWithoutKey = new X509Certificate2(Encoding.Default.GetBytes(cert));
var lines = privateKey.Split(new[] { "\n", "\r", Environment.NewLine, }, StringSplitOptions.RemoveEmptyEntries);
var filtered = string.Join(null, lines.Where(x => !x.StartsWith("-")));
var key = Convert.FromBase64String(filtered);
var rsa = RSA.Create();
rsa.ImportRSAPrivateKey(key, out _);
using var pfxWithKey = pfxWithoutKey.CopyWithPrivateKey(rsa);
var pfx = new X509Certificate2(pfxWithKey.Export(X509ContentType.Pfx), (string)null, X509KeyStorageFlags.PersistKeySet);
var store = new X509Store(StoreName.My, StoreLocation.CurrentUser, OpenFlags.ReadWrite);
try
{
store.Add(pfx);
}
finally
{
store.Close();
}
return pfx;
}
}
}To use certificate from AWS injected as environment variable to container we need to decode them from Base64 into binary format. In the code I left some comment what is going on. Most importand part is export combined (certificate + private key) and export certificate in this form to local certificate storage. Without this step you can not use combined certificated in Kestrel. We are doing that in BuildCertificate method.
If we wont to use tests we should use different structure for the solution. We need to create two folders, one for application (src) and one for tests (tests).
mkdir src
mkdir testsInside src folder we will keep main application and source code tree to build docker image. Inside tests folder we will keep our tests and we will use this solution for develop.
We need to create dedicated solutions for development/tests and add project from main source code tree (from src folder)
dotnet new sln --name CompanyName.SampleService.Tests
dotnet sln add ../src/CompanyName.SampleService.Application/CompanyName.SampleService.Application.csproj
dotnet sln add ../src/CompanyName.SampleService.Domain/CompanyName.SampleService.Domain.csproj
dotnet sln add ../src/CompanyName.SampleService.Infrastructure/CompanyName.SampleService.Infrastructure.csproj
dotnet sln add ../src/CompanyName.SampleService.WebApi/CompanyName.SampleService.WebApi.csprojWe can create our unit test project
dotnet new mstest --output CompanyName.SampleService.UnitTests
dotnet sln add ./CompanyName.SampleService.UnitTests/CompanyName.SampleService.UnitTests.csproj
cd CompanyName.SampleService.UnitTests
dotnet add reference ../../src/CompanyName.SampleService.Application/CompanyName.SampleService.Application.csproj
dotnet add reference ../../src/CompanyName.SampleService.Domain/CompanyName.SampleService.Domain.csproj
dotnet add reference ../../src/CompanyName.SampleService.Infrastructure/CompanyName.SampleService.Infrastructure.csprojWe need to add some nuget packages
dotnet add package FluentAssertions
dotnet add package MoqBecause we are using limited visibility for handlers we need to modify our *.csproj files
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<InternalsVisibleTo Include="CompanyName.SampleService.UnitTests" />
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="9.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\CompanyName.SampleService.Domain\CompanyName.SampleService.Domain.csproj" />
</ItemGroup>
</Project>Change /src/CompanyName.SampleService.Infrastructure/CompanyName.SampleService.Infrastructure.csproj file
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<InternalsVisibleTo Include="CompanyName.SampleService.UnitTests" />
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="8.1.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="5.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\CompanyName.SampleService.Application\CompanyName.SampleService.Application.csproj" />
</ItemGroup>
</Project>Now we need to create first unit test
namespace CompanyName.SampleService.UnitTests
{
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using AutoMapper;
using CompanyName.SampleService.Application.Queries;
using CompanyName.SampleService.Infrastructure.WeatherForecasts.Interfaces;
using CompanyName.SampleService.Infrastructure.WeatherForecasts.Models;
using CompanyName.SampleService.Infrastructure.WeatherForecasts.Profiles;
using CompanyName.SampleService.Infrastructure.WeatherForecasts.QueryHandlers;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
[TestClass]
public sealed class WeatherForecastsTests
{
public TestContext TestContext { get; set; }
private static readonly string[] Summaries = new[]
{
"Balmy",
"Bracing",
"Chilly",
"Cool",
"Freezing",
"Hot",
"Mild",
"Warm",
"Scorching",
"Sweltering",
};
[TestMethod]
public async Task Get_List_of_WeatherForecasts()
{
var config = new MapperConfiguration(cfg => cfg.AddProfile<WeatherForecastProfile>());
var mapper = config.CreateMapper();
var weatherForecasts = new List<WeatherForecast>
{
new WeatherForecast{
Date = new DateTime(2021, 01, 01),
Summary = "Cool",
TemperatureC = -20,
},
new WeatherForecast{
Date = new DateTime(2021, 07, 22),
Summary = "Warm",
TemperatureC = 28,
},
};
var service = new Mock<IWeatherForecastService>();
service.Setup(m => m.Get(CancellationToken.None)).ReturnsAsync(weatherForecasts);
//Arrange
var query = new GetWeatherForecasts();
var handler = new GetWeatherForecastsHandler(mapper, service.Object);
//Act
var result = await handler.Handle(query, CancellationToken.None);
this.TestContext.WriteLine(result.ToString());
//Assert
result.Should().NotBeNull();
result.Count.Should().Be(2);
foreach (var item in result)
{
item.Summary.Should().BeOneOf(Summaries);
item.TemperatureC.Should().BeInRange(-20, 55);
}
}
[TestMethod]
public async Task TestException()
{
var config = new MapperConfiguration(cfg => cfg.AddProfile<WeatherForecastProfile>());
var mapper = config.CreateMapper();
var weatherForecasts = new List<WeatherForecast>
{
new WeatherForecast{
Date = new DateTime(2021, 01, 01),
Summary = "Cool",
TemperatureC = -20,
},
new WeatherForecast{
Date = new DateTime(2021, 07, 22),
Summary = "Warm",
TemperatureC = 28,
},
};
var service = new Mock<IWeatherForecastService>();
service.Setup(m => m.Get(CancellationToken.None)).ReturnsAsync(weatherForecasts);
//Arrange
GetWeatherForecasts query = null;
var handler = new GetWeatherForecastsHandler(mapper, service.Object);
//Act
Func<Task<IReadOnlyList<CompanyName.SampleService.Application.ViewModels.WeatherForecast>>> action = async () => await handler.Handle(query, CancellationToken.None);
//Assert
await action.Should().ThrowExactlyAsync<NullReferenceException>();
}
}
}Now we can check our test
dotnet testWe can create our unit test project
dotnet new mstest --output CompanyName.SampleService.IntegrationTests
dotnet sln add ./CompanyName.SampleService.IntegrationTests/CompanyName.SampleService.IntegrationTests.csproj
cd CompanyName.SampleService.IntegrationTests
dotnet add reference ../../src/CompanyName.SampleService.WebApi/CompanyName.SampleService.WebApi.csprojWe need to add some nuget packages
dotnet add package FluentAssertions
dotnet add package Microsoft.AspNetCore.TestHost
dotnet add package Microsoft.Extensions.Configuration
dotnet add package Moqnamespace CompanyName.SampleService.IntegrationTests
{
using System.IO;
using System.Net;
using System.Net.Http;
using System.Reflection;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.Configuration;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using CompanyName.SampleService.WebApi;
using Serilog;
[TestClass]
public class IntegrationTests
{
private readonly HttpClient client;
private readonly TestServer server;
public TestContext TestContext { get; set; }
public IntegrationTests()
{
var configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.AddEnvironmentVariables()
.Build();
var build = new WebHostBuilder()
.UseContentRoot(Path.GetDirectoryName(Assembly.GetAssembly(typeof(Startup)).Location))
.UseConfiguration(configuration)
.ConfigureTestServices(services =>
{
})
.UseSerilog()
.UseStartup<Startup>();
this.server = new TestServer(build);
this.client = this.server.CreateClient();
}
[TestMethod]
public async Task WeatherForecastControllerTest()
{
//Arrange
//Act
var json = await this.client.GetStringAsync("/WeatherForecast");
//Assert
TestContext.WriteLine(json);
json.Should().NotBeNullOrWhiteSpace();
}
[TestMethod]
public async Task WeatherForecastControllerEndpointNotFoundTest()
{
//Arrange
//Act
var response = await this.client.GetAsync("/NotExists");
//Assert
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
}
[TestMethod]
public async Task HealthChecksTest()
{
//Arrange
//Act
var response = await this.client.GetStringAsync("/health");
//Assert
response.Should().NotBeNullOrWhiteSpace();
response.Should().Equals("Healthy");
}
}
}Now we can check our test
dotnet test -l "console;verbosity=detailed"Create extensions folder
mkdir extensionsCreate volumens
docker volume create sonar-data
docker volume create sonar-logsversion: "3.9"
volumes:
sonar-data:
external: true
sonar-logs:
external: true
services:
sonarqube:
image: sonarqube:9.0.1-community
ports:
- "9000:9000"
volumes:
- sonar-data:/opt/sonarqube/data
- sonar-logs:/opt/sonarqube/logs
- ./extensions:/opt/sonarqube/extensions
Now we can stert the service.
docker compose up -dUse favorites web browser and navigate to http://localhost:9000. After login/set up password please create new project (ex: SampleService).
dotnet tool install --global dotnet-sonarscannerNow we can run scanner.
dotnet sonarscanner begin /k:"SampleService" /d:sonar.login="user" /d:sonar.password="password" /d:sonar.host.url="http://localhost:9000"
dotnet build CompanyName.SampleService.sln
dotnet sonarscanner end /d:sonar.login="user" /d:sonar.password="password"Use favorites web browser and navigate to http://localhost:9000. You can see some guidelines how to make your code more secure. Following the instruction given by SonarQube we can improve our code.
namespace CompanyName.SampleService.WebApi
{
using System;
using System.Diagnostics.Contracts;
using Microsoft.AspNetCore.Mvc.Filters;
using Serilog;
public sealed class SerilogLoggingActionFilter : IActionFilter
{
private readonly IDiagnosticContext diagnosticContext;
public SerilogLoggingActionFilter(IDiagnosticContext diagnosticContext)
{
this.diagnosticContext = diagnosticContext
?? throw new ArgumentNullException(nameof(diagnosticContext));
}
public void OnActionExecuting(ActionExecutingContext context)
{
Contract.Assert(context != null);
this.diagnosticContext.Set("ActionId", context.ActionDescriptor.Id);
this.diagnosticContext.Set("ActionName", context.ActionDescriptor.DisplayName);
this.diagnosticContext.Set("RouteData", context.ActionDescriptor.RouteValues);
this.diagnosticContext.Set("ValidationState", context.ModelState.IsValid);
}
public void OnActionExecuted(ActionExecutedContext context)
{
// Method intentionally left empty.
}
}
}Change ./src/CompanyName.SampleService.Infrastructure.WeatherForecasts.Services/WeatherForecastService.cs file to
namespace CompanyName.SampleService.Infrastructure.WeatherForecasts.Services
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using CompanyName.SampleService.Infrastructure.WeatherForecasts.Interfaces;
using CompanyName.SampleService.Infrastructure.WeatherForecasts.Models;
internal sealed class WeatherForecastService : IWeatherForecastService
{
private static readonly string[] Summaries = new[]
{
"Freezing",
"Bracing",
"Chilly",
"Cool",
"Mild",
"Warm",
"Balmy",
"Hot",
"Sweltering",
"Scorching",
};
public async Task<IReadOnlyList<WeatherForecast>> Get(CancellationToken cancellationToken = default)
{
var result = Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = RandomNumberGenerator.GetInt32(-20, 55),
Summary = Summaries[RandomNumberGenerator.GetInt32(Summaries.Length)]
})
.ToList();
return await Task.FromResult(result.AsReadOnly());
}
}
}