
Previous article: ASP.NET Core Error Handling: Master Middleware, Logging & Exception Strategies (Part-20 of 40)
Table of Contents
Introduction to Razor Pages Revolution
Razor Pages vs MVC: When to Use What
Page Model Deep Dive
Handler Methods Mastery
Advanced Data Binding Techniques
Real-World E-Commerce Application
Validation and Error Handling
Partial Views and View Components
Advanced Routing Scenarios
Performance Optimization
Security Best Practices
Testing Strategies
Deployment and DevOps
1. Introduction to Razor Pages Revolution
The Evolution of Web Development in ASP.NET Core
Razor Pages represents a significant paradigm shift in how we build web applications with ASP.NET Core. Unlike the traditional MVC pattern that separates concerns across multiple files, Razor Pages embraces a page-centric approach that brings related components together.
// Traditional MVC - Multiple Files// Controller: HomeController.cs// View: Views/Home/Index.cshtml// Model: Models/IndexViewModel.cs
// Razor Pages - Unified Approach// Pages/Index.cshtml// Pages/Index.cshtml.cs (Page Model)Why Razor Pages Changed Everything
Real-Life Analogy: Think of building a house. With MVC, you have separate teams for foundation (Model), structure (Controller), and interior (View). With Razor Pages, you have dedicated teams for each room (Page) that handle everything related to that specific space.
// Before: MVC Controllers growing uncontrollablypublic class CustomerController : Controller{
public IActionResult Index() { /* list customers */ }
public IActionResult Create() { /* show form */ }
public IActionResult Create(Customer customer) { /* process form */ }
public IActionResult Edit(int id) { /* show edit form */ }
public IActionResult Edit(Customer customer) { /* process edit */ }
public IActionResult Delete(int id) { /* show delete confirmation */ }
public IActionResult DeleteConfirmed(int id) { /* process delete */ }
// ... and 20 more actions}
// After: Organized Razor Pages// Pages/Customers/Index.cshtml// Pages/Customers/Create.cshtml// Pages/Customers/Edit.cshtml// Pages/Customers/Delete.cshtmlSetting Up Your First Razor Pages Project
// Program.cs - Razor Pages Configurationvar builder = WebApplication.CreateBuilder(args);
// Add services to the container
builder.Services.AddRazorPages()
.AddRazorPagesOptions(options =>
{
options.RootDirectory = "/Pages";
options.Conventions.AddPageRoute("/Index", "");
});
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
var app = builder.Build();
// Configure the HTTP request pipelineif (!app.Environment.IsDevelopment()){
app.UseExceptionHandler("/Error");
app.UseHsts();}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();
app.Run();2. Razor Pages vs MVC: When to Use What
Architectural Comparison
// MVC Pattern Structure// Controllers/ProductsController.cspublic class ProductsController : Controller{
private readonly IProductService _productService;
public ProductsController(IProductService productService)
{
_productService = productService;
}
public async Task<IActionResult> Index()
{
var products = await _productService.GetAllAsync();
return View(products);
}
public IActionResult Create()
{
return View();
}
[HttpPost]
public async Task<IActionResult> Create(Product product)
{
if (ModelState.IsValid)
{
await _productService.CreateAsync(product);
return RedirectToAction(nameof(Index));
}
return View(product);
}}
// Razor Pages Structure// Pages/Products/Index.cshtml.cspublic class IndexModel : PageModel{
private readonly IProductService _productService;
public List<Product> Products { get; set; }
public IndexModel(IProductService productService)
{
_productService = productService;
}
public async Task OnGetAsync()
{
Products = await _productService.GetAllAsync();
}}
// Pages/Products/Create.cshtml.cspublic class CreateModel : PageModel{
private readonly IProductService _productService;
[BindProperty]
public Product Product { get; set; }
public CreateModel(IProductService productService)
{
_productService = productService;
}
public void OnGet()
{
}
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
return Page();
await _productService.CreateAsync(Product);
return RedirectToPage("./Index");
}}Decision Matrix: When to Choose Razor Pages
| Scenario | Recommended Approach | Reasoning |
|---|---|---|
| Content-focused websites | Razor Pages | Simplified page-centric development |
| Complex SPA applications | MVC + API | Better separation for complex client logic |
| Internal business apps | Razor Pages | Rapid development, clear organization |
| Large enterprise systems | MVC | Traditional separation of concerns |
| Mixed requirements | Hybrid (Razor Pages + MVC) | Flexibility for different scenarios |
Real-World Case Study: E-Learning Platform
// Pages/Courses/Index.cshtml.cspublic class IndexModel : PageModel{
private readonly ICourseService _courseService;
private readonly IEnrollmentService _enrollmentService;
public List<Course> FeaturedCourses { get; set; }
public List<Course> MyCourses { get; set; }
public List<Category> Categories { get; set; }
[BindProperty(SupportsGet = true)]
public string SearchTerm { get; set; }
[BindProperty(SupportsGet = true)]
public int? CategoryId { get; set; }
public IndexModel(ICourseService courseService, IEnrollmentService enrollmentService)
{
_courseService = courseService;
_enrollmentService = enrollmentService;
}
public async Task OnGetAsync()
{
// Multiple data operations in single handler
var featuredTask = _courseService.GetFeaturedCoursesAsync();
var categoriesTask = _courseService.GetCategoriesAsync();
if (User.Identity.IsAuthenticated)
{
var myCoursesTask = _enrollmentService.GetUserCoursesAsync(User.GetUserId());
await Task.WhenAll(featuredTask, categoriesTask, myCoursesTask);
MyCourses = myCoursesTask.Result;
}
else
{
await Task.WhenAll(featuredTask, categoriesTask);
}
FeaturedCourses = featuredTask.Result;
Categories = categoriesTask.Result;
// Apply filters
if (!string.IsNullOrEmpty(SearchTerm))
{
FeaturedCourses = FeaturedCourses
.Where(c => c.Title.Contains(SearchTerm, StringComparison.OrdinalIgnoreCase))
.ToList();
}
if (CategoryId.HasValue)
{
FeaturedCourses = FeaturedCourses
.Where(c => c.CategoryId == CategoryId.Value)
.ToList();
}
}}3. Page Model Deep Dive
Page Model Architecture
// Advanced Page Model Base Classpublic abstract class BasePageModel : PageModel{
protected readonly ILogger<BasePageModel> _logger;
protected readonly IStringLocalizer<BasePageModel> _localizer;
public BasePageModel(ILogger<BasePageModel> logger, IStringLocalizer<BasePageModel> localizer)
{
_logger = logger;
_localizer = localizer;
}
// Common properties for all pages
public string PageTitle { get; set; }
public string Breadcrumb { get; set; }
public bool ShowSidebar { get; set; } = true;
// Common methods
protected void AddSuccessMessage(string message)
{
TempData["SuccessMessage"] = _localizer[message];
}
protected void AddErrorMessage(string message)
{
TempData["ErrorMessage"] = _localizer[message];
}
protected IActionResult RedirectToLocal(string returnUrl)
{
if (Url.IsLocalUrl(returnUrl))
return Redirect(returnUrl);
return RedirectToPage("/Index");
}}
// Concrete implementationpublic class ProductDetailModel : BasePageModel{
private readonly IProductService _productService;
private readonly IReviewService _reviewService;
public Product Product { get; set; }
public List<Review> Reviews { get; set; }
public ReviewInputModel NewReview { get; set; }
[BindProperty(SupportsGet = true)]
public int Id { get; set; }
public ProductDetailModel(
IProductService productService,
IReviewService reviewService,
ILogger<ProductDetailModel> logger,
IStringLocalizer<ProductDetailModel> localizer)
: base(logger, localizer)
{
_productService = productService;
_reviewService = reviewService;
}
public async Task<IActionResult> OnGetAsync()
{
try
{
var productTask = _productService.GetByIdAsync(Id);
var reviewsTask = _reviewService.GetProductReviewsAsync(Id);
await Task.WhenAll(productTask, reviewsTask);
Product = productTask.Result;
Reviews = reviewsTask.Result;
if (Product == null)
{
_logger.LogWarning("Product with ID {ProductId} not found", Id);
return NotFound();
}
PageTitle = Product.Name;
Breadcrumb = $"{Product.Category.Name} > {Product.Name}";
return Page();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error loading product detail for ID {ProductId}", Id);
AddErrorMessage("Failed to load product details");
return RedirectToPage("/Error");
}
}}Property Binding Advanced Scenarios
// Complex binding scenariospublic class OrderCreateModel : PageModel{
// Simple property binding
[BindProperty]
public string CustomerName { get; set; }
// Complex object binding
[BindProperty]
public Order Order { get; set; }
// Collection binding
[BindProperty]
public List<OrderItem> OrderItems { get; set; } = new List<OrderItem>();
// File upload binding
[BindProperty]
public IFormFile Attachment { get; set; }
// Query string binding
[BindProperty(SupportsGet = true)]
public int? CustomerId { get; set; }
// Route data binding
[BindProperty(SupportsGet = true, Name = "id")]
public int OrderId { get; set; }
// Optional binding with default value
[BindProperty(SupportsGet = true)]
public string SortBy { get; set; } = "name";
// Secure binding (not bound from request)
[BindNever]
public string InternalReference { get; set; }
// Custom binding with validation
[BindProperty]
[Required]
[EmailAddress]
public string Email { get; set; }
public void OnGet()
{
// Pre-populate from customer if provided
if (CustomerId.HasValue)
{
var customer = _customerService.GetById(CustomerId.Value);
if (customer != null)
{
CustomerName = customer.Name;
Order = new Order { CustomerId = customer.Id };
}
}
}
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
// Process file upload
if (Attachment != null && Attachment.Length > 0)
{
var filePath = Path.Combine("uploads", Attachment.FileName);
using (var stream = new FileStream(filePath, FileMode.Create))
{
await Attachment.CopyToAsync(stream);
}
Order.AttachmentPath = filePath;
}
// Add order items to order
Order.Items = OrderItems;
await _orderService.CreateAsync(Order);
AddSuccessMessage("Order created successfully");
return RedirectToPage("./Details", new { id = Order.Id });
}
// AJAX handler for adding order items
public IActionResult OnPostAddOrderItem([FromBody] OrderItem item)
{
if (!ModelState.IsValid)
return new BadRequestObjectResult(ModelState);
OrderItems.Add(item);
return new JsonResult(new { success = true, items = OrderItems });
}}4. Handler Methods Mastery
Comprehensive Handler Patterns
public class AdvancedHandlersModel : PageModel{
// Synchronous GET handler
public void OnGet()
{
// Basic page initialization
}
// Asynchronous GET handler
public async Task OnGetAsync()
{
// Async data loading
await LoadDataAsync();
}
// GET handler with parameters
public IActionResult OnGetWithId(int id)
{
// Custom handler for specific scenarios
return RedirectToPage("./Details", new { id });
}
// POST handler
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
return Page();
await ProcessFormAsync();
return RedirectToPage("./Success");
}
// POST handler with custom name
public async Task<IActionResult> OnPostSaveDraftAsync()
{
await SaveAsDraftAsync();
return RedirectToPage("./Drafts");
}
// DELETE handler for RESTful operations
public async Task<IActionResult> OnDeleteAsync(int id)
{
await DeleteItemAsync(id);
return new OkResult();
}
// PUT handler for updates
public async Task<IActionResult> OnPutAsync([FromBody] Item item)
{
await UpdateItemAsync(item);
return new OkResult();
}
// Handler with return type
public async Task<JsonResult> OnGetSearchAsync(string term)
{
var results = await SearchItemsAsync(term);
return new JsonResult(results);
}
// File download handler
public async Task<IActionResult> OnGetDownloadAsync(int id)
{
var file = await GetFileAsync(id);
if (file == null)
return NotFound();
return File(file.Content, file.ContentType, file.Name);
}
// Handler with multiple parameters
public async Task<IActionResult> OnGetFilterAsync(string category, string sortBy, int page = 1)
{
var items = await GetFilteredItemsAsync(category, sortBy, page);
return Partial("_ItemList", items);
}}
// Real-world example: Blog Managementpublic class BlogPostModel : PageModel{
private readonly IBlogService _blogService;
private readonly IImageService _imageService;
public BlogPost Post { get; set; }
public List<BlogPost> RelatedPosts { get; set; }
public CommentInput NewComment { get; set; }
[BindProperty(SupportsGet = true)]
public string Slug { get; set; }
public BlogPostModel(IBlogService blogService, IImageService imageService)
{
_blogService = blogService;
_imageService = imageService;
}
public async Task<IActionResult> OnGetAsync()
{
if (string.IsNullOrEmpty(Slug))
return RedirectToPage("/Blog/Index");
Post = await _blogService.GetPostBySlugAsync(Slug);
if (Post == null)
return NotFound();
// Increment view count
await _blogService.IncrementViewCountAsync(Post.Id);
// Load related posts
RelatedPosts = await _blogService.GetRelatedPostsAsync(Post.CategoryId, Post.Id);
return Page();
}
public async Task<IActionResult> OnPostAddCommentAsync(CommentInput comment)
{
if (!ModelState.IsValid)
{
// Reload post data for the form
await LoadPostDataAsync();
return Page();
}
var result = await _blogService.AddCommentAsync(Post.Id, comment, User.Identity.Name);
if (result.Success)
{
AddSuccessMessage("Comment added successfully");
return RedirectToPage(new { Slug });
}
else
{
ModelState.AddModelError("", result.ErrorMessage);
await LoadPostDataAsync();
return Page();
}
}
public async Task<IActionResult> OnPostLikeAsync()
{
if (!User.Identity.IsAuthenticated)
return Challenge();
var result = await _blogService.LikePostAsync(Post.Id, User.GetUserId());
return new JsonResult(new { success = result.Success, likes = result.Likes });
}
public async Task<IActionResult> OnPostShareAsync(string platform)
{
var shareUrl = await _blogService.GenerateShareUrlAsync(Post.Id, platform);
return new JsonResult(new { url = shareUrl });
}
// AJAX handler for live search
public async Task<IActionResult> OnGetSearchPostsAsync(string query)
{
if (string.IsNullOrWhiteSpace(query) || query.Length < 3)
return new JsonResult(new { results = new List<object>() });
var posts = await _blogService.SearchPostsAsync(query);
var results = posts.Select(p => new {
id = p.Id,
title = p.Title,
slug = p.Slug,
excerpt = p.Excerpt
});
return new JsonResult(new { results });
}
private async Task LoadPostDataAsync()
{
Post = await _blogService.GetPostBySlugAsync(Slug);
RelatedPosts = await _blogService.GetRelatedPostsAsync(Post.CategoryId, Post.Id);
}}
Join the conversation! Your thoughts help the community grow.