Blazor: Bootstrap, AuthenticationStateProvider, and Repository Pattern
1. Introduction to AuthenticationStateProvider in Blazor
Authentication is an important part of modern web applications. but certain operations, should require authentication.
Blazor provides AuthenticationStateProvider, an abstraction that allows components to obtain information about the currently authenticated user. It works with the authentication infrastructure to provide a ClaimsPrincipal, which contains the user's identity and claims.
For example, imagine a gym application where a member logs in. The application needs to determine whether the member is authenticated before displaying subscription-related features.
What is AuthenticationStateProvider?
AuthenticationStateProvider is a Blazor class responsible for providing the current authentication state to components.
It helps components determine:
- Whether a user is authenticated.
- Which claims are available for the current user.
- Whether the UI should display authenticated or unauthenticated content.
It does not independently validate a JWT against the backend or secure an API endpoint. The backend must validate the access token and enforce authorization separately.
Example: Reading the authentication state
@page "/member-dashboard" @using Microsoft.AspNetCore.Components.Authorization @inject AuthenticationStateProvider AuthStateProvider <div class="container mt-4"> <div class="card shadow-sm"> <div class="card-header bg-primary text-white"> <h4>Gym Member Dashboard</h4> </div> <div class="card-body"> <h5>Welcome to Gym Management</h5> <p>@message</p> </div> </div> </div> @code { private string message = "Checking authentication..."; protected override async Task OnInitializedAsync() { var authenticationState = await AuthStateProvider.GetAuthenticationStateAsync(); var user = authenticationState.User; if (user.Identity?.IsAuthenticated == true) { message = $"Welcome, {user.Identity.Name}!"; } else { message = "Please log in to access your memberships."; } } } Explanation of the execution flow
Step 1: Inject AuthenticationStateProvider
@inject AuthenticationStateProvider AuthStateProvider This makes the authentication state provider available inside the component.
Step 2: Execute OnInitializedAsync
protected override async Task OnInitializedAsync() This lifecycle method runs when the component initializes. It is a suitable place to retrieve the initial authentication state.
Step 3: Retrieve the authentication state
var authenticationState = await AuthStateProvider.GetAuthenticationStateAsync(); This obtains the current AuthenticationState.
Step 4: Access the current user
var user = authenticationState.User; The User property is a ClaimsPrincipal containing the identity and claims provided by the authentication system.
Step 5: Check authentication
user.Identity?.IsAuthenticated == true If the condition is true, the component displays a welcome message. Otherwise, it asks the visitor to log in.
This example demonstrates how Blazor components can respond to authentication state without implementing the entire authentication system inside each component.
Understanding AuthorizeVi
AuthorizeView is a Blazor component that displays different content according to the current authentication and authorization state.
-
<Authorized>contains content shown when the user satisfies the authorization requirements. -
<NotAuthorized>contains content shown when the user does not satisfy those requirements.
In this example, Bootstrap provides the interface while Blazor's authorization infrastructure determines which content to display.
Remember that hiding a button or page section is not sufficient to protect sensitive operations. The corresponding backend API must also use authentication and authorization.
3. Understanding the Gym Membership Subscription System
The Gym Membership Management System allows members to select a gym, choose an available membership plan, specify a start date, select a payment method, and accept the terms and conditions.
When the member submits the subscription form, the backend must validate the request before saving the subscription.
Blazor WebAssembly Frontend | v ASP.NET Core Web API | v Subscription Controller | v CreateSubscriptionCommand | v MediatR Command Handler | v Subscription Service | v ISubscriptionRepository | v SubscriptionRepository | v Entity Framework Core | v SQL Server Database Responsibility of each layer
Controller: Receives the HTTP request, identifies the authenticated member, and sends the command through MediatR.
Command: Carries the data required to create the subscription.
Handler: Receives the command and invokes the service.
Service: Validates the subscription request, applies business rules, calculates derived values, and decides whether the operation is allowed.
Repository interface: Defines the database operations required by the application.
Repository implementation: Uses Entity Framework Core to retrieve and save database records.
This separation makes the application easier to maintain, test, and extend.
5. CreateSubscriptionCommand.cs
The command contains the properties required to process a subscription request.
public class CreateSubscriptionCommand : IRequest<ResponseMsg> { // Set by the controller from authenticated user claims. public Guid UserId { get; set; } [Range(1, int.MaxValue, ErrorMessage = "Please select a gym.")] public int GymId { get; set; } [Range(1, int.MaxValue, ErrorMessage = "Please select a membership plan.")] public int MembershipPlanId { get; set; } [Required(ErrorMessage = "Start date is required.")] public DateTime StartDate { get; set; } [Required(ErrorMessage = "Please select a payment method.")] public string PaymentMethod { get; set; } = string.Empty; public bool TermsAccepted { get; set; } } Explanation
UserId identifies the member creating the subscription. The backend should populate it from the authenticated user's claims rather than trusting a value supplied by the frontend.
GymId identifies the selected gym.
MembershipPlanId identifies the selected membership plan.
StartDate specifies when the membership should begin.
PaymentMethod records the selected payment method.
TermsAccepted records whether the member accepted the terms and conditions.
The [Range] attributes specify valid positive IDs. The [Required] attributes declare required fields, but additional checks are still necessary for non-nullable value types such as DateTime and for business-specific conditions.
For example, the service must reject a start date in the past and reject TermsAccepted == false.
6. ISubscriptionRepository.cs
The repository interface defines the operations needed by the subscription service.
using GymManagement.Domain.Entities; namespace GymManagement.Domain.RepositoryContracts; public interface ISubscriptionRepository { Task<GymCenter?> GetGymByIdAsync( int gymId, CancellationToken cancellationToken); Task<MembershipPlan?> GetMembershipPlanByIdAsync( int planId, CancellationToken cancellationToken); Task<bool> HasActiveMembershipInGymAsync( Guid userId, int gymId, CancellationToken cancellationToken); Task<int> CountActiveMembershipsAsync( Guid userId, CancellationToken cancellationToken); Task AddSubscriptionAsync( Subscription subscription, CancellationToken cancellationToken); } Explanation of the repository methods
GetGymByIdAsync
Retrieves the gym associated with the supplied ID. The implementation excludes soft-deleted gyms.
GetMembershipPlanByIdAsync
Retrieves the selected membership plan so the application can validate its status, gym association, price, and duration.
HasActiveMembershipInGymAsync
Checks whether the member already has an unexpired active membership in the specified gym.
CountActiveMembershipsAsync
Counts the member's unexpired active memberships across all gyms.
AddSubscriptionAsync
Adds the new subscription to the database and saves the changes.
The interface does not contain the implementation details. It specifies what the repository must be able to do.
7. SubscriptionRepository.cs
The repository implementation uses AppDbContext and Entity Framework Core to perform database operations.
using GymManagement.Domain.Entities; using GymManagement.Domain.RepositoryContracts; using GymManagement.Infrastructure.Context; using Microsoft.EntityFrameworkCore; namespace GymManagement.Infrastructure.Repository; public class SubscriptionRepository : ISubscriptionRepository { private readonly AppDbContext _context; public SubscriptionRepository(AppDbContext context) { _context = context; } public async Task<GymCenter?> GetGymByIdAsync( int gymId, CancellationToken cancellationToken) { return await _context.GymCenters .AsNoTracking() .FirstOrDefaultAsync( g => g.Id == gymId && !g.IsDeleted, cancellationToken); } public async Task<MembershipPlan?> GetMembershipPlanByIdAsync( int planId, CancellationToken cancellationToken) { return await _context.MembershipPlans .AsNoTracking() .FirstOrDefaultAsync( p => p.Id == planId, cancellationToken); } public async Task<bool> HasActiveMembershipInGymAsync( Guid userId, int gymId, CancellationToken cancellationToken) { var today = DateTime.UtcNow.Date; return await _context.Subscriptions .AnyAsync(s => s.UserId == userId && s.MembershipPlan != null && s.MembershipPlan.GymId == gymId && s.Status == "Active" && s.EndDate >= today, cancellationToken); } public async Task<int> CountActiveMembershipsAsync( Guid userId, CancellationToken cancellationToken) { var today = DateTime.UtcNow.Date; return await _context.Subscriptions .CountAsync(s => s.UserId == userId && s.Status == "Active" && s.EndDate >= today, cancellationToken); } public async Task AddSubscriptionAsync( Subscription subscription, CancellationToken cancellationToken) { await _context.Subscriptions.AddAsync( subscription, cancellationToken); await _context.SaveChangesAsync(cancellationToken); } } Understanding the implementation
The constructor receives AppDbContext through dependency injection.
private readonly AppDbContext _context; The _context field provides access to the database sets configured in the context.
Why use AsNoTracking?
.AsNoTracking() This tells Entity Framework Core not to track the retrieved entities for changes. It is useful for read-only queries, such as retrieving a gym or a membership plan for validation.
Why use FirstOrDefaultAsync?
.FirstOrDefaultAsync( g => g.Id == gymId && !g.IsDeleted, cancellationToken); This searches for a gym matching the supplied ID that has not been soft-deleted. If no matching gym exists, the method returns null.
Why use AnyAsync?
.AnyAsync(s => s.UserId == userId && s.MembershipPlan != null && s.MembershipPlan.GymId == gymId && s.Status == "Active" && s.EndDate >= today, cancellationToken); This checks whether a matching subscription exists for the specified member and gym.
The result is a Boolean:
-
truemeans a matching active subscription exists. -
falsemeans no matching active subscription exists.
Why use CountAsync?
CountAsync() counts subscriptions satisfying the specified conditions. The service can use the count to enforce the maximum of three active memberships.
Why use SaveChangesAsync?
await _context.SaveChangesAsync(cancellationToken); This persists the new subscription to the database.
The CancellationToken allows the operation to be cancelled when the calling request is cancelled.
8. Connecting authentication to subscription creation
The frontend and backend have different authentication responsibilities.
In Blazor WebAssembly, a custom AuthenticationStateProvider can read the stored access token and construct the current authentication state. The frontend's HTTP client can attach that token when calling a protected endpoint.
The backend then validates the token and identifies the authenticated member.
For example, a controller can obtain the member's GUID from the claims:
var userIdValue = User.FindFirstValue( ClaimTypes.NameIdentifier); if (!Guid.TryParse(userIdValue, out var userId)) { return Unauthorized(); } This example assumes the authenticated token contains the user ID in the claim mapped to ClaimTypes.NameIdentifier.
The controller should assign that verified ID to CreateSubscriptionCommand.UserId before sending the command to MediatR.
The frontend should not be trusted to supply the identity of the member who owns the subscription.
9. Important business-rule considerations
The repository methods provide the information required to check membership limits, but the service must decide whether the new subscription is allowed.
The service should also calculate the expiry date:
endDate = startDate.Date.AddMonths(plan.DurationInMonths); It should obtain the plan price from the database rather than accepting a price supplied by the browser.
Similarly, subscription timestamps should be assigned by the backend using UTC time.
One business-rule distinction is important: a future-dated subscription is not necessarily active yet. If future subscriptions remain Pending, the membership-count queries must follow the application's intended policy about whether pending memberships reserve a slot. A scheduled process or another reliable mechanism is also needed to transition eligible pending memberships to active and expired memberships to expired.
Finally, membership-count checks followed by a database insert can race if multiple requests arrive simultaneously. If the membership limits must be guaranteed under concurrent requests, the application needs an appropriate transaction or database-level concurrency strategy.
10. Conclusion
Blazor's AuthenticationStateProvider helps components respond to the current authentication state, while Bootstrap provides the interface used to present that state to the member.
In the Gym Membership Management System, the command carries the subscription details, the repository interface defines database operations, and the repository implementation uses Entity Framework Core to retrieve and save records.
The overall architecture separates presentation, authentication, application logic, and persistence. This allows each part of the system to be developed and maintained independently.
The central lesson is that authentication state in the frontend is useful for the user experience, but secure subscription creation depends on backend token validation, trusted user identity, service-layer business rules, and correct database operations.