Razor Pages provide a simplified approach to front end route handling and view provision. Combined with Entity Framework, the development of ASP.NET Core applications can be streamlined.
This example use three models: Enquiries, Media and Reporters.
public class Enquiry
{
[Key]
public int EnquiryId { get; set; }
public int? MediaId { get; set; }
public int? ReporterId { get; set; }
public string? Subject { get; set; }
public string? Description { get; set; }
public DateTime? Deadline { get; set; }
public bool IsArchived { get; set; } = false;
public DateTime CreatedOn { get; set; } = DateTime.UtcNow;
public DateTime UpdatedOn { get; set; } = DateTime.UtcNow;
// Navigation properties
public virtual Media? Media { get; set; }
public virtual Reporter? Reporter { get; set; }
}
public class Media
{
[Key]
public int MediaId { get; set; }
public string? Name { get; set; }
public MediaType? Type { get; set; } // Enum for media type
// Navigation properties
public virtual ICollection<Enquiry>? Enquiries { get; set; }
public virtual ICollection<Reporter>? Reporters { get; set; }
}
public enum MediaType
{
Local,
National,
Other
}
public class Reporter
{
[Key]
public int ReporterId { get; set; }
public int? MediaId { get; set; }
public string? Name { get; set; }
public string? Email { get; set; }
public string? Tel { get; set; }
public string? Mobile { get; set; }
public bool IsActive { get; set; } = true;
// Navigation properties
public virtual ICollection<Enquiry>? Enquiries { get; set; }
public virtual Media? Media { get; set; }
}
A custom database context, AppDbContext is then defined, with a DbSet<T> declared for each resource collection and additional constraints to be applied on model creation.
public class AppDbContext : DbContext
{
public AppDbContext() { }
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
{
}
public virtual DbSet<Enquiry> Enquiries { get; set; }
public virtual DbSet<Media> Media { get; set; }
public virtual DbSet<Reporter> Reporters { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// Defining a unique index on the Name property of the Media entity
modelBuilder.Entity<Media>()
.HasIndex(m => m.Name)
.IsUnique();
// Defining a unique index on the Name property of the Reporter entity
modelBuilder.Entity<Reporter>()
.HasIndex(r => r.Name)
.IsUnique();
// Defining set null behaviour for the Reporter entity on Media deletion
modelBuilder.Entity<Reporter>()
.HasOne(e => e.Media)
.WithMany(m => m.Reporters)
.OnDelete(DeleteBehavior.SetNull);
// Defining set null behaviour for the Enquiry entity on Media deletion
modelBuilder.Entity<Enquiry>()
.HasOne(e => e.Media)
.WithMany(m => m.Enquiries)
.OnDelete(DeleteBehavior.SetNull);
// Defining set null behaviour for the Enquiry entity on Reporter deletion
modelBuilder.Entity<Enquiry>()
.HasOne(e => e.Reporter)
.WithMany(m => m.Enquiries)
.OnDelete(DeleteBehavior.SetNull);
}
}
After defining the application models and registering the database context, initial migrations can be created and applied using the CLI.
dotnet ef migrations add InitialCreate
dotnet ef database update
Service classes were created to handle the interaction with the database using Entity Framework methods. These services were then registered in the dependency injection pipeline for later use.
public class EnquiryService
{
private readonly AppDbContext _context;
public EnquiryService(AppDbContext context)
{
_context = context;
}
public async Task<List<Enquiry>> GetAllEnquiriesAsync()
{
return await _context.Enquiries.ToListAsync();
}
public async Task<Enquiry?> GetEnquiryByIdAsync(int enquiryId)
{
return await _context.Enquiries.FindAsync(enquiryId);
}
public async Task<Enquiry?> AddEnquiryAsync(Enquiry newEnquiry)
{
try
{
await _context.Enquiries.AddAsync(newEnquiry);
await _context.SaveChangesAsync();
return newEnquiry;
}
catch (Exception ex)
{
return null;
}
}
public async Task<Enquiry?> UpdateEnquiryAsync(int enquiryId, Enquiry updatedEnquiry)
{
try
{
var enquiry = await _context.Enquiries.FindAsync(enquiryId) ?? throw new ArgumentException("Enquiry not found.");
enquiry.MediaId = updatedEnquiry.MediaId;
enquiry.ReporterId = updatedEnquiry.ReporterId;
enquiry.Subject = updatedEnquiry.Subject;
enquiry.Description = updatedEnquiry.Description;
enquiry.Deadline = updatedEnquiry.Deadline;
enquiry.IsArchived = updatedEnquiry.IsArchived;
enquiry.UpdatedOn = DateTime.UtcNow;
await _context.SaveChangesAsync();
return enquiry;
}
catch (Exception ex)
{
return null;
}
}
}
Razor pages were created through the Visual Studio helper which also provided corresponding PageModels for each Razor page. A page was required for the Index, Details, Create and Edit views of each resource.
Enquiries/
Create
Details
Edit
Index
Media/
Create
Details
Edit
Index
Reporters/
Create
Details
Edit
Index
The PageModels contained the calls to the necessary services and defined the information required for the view.
public class IndexModel : PageModel
{
private readonly EnquiryService _enquiryService;
public IndexModel(EnquiryService enquiryService)
{
_enquiryService = enquiryService;
}
public List<Enquiry> Enquiries { get; set; } = [];
public async Task<IActionResult> OnGetAsync()
{
Enquiries = await _enquiryService.GetAllEnquiriesAsync();
return Page();
}
}
Simple html views were generated to pull through the data to the surface.
@page
@model Enquiries.IndexModel
@{
ViewData["Title"] = "Enquiries";
}
<h1>Enquiries</h1>
<a asp-page="/Enquiries/Create">New Enquiry</a>
<ul>
@foreach (var enquiry in Model.Enquiries)
{
<li>
<strong>EnquiryId:</strong> @enquiry.EnquiryId <br />
<strong>MediaId:</strong> @enquiry.MediaId <br />
<strong>ReporterId:</strong> @enquiry.ReporterId <br />
<strong>Subject:</strong> @enquiry.Subject <br />
<strong>Description:</strong> @enquiry.Description <br />
<strong>Deadline:</strong> @enquiry.Deadline <br />
<strong>IsArchived:</strong> @enquiry.IsArchived <br />
<strong>CreatedOn:</strong> @enquiry.CreatedOn <br />
<strong>UpdatedOn:</strong> @enquiry.UpdatedOn <br />
<a asp-page="/Enquiries/Details" asp-route-EnquiryId="@enquiry.EnquiryId">View Details</a>
</li>
}
</ul>
Form data required ViewModel classes for validation purposes.
public class MediaViewModel
{
[Required(ErrorMessage = "The name is required.")]
[StringLength(100, ErrorMessage = "The name must not exceed 100 characters.")]
public string? Name { get; set; }
[EnumDataType(typeof(MediaType), ErrorMessage = "Invalid media type.")]
public MediaType? Type { get; set; }
}
The input data on the page could then be bound to the ViewModel allowing it to pre-populate the form on the edit pages and be validated on form submission for both edit and create pages.
public class EditModel : PageModel
{
private readonly MediaService _mediaService;
public EditModel(MediaService mediaService)
{
_mediaService = mediaService;
}
[BindProperty(SupportsGet = true)]
public int MediaId { get; set; }
[BindProperty]
public MediaViewModel MediaInput { get; set; } = new MediaViewModel();
public async Task<IActionResult> OnGetAsync()
{
var mediaToUpdate = await _mediaService.GetMediaByIdAsync(MediaId);
if (mediaToUpdate == null)
{
return NotFound();
}
MediaInput = new MediaViewModel
{
Name = mediaToUpdate.Name,
Type = mediaToUpdate.Type
};
return Page();
}
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
var mediaToUpdate = await _mediaService.GetMediaByIdAsync(MediaId);
if (mediaToUpdate == null)
{
return NotFound();
}
mediaToUpdate.Name = MediaInput.Name;
mediaToUpdate.Type = MediaInput.Type;
var updatedMedia = await _mediaService.UpdateMediaAsync(MediaId, mediaToUpdate);
if (updatedMedia == null)
{
ModelState.AddModelError("", "Unable to update media.");
return Page();
}
return RedirectToPage("./Details/", new { mediaId = MediaId });
}
}
This project explored many concepts including: models, DbContext, migrations, services, dependency injection, Razor Pages, PageModels, and ViewModels.
🛈 The full code for this ASP.NET Core Identity app, including the test suite, can be found on my GitHub.