Skip to content

Commit 8198596

Browse files
committed
refactor: extract service registrations to ServiceCollectionExtensions
1 parent cbe8e2d commit 8198596

File tree

10 files changed

+211
-66
lines changed

10 files changed

+211
-66
lines changed

.codacy.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ exclude_paths:
2424
- '**/Configurations/**'
2525
- '**/Data/**'
2626
- '**/Enums/**'
27+
- '**/Extensions/**'
2728
- '**/Mappings/**'
2829
- '**/Migrations/**'
2930
- '**/Models/**'

codecov.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ ignore:
5858
- .*\/Configurations\/.*
5959
- .*\/Data\/.*
6060
- .*\/Enums\/.*
61+
- .*\/Extensions\/.*
6162
- .*\/Mappings\/.*
6263
- .*\/Migrations\/.*
6364
- .*\/Models\/.*

src/Dotnet.Samples.AspNetCore.WebApi/Dotnet.Samples.AspNetCore.WebApi.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
</PropertyGroup>
99

1010
<ItemGroup Label="Development dependencies">
11+
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.0.0" />
1112
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="9.0.0">
1213
<PrivateAssets>all</PrivateAssets>
1314
</PackageReference>
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
using System.Threading.RateLimiting;
2+
using Dotnet.Samples.AspNetCore.WebApi.Configurations;
3+
using Dotnet.Samples.AspNetCore.WebApi.Data;
4+
using Dotnet.Samples.AspNetCore.WebApi.Mappings;
5+
using Dotnet.Samples.AspNetCore.WebApi.Repositories;
6+
using Dotnet.Samples.AspNetCore.WebApi.Services;
7+
using Dotnet.Samples.AspNetCore.WebApi.Utilities;
8+
using Dotnet.Samples.AspNetCore.WebApi.Validators;
9+
using FluentValidation;
10+
using Microsoft.EntityFrameworkCore;
11+
using Microsoft.OpenApi.Models;
12+
using Serilog;
13+
14+
namespace Dotnet.Samples.AspNetCore.WebApi.Extensions;
15+
16+
/// <summary>
17+
/// Extension methods for WebApplicationBuilder to encapsulate service configuration.
18+
/// </summary>
19+
public static class ServiceCollectionExtensions
20+
{
21+
/// <summary>
22+
/// Adds DbContextPool with SQLite configuration for PlayerDbContext.
23+
/// </summary>
24+
/// <param name="services">The IServiceCollection instance.</param>
25+
/// <param name="environment">The web host environment.</param>
26+
/// <returns>The IServiceCollection for method chaining.</returns>
27+
public static IServiceCollection AddDbContextPoolWithSqlite(
28+
this IServiceCollection services,
29+
IWebHostEnvironment environment
30+
)
31+
{
32+
services.AddDbContextPool<PlayerDbContext>(options =>
33+
{
34+
var dataSource = Path.Combine(
35+
AppContext.BaseDirectory,
36+
"storage",
37+
"players-sqlite3.db"
38+
);
39+
options.UseSqlite($"Data Source={dataSource}");
40+
41+
if (environment.IsDevelopment())
42+
{
43+
options.EnableSensitiveDataLogging();
44+
options.LogTo(Log.Logger.Information, LogLevel.Information);
45+
}
46+
});
47+
48+
return services;
49+
}
50+
51+
/// <summary>
52+
/// Adds a default CORS policy that allows any origin, method, and header.
53+
/// <br />
54+
/// <see href="https://learn.microsoft.com/en-us/aspnet/core/security/cors"/>
55+
/// </summary>
56+
/// <param name="services">The IServiceCollection instance.</param>
57+
/// <returns>The IServiceCollection for method chaining.</returns>
58+
public static IServiceCollection AddCorsDefaultPolicy(this IServiceCollection services)
59+
{
60+
services.AddCors(options =>
61+
{
62+
options.AddDefaultPolicy(corsBuilder =>
63+
{
64+
corsBuilder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader();
65+
});
66+
});
67+
68+
return services;
69+
}
70+
71+
/// <summary>
72+
/// Adds FluentValidation validators for Player models.
73+
/// <br />
74+
/// <see href="https://docs.fluentvalidation.net/en/latest/aspnet.html"/>
75+
/// </summary>
76+
/// <param name="services">The IServiceCollection instance.</param>
77+
/// <returns>The IServiceCollection for method chaining.</returns>
78+
public static IServiceCollection AddValidators(this IServiceCollection services)
79+
{
80+
services.AddValidatorsFromAssemblyContaining<PlayerRequestModelValidator>();
81+
return services;
82+
}
83+
84+
/// <summary>
85+
/// Sets up Swagger documentation generation and UI for the API.
86+
/// <br />
87+
/// <see href="https://learn.microsoft.com/en-us/aspnet/core/tutorials/getting-started-with-swashbuckle" />
88+
/// </summary>
89+
/// <param name="services">The IServiceCollection instance.</param>
90+
/// <param name="configuration">The application configuration.</param>
91+
/// <returns>The IServiceCollection for method chaining.</returns>
92+
public static IServiceCollection AddSwaggerConfiguration(
93+
this IServiceCollection services,
94+
IConfiguration configuration
95+
)
96+
{
97+
services.AddSwaggerGen(options =>
98+
{
99+
options.SwaggerDoc("v1", configuration.GetSection("OpenApiInfo").Get<OpenApiInfo>());
100+
options.IncludeXmlComments(SwaggerUtilities.ConfigureXmlCommentsFilePath());
101+
options.AddSecurityDefinition("Bearer", SwaggerUtilities.ConfigureSecurityDefinition());
102+
options.OperationFilter<AuthorizeCheckOperationFilter>();
103+
});
104+
105+
return services;
106+
}
107+
108+
/// <summary>
109+
/// Registers the PlayerService with the DI container.
110+
/// <br />
111+
/// <see href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection"/>
112+
/// </summary>
113+
/// <param name="services">The IServiceCollection instance.</param>
114+
/// <returns>The IServiceCollection for method chaining.</returns>
115+
public static IServiceCollection RegisterPlayerService(this IServiceCollection services)
116+
{
117+
services.AddScoped<IPlayerService, PlayerService>();
118+
return services;
119+
}
120+
121+
/// <summary>
122+
/// Adds AutoMapper configuration for Player mappings.
123+
/// <br />
124+
/// <see href="https://docs.automapper.io/en/latest/Dependency-injection.html#asp-net-core"/>
125+
/// </summary>
126+
/// <param name="services">The IServiceCollection instance.</param>
127+
/// <returns>The IServiceCollection for method chaining.</returns>
128+
public static IServiceCollection AddMappings(this IServiceCollection services)
129+
{
130+
services.AddAutoMapper(typeof(PlayerMappingProfile));
131+
return services;
132+
}
133+
134+
/// <summary>
135+
/// Registers the PlayerRepository service with the DI container.
136+
/// <br />
137+
/// <see href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection"/>
138+
/// </summary>
139+
/// <param name="services">The IServiceCollection instance.</param>
140+
/// <returns>The IServiceCollection for method chaining.</returns>
141+
public static IServiceCollection RegisterPlayerRepository(this IServiceCollection services)
142+
{
143+
services.AddScoped<IPlayerRepository, PlayerRepository>();
144+
return services;
145+
}
146+
}
Lines changed: 29 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,58 @@
1-
using Dotnet.Samples.AspNetCore.WebApi.Configurations;
2-
using Dotnet.Samples.AspNetCore.WebApi.Data;
3-
using Dotnet.Samples.AspNetCore.WebApi.Mappings;
4-
using Dotnet.Samples.AspNetCore.WebApi.Models;
5-
using Dotnet.Samples.AspNetCore.WebApi.Repositories;
6-
using Dotnet.Samples.AspNetCore.WebApi.Services;
7-
using Dotnet.Samples.AspNetCore.WebApi.Validators;
8-
using FluentValidation;
9-
using Microsoft.EntityFrameworkCore;
10-
using Microsoft.OpenApi.Models;
1+
using Dotnet.Samples.AspNetCore.WebApi.Extensions;
112
using Serilog;
123

