public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseRouting()
.UseEndpoints(endpoints =>
{
endpoints.MapGet("/api", async context =>
{
await context.Response.WriteAsync($"API is running on {env.EnvironmentName} environment");
});
});
}app.UseEndpoints(endpoints =>
{
var service = endpoints.ServiceProvider.GetService<ITeamMembersService>();
// ...
endpoints.MapGet($"{UrlPrefix}/{{id}}", async context =>
{
var id = GetIdFromRoute(context.Request.RouteValues["id"]);
var teamMember = service.GetById(id);
await context.WriteOk(teamMember);
});
endpoints.MapPost(UrlPrefix, async context =>
{
var command = await context.GetObjectFromBody<TeamMember>();
var id = service.Add(command);
await context.WriteAccepted($"Successfully created team member with id '{id}'.");
});
// ...
});Full REST Api endpoints configuration
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
}
public void Configure(IApplicationBuilder app)
{
app.UseRouting()
.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}[Route("api/team-members")]
public class TeamMembersController : ControllerBase
{
private readonly ITeamMembersService _service;
public TeamMembersController(ITeamMembersService service)
{
_service = service;
}
[HttpGet("{id}")]
public IActionResult GetById(Guid id)
{
var teamMember = _service.GetById(id);
if (teamMember == null)
{
return NotFound($"Team member with given id '{id}' does not exist.");
}
return Ok(teamMember);
}
}Create Controller
The default for ASP.NET Core is now System.Text.Json, which is new in .NET Core 3.0. Consider using System.Text.Json when possible. It's high-performance and doesn't require an additional library dependency. See docs
To use Json.NET in an ASP.NET Core 3.0 project:
- Add a package reference to
Microsoft.AspNetCore.Mvc.NewtonsoftJson.
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.0.0" />- Update
Startup.ConfigureServicesto callAddNewtonsoftJson.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers()
.AddNewtonsoftJson(options =>
options.SerializerSettings.ContractResolver =
new CamelCasePropertyNamesContractResolver());
}Check an app's status, thanks to health checks exposed by HTTP endpoints.
public void ConfigureServices(IServiceCollection services)
{
services.AddHealthChecks();
}
public void Configure(IApplicationBuilder app)
{
app.UseRouting()
.UseEndpoints(endpoints =>
{
endpoints.MapHealthChecks("/health");
});
}GET http://localhost:5000/health
HTTP/1.1 200 OK
Connection: close
Date: Tue, 18 Feb 2020 17:01:20 GMT
Content-Type: text/plain
Server: Kestrel
Cache-Control: no-store, no-cache
Pragma: no-cache
Transfer-Encoding: chunked
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Healthy
There is possibilty to define a set of health checks in pipeline.
public void ConfigureServices(IServiceCollection services)
{
services.AddHealthChecks()
.AddCheck<TeamMembersHealthCheck>("Team members health check");
}internal class TeamMembersHealthCheck : IHealthCheck
{
private readonly ITeamMembersService _service;
public TeamMembersHealthCheck(ITeamMembersService service)
{
_service = service;
}
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = new CancellationToken())
{
var isHealthy = _service.GetAll().Any();
if (isHealthy)
{
return Task.FromResult(HealthCheckResult.Healthy("Team members contains at least one element."));
}
return Task.FromResult(HealthCheckResult.Unhealthy("There are no team members registered..."));
}
}[18:30:31 ERR] Health check Team members health check completed after 0.3627ms with status Unhealthy and 'There are no team members registered...'
.NET Core supports a logging API that works with a variety of built-in and third-party logging providers.
There is an abstraction for it: ILogger<TCategoryName>
The default ASP.NET Core project templates call CreateDefaultBuilder, which adds the following logging providers:
- Console
- Debug
- EventSource
- EventLog (only when running on Windows)
You can replace the default providers with your own choices. Call ClearProviders, and add the providers you want.
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureLogging(logging =>
{
logging.ClearProviders();
logging.AddConsole();
})
// ...[Route("api/team-members")]
public class TeamMembersController : ControllerBase
{
private readonly ITeamMembersService _service;
private readonly ILogger _logger;
public TeamMembersController(ITeamMembersService service, ILogger<TeamMembersController> logger)
{
_service = service;
_logger = logger;
}
// ...
[HttpPost]
public IActionResult Post([FromBody] TeamMember teamMember)
{
var id = _service.Add(teamMember);
return Accepted($"Successfully created team member with id '{id}'.");
}
public override AcceptedResult Accepted(string message)
{
_logger.LogDebug(message);
return Accepted(value: message);
}
}- Do NOT use Serilog or any other Logger directly. Instead, use ILogger abstraction from .NET framework,
- Do NOT use comma in messages. Log event messages should be fragments, not sentences.
- USE arguments as parameters
public void DoSomething(TeamMember teamMember)
{
try
{
_dbContext.Add(teamMember);
_dbContext.SaveChanges();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occurred during adding member {Name} with grade {Grade}", teamMember.Name, teamMember.Grade);
}
}Middleware is software that's assembled into an app pipeline to handle requests and responses.
The .NET Core middleware pipeline can be configured using the following methods from IApplicationBuilder:
Adds a middleware to the pipeline. The component’s code must decide whether to terminate or continue the pipeline. We can add as many Use methods as we want. They will be executed in the order in which they were added to the pipeline.
app.Use(async (context, next) =>
{
logger.LogInformation("Executing middleware...");
context.Request.Headers.Add("correlation-id", "7a902997-bcc8-4162-aba8-fffa93d6bfad");
await next.Invoke();
logger.LogInformation("Middleware executed.");
});Extends Use() configuration about condition specified in the predicate. Conditionally creates a branch in the pipeline that is rejoined to the main pipeline (unlike with MapWhen()).
app.UseWhen(context => context.Request.Path.Value.Contains("team-members"), appBuilder =>
{
appBuilder.Use(async (context, next) =>
{
logger.LogInformation("Executing middleware...");
context.Request.Headers.Add("correlation-id", "7a902997-bcc8-4162-aba8-fffa93d6bfad");
await next.Invoke();
logger.LogInformation("Middleware executed.");
});
});Branches to appropriate middleware components, based on the incoming request's URL path.
app.Map("/api/branch", appBuilder =>
{
appBuilder.Use(async (context, next) =>
{
logger.LogInformation("Executing middleware for route '{ApiRoute}'", "api/branch");
await next.Invoke();
});
appBuilder
.UseRouting()
.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
});Extends Map() configuration about condition specified in the predicate.
app.MapWhen(context => context.Request.Path.Value.Contains("team-members"), appBuilder =>
{
appBuilder.Use(async (context, next) =>
{
logger.LogInformation("Executing middleware for route '{ApiRoute}'", "team-members");
await next.Invoke();
});
});These delegates don't receive a next parameter. The first Run delegate terminates the pipeline. Any middleware components added after Run will not be processed.
app.MapWhen(context => context.Request.Path.Value.Contains("team-members"), appBuilder =>
{
appBuilder.Run(async context =>
{
logger.LogError("Endpoint 'team-members' is not allowed while using branched middleware pipeline.");
await context.Response.WriteAsync("End of the request.");
});
});public void Configure(IApplicationBuilder app)
{
app.UseMiddleware<ExceptionHandlerMiddleware>();
}internal class ExceptionHandlerMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger _logger;
public ExceptionHandlerMiddleware(RequestDelegate next, ILogger<ExceptionHandlerMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
_logger.LogInformation("ExceptionHandlerMiddleware invoked.");
try
{
await _next(context);
}
catch (Exception ex)
{
_logger.LogError(ex, ex.Message);
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
await context.Response.WriteAsync(ex.Message);
}
}
}It's a good pattern because allow to keep configuration in same file as middleware, and makes configure method from Startup.cs easier to read.
internal static class CorrelationIdMiddlewareExtensions
{
public static IApplicationBuilder UseCorrelationIdMiddleware(
this IApplicationBuilder app) => app.UseMiddleware<CorrelationIdMiddleware>();
}
�public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseCorrelationIdMiddleware();
}Because middleware is constructed at app startup, not per-request, scoped lifetime services used by middleware constructors aren't shared with other dependency-injected types during each request. Because of this, your scoped services should be injected into method, but singletons into ctors.
internal class CorrelationIdMiddleware
{
private readonly CorrelationIdProvider _staticCorrelationIdMiddleware;
// It will serve still same instance, because middleware is constructed at app startup
public CorrelationIdMiddleware(CorrelationIdProvider staticCorrelationIdMiddleware)
{
_staticCorrelationIdMiddleware = staticCorrelationIdMiddleware;
}
// It will serve new instance for each request
public Task InvokeAsync(HttpContext context, CorrelationIdProvider scopedCorrelationIdProvider)
{
_logger.LogInformation("Static correlation ID: {CorrelationId}", _staticCorrelationIdMiddleware.CorrelationId);
context.Request.Headers.Add("correlation-id", $"{scopedCorrelationIdProvider.CorrelationId}");
return _next.Invoke(context);
}
}<PackageReference Include="Serilog" Version="2.9.0" />
<PackageReference Include="Serilog.AspNetCore" Version="3.1.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />Serilog completely replaces the logging implementation on .NET Core: it’s not just a provider that works side-by-side with the built-in logging, but rather, an alternative implementation of the .NET Core logging APIs.
That's why we can safely remove Logging configuration from appsettings.json.
public static void Main(string[] args)
{
Log.Logger = new LoggerConfiguration()
.Enrich.FromLogContext()
.WriteTo.Console()
.CreateLogger();
try
{
Log.Information("Starting up");
CreateHostBuilder(args).Build().Run();
}
catch (Exception ex)
{
Log.Fatal(ex, "Host builder error");
}
finally
{
Log.CloseAndFlush();
}
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseSerilog()
.ConfigureLogging(logging =>
{
logging.ClearProviders();
logging.AddConsole();
});public void Configure(IApplicationBuilder app)
{
app.UseSerilogRequestLogging();
}Example:
[14:54:49 INF] HTTP POST /api/team-members responded 202 in 468.0278 ms
docker pull datalust/seq
docker run -e ACCEPT_EULA=Y -p 5341:80 datalust/seq:latest
docker run -e ACCEPT_EULA=Y -v C:/Seq/data:/data -p 5341:80 datalust/seq:latest
<PackageReference Include="Serilog.Sinks.Seq" Version="4.0.0" />Log.Logger = new LoggerConfiguration()
.Enrich.FromLogContext()
.WriteTo.Console()
.WriteTo.Seq("http://localhost:5341") // default Seq port
.CreateLogger();<PackageReference Include="Autofac" Version="4.9.4" />
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="5.0.1" />public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseServiceProviderFactory(new AutofacServiceProviderFactory())
// ...public void ConfigureContainer(ContainerBuilder builder)
{
builder
.Register(factory =>
{
var initTeamMembers = new[]
{
new TeamMember(Guid.NewGuid(), "John", Role.DotNet, 5),
new TeamMember(Guid.NewGuid(), "Franc", Role.DotNet, 6),
new TeamMember(Guid.NewGuid(), "Robert", Role.JavaScript, 2),
new TeamMember(Guid.NewGuid(), "Alex", Role.DevOps, 5)
};
return new TeamMembersService(initTeamMembers);
})
.As<ITeamMembersService>()
.SingleInstance();
}ConfigureContainer is where you can register things directly with Autofac. This runs after ConfigureServices so the things here will override registrations made in ConfigureServices. Don't build the container - that gets done for you by the factory.
HINT: If, for some reason, you need a reference to the built container, you can use the convenience extension method
GetAutofacRoot.
public ILifetimeScope AutofacContainer { get; private set; }
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
AutofacContainer = app.ApplicationServices.GetAutofacRoot();
}- Swagger
- SignalR
- Authentication
- Authorization
- CORS
- C# 8
- Multiple web apps in one process