What does AddScoped() mean?
A scoped service is normally created once per dependency injection scope. In a typical ASP.NET Core Web API request, this means one instance per request scope.
This is commonly used for application services and repositories that depend on a scoped AppDbContext.
Why use interfaces?
Interfaces separate what a service does from how it does it.
This supports maintainability and the separation of concerns used in layered architectures.
// Add the using statements from your own project here.
// Example: your DbContext, ApplicationUser, exception handler,
// JWT options, and any application-specific services.
using FluentValidation;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Authorization;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi;
using System.Security.Claims;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
// 1. Database configuration
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(
builder.Configuration.GetConnectionString("DefaultConnection")));
// 2. Swagger configuration
builder.Services.AddSwaggerGen(options =>
{
options.AddSecurityDefinition("bearer", new OpenApiSecurityScheme
{
Type = SecuritySchemeType.Http,
Scheme = "bearer",
BearerFormat = "JWT",
Description = "JWT Authorization header using the bearer scheme"
});
options.AddSecurityRequirement(document =>
new OpenApiSecurityRequirement
{
[new OpenApiSecuritySchemeReference("bearer", document)] = []
});
});
// 3. Exception handling
builder.Services.AddExceptionHandler<AppExceptionHandler>();
builder.Services.AddProblemDetails();
// 4. HTTP context access
builder.Services.AddHttpContextAccessor();
// 5. JWT configuration
// Register your JwtOptions and token service here if your project uses them.
// 6. MediatR and FluentValidation
// Register your application's handlers, pipeline behaviors,
// and validators here if your project uses them.
// 7. Controllers and global authorization
builder.Services.AddControllers(options =>
{
options.Filters.Add(
new ProducesAttribute("application/json"));
options.Filters.Add(
new ConsumesAttribute("application/json"));
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
});
// 8. ASP.NET Core Identity
builder.Services
.AddIdentity<ApplicationUser, IdentityRole<Guid>>()
.AddEntityFrameworkStores<AppDbContext>()
.AddDefaultTokenProviders();
// 9. JWT authentication
builder.Services
.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme =
JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme =
JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme =
JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.SaveToken = true;
options.TokenValidationParameters =
new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateIssuerSigningKey = true,
ValidAudience =
builder.Configuration["JWT:ValidAudience"],
ValidIssuer =
builder.Configuration["JWT:ValidIssuer"],
ClockSkew = TimeSpan.Zero,
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(
builder.Configuration["JWT:Secret"]
?? throw new InvalidOperationException(
"JWT secret is missing.")))
};
options.Events = new JwtBearerEvents
{
OnTokenValidated = async context =>
{
var userManager =
context.HttpContext.RequestServices
.GetRequiredService<UserManager<ApplicationUser>>();
var userId = context.Principal?
.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var tokenVersion = context.Principal?
.FindFirst("TokenVersion")?.Value;
if (userId == null || tokenVersion == null)
{
context.Fail("Invalid token.");
return;
}
var user = await userManager.FindByIdAsync(userId);
if (user == null ||
user.TokenVersion.ToString() != tokenVersion)
{
context.Fail("Token has been revoked.");
}
},
OnChallenge = async context =>
{
context.HandleResponse();
context.Response.StatusCode =
StatusCodes.Status401Unauthorized;
context.Response.ContentType = "application/json";
await context.Response.WriteAsJsonAsync(new
{
StatusCode = 401,
Message = "Please log in with a valid token."
});
}
};
});
// 10. Build the application
var app = builder.Build();
// 11. Middleware pipeline
app.UseExceptionHandler();
app.UseSwagger();
app.UseSwaggerUI(options =>
{
options.SwaggerEndpoint(
"/swagger/v1/swagger.json",
"Web API");
options.RoutePrefix = string.Empty;
options.DocumentTitle = "Web API";
});
app.UseAuthentication();
app.UseAuthorization();
// 12. Map controller endpoints
app.MapControllers();
// Add your own database seeding method here if required.
// 13. Start the application
app.Run();
2. What Is the Purpose of Program.cs?
In an ASP.NET Core Web API, Program.cs is the application's entry point and configuration file.
It tells ASP.NET Core:
- Which database to use.
- Which services and repositories to register.
- How to validate incoming requests.
- How to authenticate users using JWT.
- How to authorize access to controllers.
- How to display API documentation through Swagger.
- How to handle exceptions.
- How to initialize database seed data.
- How to start accepting HTTP requests.
Your project uses a layered architecture:
- API layer: Receives HTTP requests and configures the application.
- Application layer: Contains business operations, services, CQRS handlers, and validation.
- Domain layer: Contains core entities and repository contracts.
- Infrastructure layer: Contains database access, repository implementations, and seeding.
The purpose of Program.cs is to connect these pieces and configure the
What is args?
args contains command-line arguments passed to the application.
Why do we use it?
Before the application starts handling requests, we need to register the services and configure how the application behaves.
For example, your code uses:
builder.Services.AddDbContext<AppDbContext>(...);
to register the database context, and:
builder.Configuration.GetConnectionString("DefaultConnection")
to retrieve the database connection string.
In simple words: The builder is where you prepare and configure your application before building it.
5. Configuring Entity Framework Core and SQL Server
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(
builder.Configuration.GetConnectionString("DefaultConnection")));
This registers your Entity Framework Core database context with dependency injection.
A. AddDbContext<AppDbContext>()
builder.Services.AddDbContext<AppDbContext>(...)
AppDbContext represents the application's session with the database.
It typically contains:
-
DbSetproperties representing database tables. - Entity configuration.
- Database relationship configuration.
- Database operations performed through Entity Framework Core.
B. options.UseSqlServer()
options.UseSqlServer(...)
This configures Entity Framework Core to use SQL Server as the database provider.
Other providers exist, but this application uses SQL Server.
C. GetConnectionString("DefaultConnection")
builder.Configuration.GetConnectionString("DefaultConnection")
This retrieves the connection string named DefaultConnection from the application's configuration.
For example, your appsettings.json might contain:
{
"ConnectionStrings": {
"DefaultConnection": "YOUR_SQL_SERVER_CONNECTION_STRING"
}
}
The actual connection string should be configured for your environment rather than hard-coded into Program.cs.
Why do we need this?
When a controller, handler, or repository needs to access the database, dependency injection can provide an AppDbContext configured for SQL Server.
Execution flow:
- The application reads the connection string.
- Entity Framework Core is configured to use SQL Server.
-
AppDbContextis registered with dependency injection. - Components that request the context can use it for database operations.
6. Configuring Swagger and JWT Authorization
builder.Services.AddSwaggerGen(options =>
{
options.AddSecurityDefinition("bearer", new OpenApiSecurityScheme
{
Type = SecuritySchemeType.Http,
Scheme = "bearer",
BearerFormat = "JWT",
Description = "JWR Authorization header using the bearer scheme"
});
options.AddSecurityRequirement(document => new OpenApiSecurityRequirement
{
[new OpenApiSecuritySchemeReference("bearer", document)] = []
});
});
What is Swagger?
Swagger UI provides a browser-based interface for exploring and testing your API endpoints.
A. AddSwaggerGen()
builder.Services.AddSwaggerGen(...)
B. AddHttpContextAccessor()
builder.Services.AddHttpContextAccessor();
This registers IHttpContextAccessor, which can provide access to the current HttpContext where available.
The HttpContext represents the current HTTP request and response and can provide access to:
- Request headers.
- Response status.
- Request services.
- The authenticated user's
ClaimsPrincipal.
This is useful if CurrentUser obtains user information from the current HTTP context.
Important: HttpContext is request-specific. Do not store it or request-specific user information in a long-lived object for use across unrelated requests.
For example:
Registration command
|
v
MediatR pipeline
|
v
ValidationBehavior
|
+---- Invalid ----> Validation exception
|
+---- Valid ------> RegisterCommandHandler
---
## 13. Registering FluentValidation Validators
csharp
builder.Services.AddValidatorsFromAssembly(
typeof(RegisterCommandValidator).Assembly);
This tells FluentValidation to discover and register validators in the assembly containing `RegisterCommandValidator`.
A validator might check that:
- An email address is present and valid.
- A password meets requirements.
- A required field is not empty.
For example, a registration validator might contain rules similar to:
csharp
RuleFor(x => x.Email)
.NotEmpty()
.EmailAddress();
The example is illustrative; your actual rules depend on your validator.
### Why register validators?
Your `ValidationBehavior<,>` needs to obtain the relevant validators from dependency injection.
The typical flow is:
1. A command is sent through MediatR.
2. The validation behavior finds validators for that command.
3. The validators evaluate the command.
4. If validation succeeds, the handler executes.
5. If validation fails, the behavior reports the errors according to its implementation.
Registering validators alone does not automatically execute them. The pipeline behavior is what connects validation to MediatR request processing.
---
## 14. Configuring Controllers and Global MVC Filters
csharp
builder.Services.AddControllers(options =>
{
options.Filters.Add(new ProducesAttribute("application/json"));
options.Filters.Add(new ConsumesAttribute("application/json"));
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
});
This registers controller support and configures global MVC filters.
### A. `AddControllers()`
csharp
builder.Services.AddControllers(...)
This enables controller-based API endpoints.
For example, a controller might define:
markdown
The controller can receive HTTP requests and call application services or MediatR handlers.
B. ProducesAttribute
options.Filters.Add(
new ProducesAttribute("application/json"));
This declares that controller actions produce JSON responses by default.
It helps communicate the expected response format to clients and API documentation tools.
This creates an authorization policy that requires the user to be authenticated.
### E. Adding the authorization filter
csharp
options.Filters.Add(new AuthorizeFilter(policy));
This applies the policy globally to MVC controller actions.
**What does this mean in practice?**
Controller actions generally require an authenticated user unless you explicitly allow anonymous access.
For example, registration and login endpoints usually need to be accessible before the user has logged in.
You can mark such actions with:
csharp
[AllowAnonymous]
This is important because otherwise the global authorization filter can prevent an unauthenticated user from reaching those endpoints.
### Authentication versus authorization
- **Authentication:** Determines who the user is.
- **Authorization:** Determines whether the user is permitted to perform an operation.
Your global policy requires authentication, but it does not by itself restrict actions to particular roles.
---
## 15. Configuring ASP.NET Core Identity
csharp
builder.Services.AddIdentity>()
.AddRoles>()
.AddEntityFrameworkStores()
.AddDefaultTokenProviders();
This configures ASP.NET Core Identity for managing application users and roles.
### A. `AddIdentity<ApplicationUser, IdentityRole<Guid>>()`
This configures Identity to use your custom `ApplicationUser` type and a role type whose primary key is `Guid`.
Your custom user class can contain standard Identity fields as well as additional application-specific properties.
### B. `AddRoles<IdentityRole<Guid>>()`
This explicitly enables role-related Identity services.
`AddIdentity` already configures role support for the specified role type, so this additional call is generally redundant. You can usually remove it unless you have a specific reason to retain it.
### C. `AddEntityFrameworkStores<AppDbContext>()`
This configures Identity to persist its user, role, and related Identity data using Entity Framework Core and your `AppDbContext`.
### D. `AddDefaultTokenProviders()`
This registers the default Identity token providers, which can support features such as password-reset and email-confirmation tokens.
These Identity tokens are different from JWT bearer access tokens.
### Why do we need Identity?
Identity provides established user and role management features instead of requiring you to implement every user-management operation manually.
---
## 16. Configuring JWT Authentication
csharp
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
// JWT configuration
});
This configures JWT bearer authentication.
### What is JWT authentication?
JWT stands for JSON Web Token.
A client typically receives an access token after successful login and sends it with requests to protected API endpoints.
For example:
http
Authorization: Bearer eyJhbGciOi...
The backend validates the token before allowing the request to proceed.
### A. `AddAuthentication()`
This registers authentication services and configures the default authentication schemes.
### B. `DefaultAuthenticateScheme`
csharp
options.DefaultAuthenticateScheme =
JwtBearerDefaults.AuthenticationScheme;
This tells ASP.NET Core to use the JWT bearer handler to authenticate requests by default.
### C. `DefaultChallengeScheme`
csharp
options.DefaultChallengeScheme =
JwtBearerDefaults.AuthenticationScheme;
This selects the JWT bearer handler when an unauthenticated request needs an authentication challenge.
### D. `DefaultScheme`
csharp
options.DefaultScheme =
JwtBearerDefaults.AuthenticationScheme;
This sets the default scheme for operations that use the default scheme.
### Why configure all three?
It makes JWT bearer authentication the default for the application's authentication operations.
**Important:** Configuring Identity and JWT authentication together does not automatically mean every Identity cookie or endpoint behaves like a JWT endpoint. Your authentication scheme settings determine which handler is used for authentication and challenges.
---
## 17. Configuring the JWT Bearer Handler
csharp
.AddJwtBearer(options =>
{
options.SaveToken = true;
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidAudience = builder.Configuration["JWT:ValidAudience"],
ValidIssuer = builder.Configuration["JWT:ValidIssuer"],
ClockSkew = TimeSpan.Zero,
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(
builder.Configuration["JWT:Secret"]))
};
// Events are configured here.
});
This configures how incoming JWTs are processed and validated.
### A. `SaveToken`
csharp
options.SaveToken = true;
This tells the handler to save the token in the authentication properties after successful authentication.
It does not mean the token is automatically stored in browser local storage.
### B. `RequireHttpsMetadata`
csharp
options.RequireHttpsMetadata = false;
This setting affects HTTPS requirements when retrieving metadata from an identity provider. It does not turn off HTTPS for your API and does not make an HTTP API secure.
For a typical self-contained JWT validation setup, no metadata endpoint is configured by this line itself. In production, use HTTPS and review this setting in the context of your authentication design.
### C. `TokenValidationParameters`
csharp
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidAudience = builder.Configuration["JWT:ValidAudience"],
ValidIssuer = builder.Configuration["JWT:ValidIssuer"],
ClockSkew = TimeSpan.Zero,
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(
builder.Configuration["JWT:Secret"]))
};
This configures the checks used to validate incoming JWTs.
#### `ValidateIssuer = true`
The token issuer must be validated.
The issuer identifies the authority that created the token.
#### `ValidateAudience = true`
The token audience must be validated.
The audience identifies the intended recipient of the token, such as your API.
#### `ValidAudience`
csharp
ValidAudience = builder.Configuration["JWT:ValidAudience"]
This specifies the expected audience from configuration.
#### `ValidIssuer`
csharp
ValidIssuer = builder.Configuration["JWT:ValidIssuer"]
This specifies the expected issuer.
The values used to generate tokens must match the values expected during validation.
#### `ClockSkew = TimeSpan.Zero`
csharp
ClockSkew = TimeSpan.Zero
This removes the default allowance for small clock differences when checking time-related claims such as expiration.
As a result, expiration checks are applied without that extra tolerance.
#### `IssuerSigningKey`
csharp
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(
builder.Configuration["JWT:Secret"]))
This constructs the symmetric signing key used to validate the token's signature.
- `Encoding.UTF8.GetBytes()` converts the configured secret string into
markdown
The actual value and claim mapping depend on how your token is created and validated.
The ?. operators safely handle missing intermediate objects.
Step 3: Extract TokenVersion
var tokenVersion = context.Principal?
.FindFirst("TokenVersion")?.Value;
This reads a custom claim named TokenVersion.
Top comments (1)
Maintaining a clean, service-oriented structure within the Program.cs entry point is a solid operational foundation for enterprise .NET applications. Your architectural breakdown perfectly aligns with the principles of Clean Architecture and explicit dependency separation.
The configuration layout demonstrates several production-grade practices: