A comprehensive development guide for building modern web applications using Blazor, C#, ASP.NET Core, and Entity Framework Core with Visual Studio Enterprise integration
This agents.md example showcases industry best practices for AI agent instruction. The agents.md example provides a comprehensive template that you can adapt for your specific project requirements.
Every element in this agents.md example has been carefully designed to optimize OpenAI Codex performance and ensure consistent AI agent behavior across your development workflow.
To use this agents.md example in your project, download the template and customize it according to your specific needs. This agents.md example serves as a solid foundation for your AI agent configuration.
Study the structure and conventions used in this agents.md example to understand how successful projects implement AI agent instruction for optimal results.
This comprehensive guide outlines best practices for developing modern web applications using Blazor, C#, ASP.NET Core, and Entity Framework Core. The guide emphasizes efficient development workflows using Visual Studio Enterprise for running, debugging, and testing, while leveraging Cursor AI for code editing and AI-powered suggestions. It focuses on component-based UI development, proper separation of concerns, and following .NET conventions.
# 1. Create new Blazor project
dotnet new blazorserver -n MyBlazorApp
# or for WebAssembly
dotnet new blazorwasm -n MyBlazorApp
# 2. Navigate to project directory
cd MyBlazorApp
# 3. Add Entity Framework Core
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools
dotnet add package Microsoft.EntityFrameworkCore.Design
# 4. Add additional packages
dotnet add package FluentValidation.AspNetCore
dotnet add package Serilog.AspNetCore
dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis
# 5. Restore packages
dotnet restore
# 6. Open in Visual Studio Enterprise
start MyBlazorApp.sln
BlazorApp/
├── Program.cs # Application entry point
├── appsettings.json # Configuration
├── Components/ # Blazor components
│ ├── Layout/ # Layout components
│ │ ├── MainLayout.razor
│ │ ├── NavMenu.razor
│ │ └── MainLayout.razor.css
│ ├── Pages/ # Page components
│ │ ├── Home.razor
│ │ ├── Counter.razor
│ │ └── Weather.razor
│ └── Shared/ # Shared components
│ ├── ErrorBoundary.razor
│ └── LoadingSpinner.razor
├── Data/ # Data layer
│ ├── ApplicationDbContext.cs
│ ├── Models/
│ └── Repositories/
├── Services/ # Business logic services
│ ├── IUserService.cs
│ └── UserService.cs
├── Models/ # Domain models
├── ViewModels/ # View models for complex components
├── Validators/ # FluentValidation validators
├── wwwroot/ # Static files
│ ├── css/
│ ├── js/
│ └── images/
└── Tests/ # Test projects
├── UnitTests/
└── IntegrationTests/
// Example: Proper naming conventions
public interface IUserService
{
Task<User> GetUserByIdAsync(int userId);
}
public class UserService : IUserService
{
private readonly ApplicationDbContext _context;
private readonly ILogger<UserService> _logger;
public UserService(ApplicationDbContext context, ILogger<UserService> logger)
{
_context = context;
_logger = logger;
}
public async Task<User> GetUserByIdAsync(int userId)
{
return await _context.Users.FindAsync(userId);
}
}
@page "/user-profile/{UserId:int}"
@using MyBlazorApp.Services
@inject IUserService UserService
@inject IJSRuntime JSRuntime
<PageTitle>User Profile</PageTitle>
@if (isLoading)
{
<LoadingSpinner />
}
else if (user != null)
{
<div class="user-profile">
<h2>@user.Name</h2>
<p>Email: @user.Email</p>
<EditForm Model="user" OnValidSubmit="HandleValidSubmit">
<DataAnnotationsValidator />
<ValidationSummary />
<div class="form-group">
<label for="name">Name:</label>
<InputText id="name" @bind-Value="user.Name" class="form-control" />
<ValidationMessage For="@(() => user.Name)" />
</div>
<div class="form-group">
<label for="email">Email:</label>
<InputText id="email" @bind-Value="user.Email" class="form-control" />
<ValidationMessage For="@(() => user.Email)" />
</div>
<button type="submit" class="btn btn-primary" disabled="@isSubmitting">
@if (isSubmitting)
{
<span class="spinner-border spinner-border-sm" role="status"></span>
<span>Updating...</span>
}
else
{
<span>Update Profile</span>
}
</button>
</EditForm>
</div>
}
else
{
<div class="alert alert-danger">User not found.</div>
}
@code {
[Parameter] public int UserId { get; set; }
private User? user;
private bool isLoading = true;
private bool isSubmitting = false;
protected override async Task OnInitializedAsync()
{
await LoadUserAsync();
}
protected override async Task OnParametersSetAsync()
{
if (UserId != user?.Id)
{
await LoadUserAsync();
}
}
private async Task LoadUserAsync()
{
try
{
isLoading = true;
user = await UserService.GetUserByIdAsync(UserId);
}
catch (Exception ex)
{
// Handle error appropriately
await JSRuntime.InvokeVoidAsync("console.error", $"Error loading user: {ex.Message}");
}
finally
{
isLoading = false;
}
}
private async Task HandleValidSubmit()
{
try
{
isSubmitting = true;
await UserService.UpdateUserAsync(user!);
await JSRuntime.InvokeVoidAsync("alert", "Profile updated successfully!");
}
catch (Exception ex)
{
await JSRuntime.InvokeVoidAsync("alert", $"Error updating profile: {ex.Message}");
}
finally
{
isSubmitting = false;
}
}
}
// Use ShouldRender() to control component re-rendering
public partial class OptimizedComponent : ComponentBase
{
[Parameter] public string Title { get; set; } = string.Empty;
[Parameter] public List<Item> Items { get; set; } = new();
private string previousTitle = string.Empty;
private int previousItemsCount = 0;
protected override bool ShouldRender()
{
// Only re-render if meaningful changes occurred
bool shouldRender = Title != previousTitle || Items.Count != previousItemsCount;
if (shouldRender)
{
previousTitle = Title;
previousItemsCount = Items.Count;
}
return shouldRender;
}
protected override void OnParametersSet()
{
// Minimize work in OnParametersSet
base.OnParametersSet();
}
}
// Use EventCallback for efficient event handling
@code {
[Parameter] public EventCallback<string> OnItemSelected { get; set; }
private async Task HandleItemClick(string itemId)
{
// Pass only minimal data when triggering events
await OnItemSelected.InvokeAsync(itemId);
}
}
// Data/ApplicationDbContext.cs
using Microsoft.EntityFrameworkCore;
using MyBlazorApp.Models;
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
{
}
public DbSet<User> Users { get; set; }
public DbSet<Product> Products { get; set; }
public DbSet<Order> Orders { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// Configure User entity
modelBuilder.Entity<User>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Name).IsRequired().HasMaxLength(100);
entity.Property(e => e.Email).IsRequired().HasMaxLength(255);
entity.HasIndex(e => e.Email).IsUnique();
});
// Configure relationships
modelBuilder.Entity<Order>(entity =>
{
entity.HasOne(o => o.User)
.WithMany(u => u.Orders)
.HasForeignKey(o => o.UserId)
.OnDelete(DeleteBehavior.Cascade);
});
// Seed data
modelBuilder.Entity<User>().HasData(
new User { Id = 1, Name = "Admin User", Email = "[email protected]" }
);
}
}
// Data/Repositories/IUserRepository.cs
public interface IUserRepository
{
Task<User?> GetByIdAsync(int id);
Task<User?> GetByEmailAsync(string email);
Task<IEnumerable<User>> GetAllAsync();
Task<User> CreateAsync(User user);
Task<User> UpdateAsync(User user);
Task DeleteAsync(int id);
}
// Data/Repositories/UserRepository.cs
public class UserRepository : IUserRepository
{
private readonly ApplicationDbContext _context;
public UserRepository(ApplicationDbContext context)
{
_context = context;
}
public async Task<User?> GetByIdAsync(int id)
{
return await _context.Users
.Include(u => u.Orders)
.FirstOrDefaultAsync(u => u.Id == id);
}
public async Task<User?> GetByEmailAsync(string email)
{
return await _context.Users
.FirstOrDefaultAsync(u => u.Email == email);
}
public async Task<IEnumerable<User>> GetAllAsync()
{
return await _context.Users
.OrderBy(u => u.Name)
.ToListAsync();
}
public async Task<User> CreateAsync(User user)
{
_context.Users.Add(user);
await _context.SaveChangesAsync();
return user;
}
public async Task<User> UpdateAsync(User user)
{
_context.Users.Update(user);
await _context.SaveChangesAsync();
return user;
}
public async Task DeleteAsync(int id)
{
var user = await _context.Users.FindAsync(id);
if (user != null)
{
_context.Users.Remove(user);
await _context.SaveChangesAsync();
}
}
}
// Validators/UserValidator.cs
using FluentValidation;
using MyBlazorApp.Models;
public class UserValidator : AbstractValidator<User>
{
public UserValidator()
{
RuleFor(x => x.Name)
.NotEmpty().WithMessage("Name is required")
.Length(2, 100).WithMessage("Name must be between 2 and 100 characters");
RuleFor(x => x.Email)
.NotEmpty().WithMessage("Email is required")
.EmailAddress().WithMessage("Invalid email format")
.MaximumLength(255).WithMessage("Email must not exceed 255 characters");
}
}
// Register in Program.cs
builder.Services.AddScoped<IValidator<User>, UserValidator>();
@* Components/Shared/ErrorBoundary.razor *@
@inherits ErrorBoundaryBase
<div class="error-boundary">
@if (CurrentException is not null)
{
<div class="alert alert-danger" role="alert">
<h4 class="alert-heading">Something went wrong!</h4>
<p>An error occurred while processing your request.</p>
@if (Environment.IsDevelopment())
{
<hr>
<p class="mb-0">
<strong>Exception:</strong> @CurrentException.Message<br>
<strong>Stack Trace:</strong><br>
<pre>@CurrentException.StackTrace</pre>
</p>
}
<button class="btn btn-primary mt-3" @onclick="Recover">
Try Again
</button>
</div>
}
else
{
@ChildContent
}
</div>
@code {
[Parameter] public RenderFragment? ChildContent { get; set; }
protected override Task OnErrorAsync(Exception exception)
{
// Log the error
Console.WriteLine($"Error boundary caught exception: {exception}");
return Task.CompletedTask;
}
}
// Services/StateContainer.cs
public class StateContainer
{
private string? _currentUser;
private List<Notification> _notifications = new();
public string? CurrentUser
{
get => _currentUser;
set
{
_currentUser = value;
NotifyStateChanged();
}
}
public IReadOnlyList<Notification> Notifications => _notifications.AsReadOnly();
public event Action? OnChange;
public void AddNotification(Notification notification)
{
_notifications.Add(notification);
NotifyStateChanged();
}
public void RemoveNotification(int id)
{
_notifications.RemoveAll(n => n.Id == id);
NotifyStateChanged();
}
private void NotifyStateChanged() => OnChange?.Invoke();
}
// Register in Program.cs
builder.Services.AddScoped<StateContainer>();
// Install Fluxor package
// dotnet add package Fluxor.Blazor.Web
// State/UserState.cs
using Fluxor;
[FeatureState]
public record UserState
{
public User? CurrentUser { get; init; }
public bool IsLoading { get; init; }
public string? ErrorMessage { get; init; }
}
public class UserStateFeature : Feature<UserState>
{
public override string GetName() => "User";
protected override UserState GetInitialState() => new()
{
CurrentUser = null,
IsLoading = false,
ErrorMessage = null
};
}
// Actions/UserActions.cs
public record LoadUserAction(int UserId);
public record LoadUserSuccessAction(User User);
public record LoadUserFailureAction(string ErrorMessage);
// Effects/UserEffects.cs
[UsedImplicitly]
public class UserEffects
{
private readonly IUserService _userService;
public UserEffects(IUserService userService)
{
_userService = userService;
}
[EffectMethod]
public async Task HandleLoadUser(LoadUserAction action, IDispatcher dispatcher)
{
try
{
var user = await _userService.GetUserByIdAsync(action.UserId);
dispatcher.Dispatch(new LoadUserSuccessAction(user));
}
catch (Exception ex)
{
dispatcher.Dispatch(new LoadUserFailureAction(ex.Message));
}
}
}
// Reducers/UserReducers.cs
public static class UserReducers
{
[ReducerMethod]
public static UserState ReduceLoadUser(UserState state, LoadUserAction action) =>
state with { IsLoading = true, ErrorMessage = null };
[ReducerMethod]
public static UserState ReduceLoadUserSuccess(UserState state, LoadUserSuccessAction action) =>
state with { CurrentUser = action.User, IsLoading = false, ErrorMessage = null };
[ReducerMethod]
public static UserState ReduceLoadUserFailure(UserState state, LoadUserFailureAction action) =>
state with { IsLoading = false, ErrorMessage = action.ErrorMessage };
}
// Services/CachedUserService.cs
public class CachedUserService : IUserService
{
private readonly IUserService _userService;
private readonly IMemoryCache _cache;
private readonly ILogger<CachedUserService> _logger;
private readonly TimeSpan _cacheExpiration = TimeSpan.FromMinutes(15);
public CachedUserService(IUserService userService, IMemoryCache cache, ILogger<CachedUserService> logger)
{
_userService = userService;
_cache = cache;
_logger = logger;
}
public async Task<User?> GetUserByIdAsync(int userId)
{
string cacheKey = $"user_{userId}";
if (_cache.TryGetValue(cacheKey, out User? cachedUser))
{
_logger.LogInformation("User {UserId} retrieved from cache", userId);
return cachedUser;
}
var user = await _userService.GetUserByIdAsync(userId);
if (user != null)
{
_cache.Set(cacheKey, user, _cacheExpiration);
_logger.LogInformation("User {UserId} cached for {Expiration}", userId, _cacheExpiration);
}
return user;
}
public async Task<User> UpdateUserAsync(User user)
{
var updatedUser = await _userService.UpdateUserAsync(user);
// Invalidate cache
string cacheKey = $"user_{user.Id}";
_cache.Remove(cacheKey);
_logger.LogInformation("Cache invalidated for user {UserId}", user.Id);
return updatedUser;
}
}
// Services/DistributedCacheService.cs
public class DistributedCacheService
{
private readonly IDistributedCache _cache;
private readonly ILogger<DistributedCacheService> _logger;
public DistributedCacheService(IDistributedCache cache, ILogger<DistributedCacheService> logger)
{
_cache = cache;
_logger = logger;
}
public async Task<T?> GetAsync<T>(string key) where T : class
{
try
{
var cachedValue = await _cache.GetStringAsync(key);
if (cachedValue != null)
{
return JsonSerializer.Deserialize<T>(cachedValue);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error retrieving from cache with key {Key}", key);
}
return null;
}
public async Task SetAsync<T>(string key, T value, TimeSpan expiration) where T : class
{
try
{
var serializedValue = JsonSerializer.Serialize(value);
var options = new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = expiration
};
await _cache.SetStringAsync(key, serializedValue, options);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error setting cache with key {Key}", key);
}
}
public async Task RemoveAsync(string key)
{
try
{
await _cache.RemoveAsync(key);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error removing cache with key {Key}", key);
}
}
}
// Tests/UnitTests/UserServiceTests.cs
using Xunit;
using Moq;
using Microsoft.Extensions.Logging;
using MyBlazorApp.Services;
using MyBlazorApp.Data.Repositories;
public class UserServiceTests
{
private readonly Mock<IUserRepository> _mockRepository;
private readonly Mock<ILogger<UserService>> _mockLogger;
private readonly UserService _userService;
public UserServiceTests()
{
_mockRepository = new Mock<IUserRepository>();
_mockLogger = new Mock<ILogger<UserService>>();
_userService = new UserService(_mockRepository.Object, _mockLogger.Object);
}
[Fact]
public async Task GetUserByIdAsync_ValidId_ReturnsUser()
{
// Arrange
var userId = 1;
var expectedUser = new User { Id = userId, Name = "Test User", Email = "[email protected]" };
_mockRepository.Setup(r => r.GetByIdAsync(userId)).ReturnsAsync(expectedUser);
// Act
var result = await _userService.GetUserByIdAsync(userId);
// Assert
Assert.NotNull(result);
Assert.Equal(expectedUser.Id, result.Id);
Assert.Equal(expectedUser.Name, result.Name);
Assert.Equal(expectedUser.Email, result.Email);
}
[Fact]
public async Task GetUserByIdAsync_InvalidId_ReturnsNull()
{
// Arrange
var userId = 999;
_mockRepository.Setup(r => r.GetByIdAsync(userId)).ReturnsAsync((User?)null);
// Act
var result = await _userService.GetUserByIdAsync(userId);
// Assert
Assert.Null(result);
}
}
// Tests/UnitTests/ComponentTests/UserProfileTests.cs
using Bunit;
using Microsoft.Extensions.DependencyInjection;
using Moq;
using Xunit;
using MyBlazorApp.Components.Pages;
using MyBlazorApp.Services;
public class UserProfileTests : TestContext
{
[Fact]
public void UserProfile_LoadsUserData_DisplaysCorrectly()
{
// Arrange
var mockUserService = new Mock<IUserService>();
var testUser = new User { Id = 1, Name = "Test User", Email = "[email protected]" };
mockUserService.Setup(s => s.GetUserByIdAsync(1)).ReturnsAsync(testUser);
Services.AddSingleton(mockUserService.Object);
// Act
var component = RenderComponent<UserProfile>(parameters => parameters
.Add(p => p.UserId, 1));
// Assert
Assert.Contains("Test User", component.Markup);
Assert.Contains("[email protected]", component.Markup);
}
}
// Program.cs - Configure Authentication
builder.Services.AddDefaultIdentity<IdentityUser>(options =>
{
options.SignIn.RequireConfirmedAccount = false;
options.Password.RequireDigit = true;
options.Password.RequiredLength = 8;
options.Password.RequireNonAlphanumeric = true;
options.Password.RequireUppercase = true;
options.Password.RequireLowercase = true;
})
.AddEntityFrameworkStores<ApplicationDbContext>();
// Configure authorization policies
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("AdminOnly", policy => policy.RequireRole("Admin"));
options.AddPolicy("UserOrAdmin", policy => policy.RequireRole("User", "Admin"));
});
// Services/JwtTokenService.cs
public class JwtTokenService
{
private readonly IConfiguration _configuration;
public JwtTokenService(IConfiguration configuration)
{
_configuration = configuration;
}
public string GenerateToken(User user)
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(_configuration["Jwt:SecretKey"]!);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(ClaimTypes.Name, user.Name),
new Claim(ClaimTypes.Email, user.Email)
}),
Expires = DateTime.UtcNow.AddHours(24),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key),
SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
}
// For Blazor Server - Optimize SignalR connection
// Program.cs
builder.Services.AddSignalR(options =>
{
options.MaximumReceiveMessageSize = 1024 * 1024; // 1MB
options.EnableDetailedErrors = builder.Environment.IsDevelopment();
});
// For Blazor WebAssembly - Optimize bundle size
// Program.cs
builder.Services.AddScoped(sp => new HttpClient
{
BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
});
// Use lazy loading for large components
@using Microsoft.AspNetCore.Components.Web.Virtualization
<Virtualize Items="@largeDataSet" Context="item">
<ItemContent>
<div>@item.Name</div>
</ItemContent>
<Placeholder>
<div>Loading...</div>
</Placeholder>
</Virtualize>
// Program.cs - Configure Swagger
if (builder.Environment.IsDevelopment())
{
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo
{
Title = "Blazor API",
Version = "v1",
Description = "API for Blazor application"
});
// Include XML comments
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
c.IncludeXmlComments(xmlPath);
});
}
// Configure the HTTP request pipeline
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Blazor API V1");
});
}
This comprehensive guide provides a solid foundation for building scalable, maintainable Blazor applications with proper separation of concerns, performance optimization, and security considerations.