13-
var builder = WebApplication.CreateBuilder(args);
14-
154
/* -----------------------------------------------------------------------------
16-
* Configuration
5+
* Web Application
6+
* https://learn.microsoft.com/en-us/aspnet/core/fundamentals/startup
177
* -------------------------------------------------------------------------- */
8+
9+
var builder = WebApplication.CreateBuilder(args);
10+
11+
/* Configurations ----------------------------------------------------------- */
12+
1813
builder
1914
.Configuration.SetBasePath(AppContext.BaseDirectory)
2015
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
2116
.AddEnvironmentVariables();
2217

23-
/* -----------------------------------------------------------------------------
24-
* Logging
25-
* -------------------------------------------------------------------------- */
26-
Log.Logger = new LoggerConfiguration().ReadFrom.Configuration(builder.Configuration).CreateLogger();
18+
/* Logging ------------------------------------------------------------------ */
2719

28-
/* Serilog ------------------------------------------------------------------ */
20+
Log.Logger = new LoggerConfiguration().ReadFrom.Configuration(builder.Configuration).CreateLogger();
2921
builder.Host.UseSerilog();
3022

31-
/* -----------------------------------------------------------------------------
32-
* Services
33-
* -------------------------------------------------------------------------- */
23+
/* Controllers -------------------------------------------------------------- */
24+
3425
builder.Services.AddControllers();
26+
builder.Services.AddCorsDefaultPolicy();
27+
builder.Services.AddHealthChecks();
28+
builder.Services.AddValidators();
3529

