Introduction: What is ASP.NET Core?
ASP.NET Core is a cross-platform, high-performance, open-source framework for building modern web applications that run on Windows, Linux, and macOS. It represents a redesign of ASP.NET that unifies the previously separate ASP.NET MVC and ASP.NET Web API into a single programming model. Built with a modular architecture, ASP.NET Core allows developers to create web applications, APIs, real-time hubs, and full-stack web solutions with superior performance, security, and scalability.
Core ASP.NET Core Concepts
Framework Architecture
- Modular Design: Components can be included via NuGet packages
- Middleware Pipeline: Request processing through configurable components
- Dependency Injection: Built-in IoC container for loose coupling
- Configuration System: Environment-based settings from multiple sources
- Hosting Model: Flexibility to self-host or run under IIS/Apache/Nginx
Application Models
- MVC (Model-View-Controller): Structured pattern for web applications
- Razor Pages: Page-based model for simpler web UI development
- Blazor: Component-based UI framework using C# instead of JavaScript
- Web API: RESTful service development
- gRPC: High-performance RPC framework
- SignalR: Real-time web functionality
ASP.NET Core Project Structure
Key Files
- Program.cs: Application entry point and configuration
- appsettings.json: Configuration settings
- Startup.cs (older versions): App configuration and service setup
- wwwroot/: Static files (CSS, JS, images)
- Controllers/: For MVC/API controllers
- Views/: For MVC views
- Pages/: For Razor Pages
- Models/: For data models
- Data/: For database context and migrations
Configuration Flow
// Program.cs in ASP.NET Core 6+
var builder = WebApplication.CreateBuilder(args);
// Service configuration
builder.Services.AddControllers();
builder.Services.AddDbContext<ApplicationDbContext>();
var app = builder.Build();
// Middleware configuration
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapControllers();
app.Run();
MVC Development
Controller Basics
[Route("api/[controller]")]
[ApiController]
public class ProductsController : ControllerBase
{
private readonly ApplicationDbContext _context;
public ProductsController(ApplicationDbContext context)
{
_context = context;
}
[HttpGet]
public async Task<ActionResult<IEnumerable<Product>>> GetProducts()
{
return await _context.Products.ToListAsync();
}
[HttpGet("{id}")]
public async Task<ActionResult<Product>> GetProduct(int id)
{
var product = await _context.Products.FindAsync(id);
if (product == null)
{
return NotFound();
}
return product;
}
[HttpPost]
public async Task<ActionResult<Product>> PostProduct(Product product)
{
_context.Products.Add(product);
await _context.SaveChangesAsync();
return CreatedAtAction(nameof(GetProduct), new { id = product.Id }, product);
}
}
Razor View Basics
@model Product
<h1>@Model.Name</h1>
<p>Price: @Model.Price.ToString("C")</p>
@if (Model.IsInStock)
{
<button>Add to Cart</button>
}
else
{
<p>Out of stock</p>
}
Tag Helpers
<a asp-controller="Home" asp-action="Index">Home</a>
<form asp-controller="Account" asp-action="Login" method="post">
<div>
<label asp-for="Email"></label>
<input asp-for="Email">
<span asp-validation-for="Email"></span>
</div>
<button type="submit">Login</button>
</form>
Razor Pages
PageModel Structure
public class CreateModel : PageModel
{
private readonly ApplicationDbContext _context;
public CreateModel(ApplicationDbContext context)
{
_context = context;
}
[BindProperty]
public Product Product { get; set; }
public IActionResult OnGet()
{
return Page();
}
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
_context.Products.Add(Product);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
}
Razor Page Markup
@page
@model CreateModel
@{
ViewData["Title"] = "Create Product";
}
<h1>@ViewData["Title"]</h1>
<form method="post">
<div class="form-group">
<label asp-for="Product.Name"></label>
<input asp-for="Product.Name" class="form-control" />
<span asp-validation-for="Product.Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Product.Price"></label>
<input asp-for="Product.Price" class="form-control" />
<span asp-validation-for="Product.Price" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-primary">Create</button>
</form>
Blazor Development
Blazor Component
@page "/counter"
<h1>Counter</h1>
<p>Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Increment</button>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}
Blazor Server vs WebAssembly vs Web App
| Feature | Blazor Server | Blazor WebAssembly | Blazor Web App |
|---|---|---|---|
| Execution | Server-side | Client-side (browser) | Both server and client |
| Initial Load | Fast | Slower (downloads .NET runtime) | Configurable |
| Network Dependency | Constant connection required | Only for API calls | Adaptable |
| Hardware Requirements | Higher server resources | Higher client resources | Flexible |
| Offline Support | None | Possible | Configurable |
| Integration with Existing Apps | Easier | Can be isolated | Most flexible |
API Development
Minimal API Example
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.MapGet("/todos", async (TodoDb db) =>
await db.Todos.ToListAsync());
app.MapGet("/todos/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id) is Todo todo
? Results.Ok(todo)
: Results.NotFound());
app.MapPost("/todos", async (Todo todo, TodoDb db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todos/{todo.Id}", todo);
});
app.Run();
OpenAPI/Swagger Integration
// Add to Program.cs
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo {
Title = "My API",
Version = "v1",
Description = "A simple API example"
});
});
// In middleware pipeline
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API v1"));
}
Authentication & Authorization
JWT Authentication Setup
// Add services
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["JWT:Issuer"],
ValidAudience = builder.Configuration["JWT:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["JWT:Secret"]))
};
});
// Add middleware
app.UseAuthentication();
app.UseAuthorization();
Authorization Policies
// Setup policies
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("AdminOnly", policy =>
policy.RequireRole("Administrator"));
options.AddPolicy("RequireMfa", policy =>
policy.RequireClaim("amr", "mfa"));
});
// Use in controllers/endpoints
[Authorize(Policy = "AdminOnly")]
public IActionResult AdminDashboard()
{
return View();
}
Data Access with Entity Framework Core
DbContext Setup
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
public DbSet<Product> Products { get; set; }
public DbSet<Category> Categories { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Product>()
.HasOne(p => p.Category)
.WithMany(c => c.Products)
.HasForeignKey(p => p.CategoryId);
modelBuilder.Entity<Category>()
.HasData(
new Category { Id = 1, Name = "Electronics" },
new Category { Id = 2, Name = "Books" }
);
}
}
Common EF Core Operations
// Adding entity
_context.Products.Add(new Product { Name = "Laptop", Price = 999.99m });
await _context.SaveChangesAsync();
// Querying with filtering
var expensiveProducts = await _context.Products
.Where(p => p.Price > 500)
.ToListAsync();
// Include related data
var productsWithCategories = await _context.Products
.Include(p => p.Category)
.ToListAsync();
// Update entity
var product = await _context.Products.FindAsync(1);
product.Price = 899.99m;
await _context.SaveChangesAsync();
// Delete entity
var productToDelete = await _context.Products.FindAsync(2);
_context.Products.Remove(productToDelete);
await _context.SaveChangesAsync();
Configuration and Options Pattern
Configuration Setup
// In Program.cs
var builder = WebApplication.CreateBuilder(args);
// Configuration sources are already set up in CreateBuilder
// Access configuration
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
// Strongly-typed configuration
builder.Services.Configure<SmtpSettings>(
builder.Configuration.GetSection("SmtpSettings"));
Using Options Pattern
// Options class
public class SmtpSettings
{
public string Server { get; set; }
public int Port { get; set; }
public string Username { get; set; }
public string Password { get; set; }
}
// In a service
public class EmailService
{
private readonly SmtpSettings _smtpSettings;
public EmailService(IOptions<SmtpSettings> smtpSettings)
{
_smtpSettings = smtpSettings.Value;
}
public void SendEmail(string to, string subject, string body)
{
// Use _smtpSettings to send email
}
}
Dependency Injection
Service Lifetimes
// Transient: New instance created for each request
builder.Services.AddTransient<ITransientService, TransientService>();
// Scoped: New instance created for each HTTP request
builder.Services.AddScoped<IScopedService, ScopedService>();
// Singleton: Single instance for application lifetime
builder.Services.AddSingleton<ISingletonService, SingletonService>();
Service Registration Patterns
// Interface-based injection
builder.Services.AddScoped<IProductRepository, SqlProductRepository>();
// Concrete class injection
builder.Services.AddScoped<ProductService>();
// Factory-based injection
builder.Services.AddSingleton<IHttpClientFactory, HttpClientFactory>();
builder.Services.AddHttpClient("github", c =>
{
c.BaseAddress = new Uri("https://api.github.com/");
c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory");
});
Middleware Components
Custom Middleware
public class RequestLoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestLoggingMiddleware> _logger;
public RequestLoggingMiddleware(RequestDelegate next, ILogger<RequestLoggingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
_logger.LogInformation($"Request: {context.Request.Method} {context.Request.Path}");
// Call the next middleware in the pipeline
await _next(context);
_logger.LogInformation($"Response: {context.Response.StatusCode}");
}
}
// Extension method for cleaner registration
public static class RequestLoggingMiddlewareExtensions
{
public static IApplicationBuilder UseRequestLogging(this IApplicationBuilder builder)
{
return builder.UseMiddleware<RequestLoggingMiddleware>();
}
}
// In Program.cs
app.UseRequestLogging();
Built-in Middleware (in typical order)
// Exception handling
app.UseExceptionHandler("/Error");
// HTTPS redirection
app.UseHttpsRedirection();
// Static files
app.UseStaticFiles();
// Cookie policy
app.UseCookiePolicy();
// Routing
app.UseRouting();
// CORS
app.UseCors("MyCorsPolicyName");
// Authentication
app.UseAuthentication();
// Authorization
app.UseAuthorization();
// Response compression
app.UseResponseCompression();
// Response caching
app.UseResponseCaching();
// Endpoint execution
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapRazorPages();
endpoints.MapBlazorHub();
});
Handling Common Web Tasks
Form Validation
// Model with validation attributes
public class ProductViewModel
{
[Required]
[StringLength(100, MinimumLength = 3)]
public string Name { get; set; }
[Required]
[Range(0.01, 10000)]
[DataType(DataType.Currency)]
public decimal Price { get; set; }
[Required]
[Display(Name = "Category")]
public int CategoryId { get; set; }
}
// In controller
[HttpPost]
public async Task<IActionResult> Create(ProductViewModel model)
{
if (!ModelState.IsValid)
{
// Return view with errors
return View(model);
}
// Process valid form
var product = new Product
{
Name = model.Name,
Price = model.Price,
CategoryId = model.CategoryId
};
_context.Products.Add(product);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
File Upload Handling
// File upload in model
public class ProductUploadModel
{
[Required]
public string Name { get; set; }
[Required]
public IFormFile ImageFile { get; set; }
}
// In controller
[HttpPost]
public async Task<IActionResult> Upload(ProductUploadModel model)
{
if (ModelState.IsValid)
{
// Process file
var fileName = Guid.NewGuid().ToString() + Path.GetExtension(model.ImageFile.FileName);
var filePath = Path.Combine(_environment.WebRootPath, "uploads", fileName);
using (var fileStream = new FileStream(filePath, FileMode.Create))
{
await model.ImageFile.CopyToAsync(fileStream);
}
// Save product with image path
var product = new Product
{
Name = model.Name,
ImagePath = "/uploads/" + fileName
};
_context.Products.Add(product);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
return View(model);
}
Error Handling
// Global exception handling in Program.cs
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseStatusCodePagesWithReExecute("/Error/{0}");
app.UseHsts();
}
// Error controller
[AllowAnonymous]
public class ErrorController : Controller
{
[Route("Error")]
public IActionResult Error()
{
var exceptionHandlerPathFeature =
HttpContext.Features.Get<IExceptionHandlerPathFeature>();
// Log the exception
_logger.LogError(exceptionHandlerPathFeature?.Error, "Unhandled exception");
return View();
}
[Route("Error/{statusCode}")]
public IActionResult StatusCodeError(int statusCode)
{
var statusCodeReExecuteFeature =
HttpContext.Features.Get<IStatusCodeReExecuteFeature>();
ViewBag.StatusCode = statusCode;
ViewBag.OriginalPath = statusCodeReExecuteFeature?.OriginalPath;
return View();
}
}
Testing
Unit Testing Controllers
// Using xUnit, Moq, and FluentAssertions
public class ProductsControllerTests
{
[Fact]
public async Task GetProducts_ReturnsAllProducts()
{
// Arrange
var mockRepo = new Mock<IProductRepository>();
mockRepo.Setup(repo => repo.GetAllAsync())
.ReturnsAsync(GetTestProducts());
var controller = new ProductsController(mockRepo.Object);
// Act
var result = await controller.GetProducts();
// Assert
var actionResult = Assert.IsType<ActionResult<IEnumerable<Product>>>(result);
var returnValue = Assert.IsType<OkObjectResult>(actionResult.Result);
var products = Assert.IsType<List<Product>>(returnValue.Value);
Assert.Equal(3, products.Count);
}
private List<Product> GetTestProducts()
{
return new List<Product>
{
new Product { Id = 1, Name = "Test Product 1" },
new Product { Id = 2, Name = "Test Product 2" },
new Product { Id = 3, Name = "Test Product 3" }
};
}
}
Integration Testing
// WebApplicationFactory-based testing
public class ProductsApiIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public ProductsApiIntegrationTests(WebApplicationFactory<Program> factory)
{
_factory = factory.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
// Remove the app's ApplicationDbContext registration
var descriptor = services.SingleOrDefault(
d => d.ServiceType == typeof(DbContextOptions<ApplicationDbContext>));
if (descriptor != null)
{
services.Remove(descriptor);
}
// Add ApplicationDbContext using an in-memory database
services.AddDbContext<ApplicationDbContext>(options =>
{
options.UseInMemoryDatabase("InMemoryDbForTesting");
});
// Seed test data
var sp = services.BuildServiceProvider();
using (var scope = sp.CreateScope())
{
var scopedServices = scope.ServiceProvider;
var db = scopedServices.GetRequiredService<ApplicationDbContext>();
db.Database.EnsureCreated();
// Seed the database
if (!db.Products.Any())
{
db.Products.AddRange(
new Product { Id = 1, Name = "Test Product 1", Price = 9.99m },
new Product { Id = 2, Name = "Test Product 2", Price = 19.99m }
);
db.SaveChanges();
}
}
});
});
}
[Fact]
public async Task GetProducts_ReturnsSuccessAndProducts()
{
// Arrange
var client = _factory.CreateClient();
// Act
var response = await client.GetAsync("/api/products");
// Assert
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
var products = JsonSerializer.Deserialize<List<Product>>(content,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
Assert.Equal(2, products.Count);
Assert.Contains(products, p => p.Name == "Test Product 1");
Assert.Contains(products, p => p.Name == "Test Product 2");
}
}
Performance Optimization
Response Caching
// Add service
builder.Services.AddResponseCaching();
// Configure middleware
app.UseResponseCaching();
// Use in controller/endpoint
[HttpGet]
[ResponseCache(Duration = 60, Location = ResponseCacheLocation.Any)]
public async Task<ActionResult<IEnumerable<Product>>> GetProducts()
{
return await _context.Products.ToListAsync();
}
Output Caching (Newer API)
// Add service
builder.Services.AddOutputCache(options =>
{
// Configure default policy
options.AddBasePolicy(builder => builder.Expire(TimeSpan.FromMinutes(5)));
// Add named policies
options.AddPolicy("ShortLived", builder =>
builder.Expire(TimeSpan.FromSeconds(10))
.SetVaryByQuery("page", "pageSize"));
});
// Use middleware
app.UseOutputCache();
// Use in controller/endpoint
[HttpGet]
[OutputCache(PolicyName = "ShortLived")]
public async Task<ActionResult<IEnumerable<Product>>> GetProducts(
[FromQuery] int page = 1,
[FromQuery] int pageSize = 10)
{
// Implementation
}
Memory Caching
// Add service
builder.Services.AddMemoryCache();
// Use in service
public class CachedProductService
{
private readonly IMemoryCache _cache;
private readonly ApplicationDbContext _context;
public CachedProductService(IMemoryCache cache, ApplicationDbContext context)
{
_cache = cache;
_context = context;
}
public async Task<List<Product>> GetProductsAsync()
{
// Check if data exists in cache
if (!_cache.TryGetValue("AllProducts", out List<Product> products))
{
// If not in cache, get from database
products = await _context.Products.ToListAsync();
// Save to cache with expiration
var cacheOptions = new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(TimeSpan.FromMinutes(10))
.SetSlidingExpiration(TimeSpan.FromMinutes(2));
_cache.Set("AllProducts", products, cacheOptions);
}
return products;
}
}
Common Challenges and Solutions
Cross-Origin Resource Sharing (CORS)
// Add service with policy
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowSpecificOrigin", builder =>
{
builder.WithOrigins("https://example.com")
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
});
options.AddPolicy("AllowAnyOrigin", builder =>
{
builder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
});
});
// Use middleware
app.UseCors("AllowSpecificOrigin");
// Or apply to specific controllers/endpoints
[EnableCors("AllowAnyOrigin")]
[Route("api/[controller]")]
public class PublicApiController : ControllerBase
{
// Implementation
}
Handling Long-Running Operations
// Add background service
public class ProcessingService : BackgroundService
{
private readonly ILogger<ProcessingService> _logger;
public ProcessingService(ILogger<ProcessingService> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Processing Service is starting.");
while (!stoppingToken.IsCancellationRequested)
{
_logger.LogInformation("Processing items...");
// Process work items
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
}
_logger.LogInformation("Processing Service is stopping.");
}
}
// Register service
builder.Services.AddHostedService<ProcessingService>();
Localization and Globalization
// Add services
builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");
builder.Services.AddMvc()
.AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix)
.AddDataAnnotationsLocalization();
builder.Services.Configure<RequestLocalizationOptions>(options =>
{
var supportedCultures = new[]
{
new CultureInfo("en-US"),
new CultureInfo("fr"),
new CultureInfo("es")
};
options.DefaultRequestCulture = new RequestCulture("en-US");
options.SupportedCultures = supportedCultures;
options.SupportedUICultures = supportedCultures;
});
// Use middleware
app.UseRequestLocalization();
// In controller
public class HomeController : Controller
{
private readonly IStringLocalizer<HomeController> _localizer;
public HomeController(IStringLocalizer<HomeController> localizer)
{
_localizer = localizer;
}
public IActionResult Index()
{
ViewData["Welcome"] = _localizer["WelcomeMessage"];
return View();
}
}
Best Practices
Security Best Practices
- Use HTTPS: Enable HTTPS in development and production
- Input Validation: Validate all user inputs with model validation
- Output Encoding: Use built-in features like
@Html.Rawsparingly - Anti-forgery Tokens: Use
@Html.AntiForgeryToken()for forms - Authorization Policies: Implement granular authorization with policies
- Secure Configuration: Store secrets in User Secrets or Key Vault
- Data Protection API: Use for cookie protection and encryption
- Regular Updates: Keep dependencies and frameworks updated
Performance Best Practices
- Asynchronous Programming: Use async/await for I/O-bound operations
- Caching Strategy: Implement appropriate caching for frequently accessed data
- Bundling & Minification: Optimize static assets for production
- Efficient Queries: Use specific queries and avoid N+1 query problems
- Lazy Loading: Configure Entity Framework appropriately
- Response Compression: Enable for appropriate content types
- Health Checks: Implement to monitor application health
- Use Minimal APIs: For simple CRUD operations when appropriate
Deployment Best Practices
- Environment Configuration: Use environment-specific settings
- Containerization: Use Docker for consistent deployments
- CI/CD Pipeline: Automate build, test, and deployment processes
- Logging & Monitoring: Implement comprehensive logging and monitoring
- Database Migrations: Use EF Core migrations for database updates
- Load Balancing: Deploy behind load balancer for high availability
- Backup Strategy: Implement regular database backups
- Rollback Plan: Have a strategy for rolling back problematic deployments
Resources for Further Learning
Official Documentation
Community Resources
Recommended Books
- “ASP.NET Core in Action” by Andrew Lock
- “Pro ASP.NET Core 6” by Adam Freeman
- “Microservices with ASP.NET Core” by Christian Horsdal
- “Entity Framework Core in Action” by Jon P. Smith
This cheat sheet provides a comprehensive reference for ASP.NET Core development. For the most current features and practices, always refer to the official documentation as the framework continues to evolve.