36-
/* Entity Framework Core ---------------------------------------------------- */
37-
builder.Services.AddDbContextPool<PlayerDbContext>(options =>
30+
if (builder.Environment.IsDevelopment())
3831
{
39-
var dataSource = Path.Combine(AppContext.BaseDirectory, "storage", "players-sqlite3.db");
40-
41-
options.UseSqlite($"Data Source={dataSource}");
32+
builder.Services.AddSwaggerConfiguration(builder.Configuration);
33+
}
4234

43-
if (builder.Environment.IsDevelopment())
44-
{
45-
options.EnableSensitiveDataLogging();
46-
options.LogTo(Log.Logger.Information, LogLevel.Information);
47-
}
48-
});
35+
/* Services ----------------------------------------------------------------- */
4936

50-
builder.Services.AddScoped<IPlayerRepository, PlayerRepository>();
51-
builder.Services.AddScoped<IPlayerService, PlayerService>();
37+
builder.Services.RegisterPlayerService();
5238
builder.Services.AddMemoryCache();
53-
builder.Services.AddHealthChecks();
39+
builder.Services.AddMappings();
5440

55-
/* AutoMapper --------------------------------------------------------------- */
56-
builder.Services.AddAutoMapper(typeof(PlayerMappingProfile));
41+
/* Repositories ------------------------------------------------------------- */
5742

58-
/* FluentValidation --------------------------------------------------------- */
59-
builder.Services.AddScoped<IValidator<PlayerRequestModel>, PlayerRequestModelValidator>();
43+
builder.Services.RegisterPlayerRepository();
6044

61-
if (builder.Environment.IsDevelopment())
62-
{
63-
/* Swagger UI ----------------------------------------------------------- */
64-
// https://learn.microsoft.com/en-us/aspnet/core/tutorials/getting-started-with-swashbuckle
65-
builder.Services.AddSwaggerGen(options =>
66-
{
67-
options.SwaggerDoc("v1", builder.Configuration.GetSection("SwaggerDoc").Get<OpenApiInfo>());
68-
options.IncludeXmlComments(SwaggerGenDefaults.ConfigureXmlCommentsFilePath());
69-
options.AddSecurityDefinition("Bearer", SwaggerGenDefaults.ConfigureSecurityDefinition());
70-
options.OperationFilter<AuthorizeCheckOperationFilter>();
71-
});
72-
}
45+
/* Data --------------------------------------------------------------------- */
46+
47+
builder.Services.AddDbContextPoolWithSqlite(builder.Environment);
7348

7449
var app = builder.Build();
7550

7651
/* -----------------------------------------------------------------------------
7752
* Middlewares
7853
* https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware
7954
* -------------------------------------------------------------------------- */
55+
8056
app.UseSerilogRequestLogging();
8157

8258
if (app.Environment.IsDevelopment())
@@ -85,16 +61,10 @@
8561
app.UseSwaggerUI();
8662
}
8763

88-
// https://learn.microsoft.com/en-us/aspnet/core/security/enforcing-ssl
8964
app.UseHttpsRedirection();
90-
91-
// https://learn.microsoft.com/en-us/aspnet/core/security/cors
9265
app.UseCors();
93-
94-
// https://learn.microsoft.com/en-us/aspnet/core/fundamentals/routing#endpoints
95-
app.MapControllers();
96-
97-
// https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/health-checks
66+
app.UseRateLimiter();
9867
app.MapHealthChecks("/health");
68+
app.MapControllers();
9969

10070
await app.RunAsync();

src/Dotnet.Samples.AspNetCore.WebApi/Configurations/SwaggerDocOptions.cs renamed to src/Dotnet.Samples.AspNetCore.WebApi/Utilities/SwaggerUtilities.cs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
using System.Reflection;
22
using Microsoft.OpenApi.Models;
33

4-
namespace Dotnet.Samples.AspNetCore.WebApi.Configurations;
4+
namespace Dotnet.Samples.AspNetCore.WebApi.Utilities;
55

66
/// <summary>
7-
/// Provides centralized configuration methods for Swagger/OpenAPI docs.
8-
/// Includes XML comments path resolution and security (JWT Bearer) setup.
7+
/// Utility methods for Swagger/OpenAPI configuration.
8+
/// Contains reusable helper methods that create OpenAPI objects.
99
/// </summary>
10-
public static class SwaggerGenDefaults
10+
public static class SwaggerUtilities
1111
{
1212
/// <summary>
1313
/// Resolves the path to the XML comments file generated from code
@@ -17,10 +17,16 @@ public static class SwaggerGenDefaults
1717
/// <returns>Full file path to the XML documentation file.</returns>
1818
public static string ConfigureXmlCommentsFilePath()
1919
{
20-
return Path.Combine(
20+
var path = Path.Combine(
2121
AppContext.BaseDirectory,
2222
$"{Assembly.GetExecutingAssembly().GetName().Name}.xml"
2323
);
24+
25+
if (!File.Exists(path))
26+
{
27+
throw new FileNotFoundException("XML comments file not found.", path);
28+
}
29+
return path;
2430
}
2531

2632
/// <summary>

src/Dotnet.Samples.AspNetCore.WebApi/appsettings.Development.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
],
2222
"Enrich": ["FromLogContext"]
2323
},
24-
"SwaggerDoc": {
24+
"OpenApiInfo": {
2525
"Version": "1.0.0",
2626
"Title": "Dotnet.Samples.AspNetCore.WebApi",
2727
"Description": "🧪 Proof of Concept for a Web API (Async) made with .NET 8 (LTS) and ASP.NET Core 8.0",

src/Dotnet.Samples.AspNetCore.WebApi/appsettings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
],
2222
"Enrich": ["FromLogContext"]
2323
},
24-
"SwaggerDoc": {
24+
"OpenApiInfo": {
2525
"Version": "1.0.0",
2626
"Title": "Dotnet.Samples.AspNetCore.WebApi",
2727
"Description": "🧪 Proof of Concept for a Web API (Async) made with .NET 8 (LTS) and ASP.NET Core 8.0",

src/Dotnet.Samples.AspNetCore.WebApi/packages.lock.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,16 @@
1717
"resolved": "12.0.0",
1818
"contentHash": "8NVLxtMUXynRHJIX3Hn1ACovaqZIJASufXIIFkD0EUbcd5PmMsL1xUD5h548gCezJ5BzlITaR9CAMrGe29aWpA=="
1919
},
20+
"FluentValidation.DependencyInjectionExtensions": {
21+
"type": "Direct",
22+
"requested": "[12.0.0, )",
23+
"resolved": "12.0.0",
24+
"contentHash": "B28fBRL1UjhGsBC8fwV6YBZosh+SiU1FxdD7l7p5dGPgRlVI7UnM+Lgzmg+unZtV1Zxzpaw96UY2MYfMaAd8cg==",
25+
"dependencies": {
26+
"FluentValidation": "12.0.0",
27+
"Microsoft.Extensions.Dependencyinjection.Abstractions": "2.1.0"
28+
}
29+
},
2030
"Microsoft.AspNetCore.OpenApi": {
2131
"type": "Direct",
2232
"requested": "[8.0.17, )",

test/Dotnet.Samples.AspNetCore.WebApi.Tests/packages.lock.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,15 @@
7171
"resolved": "12.0.0",
7272
"contentHash": "8NVLxtMUXynRHJIX3Hn1ACovaqZIJASufXIIFkD0EUbcd5PmMsL1xUD5h548gCezJ5BzlITaR9CAMrGe29aWpA=="
7373
},
74+
"FluentValidation.DependencyInjectionExtensions": {
75+
"type": "Transitive",
76+
"resolved": "12.0.0",
77+
"contentHash": "B28fBRL1UjhGsBC8fwV6YBZosh+SiU1FxdD7l7p5dGPgRlVI7UnM+Lgzmg+unZtV1Zxzpaw96UY2MYfMaAd8cg==",
78+
"dependencies": {
79+
"FluentValidation": "12.0.0",
80+
"Microsoft.Extensions.Dependencyinjection.Abstractions": "2.1.0"
81+
}
82+
},
7483
"Microsoft.AspNetCore.OpenApi": {
7584
"type": "Transitive",
7685
"resolved": "8.0.17",
@@ -591,6 +600,7 @@
591600
"dependencies": {
592601
"AutoMapper": "[14.0.0, )",
593602
"FluentValidation": "[12.0.0, )",
603+
"FluentValidation.DependencyInjectionExtensions": "[12.0.0, )",
594604
"Microsoft.AspNetCore.OpenApi": "[8.0.17, )",
595605
"Microsoft.EntityFrameworkCore.Sqlite": "[9.0.6, )",
596606
"Microsoft.Extensions.Configuration.Json": "[9.0.6, )",

0 commit comments

Comments
 (0)