Goal Provide a concise, LLM-friendly rule set for building ASP.NET Core MVC pages that stay reusable, testable, and maintainable.
- Think in Features, Not Layers – co-locate Controllers, Views, ViewModels, Tag Helpers, and View Components under
/Features/<FeatureName>/…so everything a feature needs lives together. - Shape Data Up-Front – perform all heavy lifting (queries, transforms) in services or controllers; Razor files should only render.
- Prefer Composition over Inheritance – build UI from small, isolated pieces (partials, components, Tag Helpers) instead of giant base pages.
| When to reach for… | Use it for… | Never use it for… |
|---|---|---|
Partial View (_Card.cshtml) |
Simple, synchronous markup reuse; tiny ViewModels | DB calls, service resolution, cross-concern logic |
View Component (RecentPostsViewComponent) |
Reusable blocks with server logic, async work, or caching | One-off fragments that render once |
Custom Tag Helper (<pb-card …/>) |
Encapsulating repeated HTML patterns and attributes | Logic that touches HttpContext or business rules |
Razor Component (MyAlert.razor) |
Interactive UI in Blazor-enabled apps; SSR + hydration | Pure MVC pages without Blazor |
R1 – Organize by feature Group files by the feature they deliver, not by their technical role.
R2 – Strongly-typed ViewModels
Avoid ViewBag / dynamic; keep ViewModels flat, immutable, and serializable.
R3 – Single authoritative Layout
Put <html>, <head>, nav, and footer in _Layout.cshtml; child views declare only what changes.
R4 – Split views at ~60 lines or 3+ duplications Move repeated markup into a Partial View.
R5 – Use View Components for logic-rich blocks Async calls, service injection, or caching = View Component.
R6 – Wrap UI patterns in Tag Helpers Copy-pasting HTML? Make a Tag Helper with an attribute-driven API.
R7 – Consider Razor Components for rich interactivity
If Blazor is in play, favor .razor over JavaScript widgets.
R8 – Lean on built-in form Tag Helpers
<input asp-for> keeps names, IDs, and validation wired automatically.
R9 – Zero business logic in .cshtml
No LINQ, loops over DbSets, or complex if chains—prepare in controller.
R10 – Feature-scoped assets
Each feature/component gets its own SCSS/JS bundle; load with asp-append-version.
R11 – Depend on DI within View Components
Inject services via constructor; never use GetService manually in views.
R12 – Naming Conventions
Leading underscores for partials, *ViewComponent suffix, *TagHelper suffix.
R13 – Build for Accessibility
Expose aria-* where relevant; run axe-core in CI.
R14 – Internationalize Early
Wrap strings with IStringLocalizer; no hard-coded English.
R15 – Unit-Test UI Pieces
Render Tag Helpers in isolation; spin up WebApplicationFactory for full page tests.
R16 – Async where it matters Make View Components async and let EF Core/HTTP clients use async I/O.
R17 – Security Default-On
Antiforgery tokens via <form asp-antiforgery="true">; escape all outputs unless verified safe.
R18 – Document Intent XML-doc every Tag Helper attribute and View Component class.
R19 – Common Anti-Patterns to Avoid Massive pages (>300 lines), repeating inline scripts, partials inside tight loops without pre-fetched data, business logic in Tag Helpers.
R20 – Pre-Merge Checklist ✅ Strongly-typed ViewModel ✅ No duplicated markup ✅ Component/unit tests exist ✅ Accessibility/localization ready ✅ Assets bundled & versioned
R21 – Use URL‑Generation Tag Helpers
Always link with asp-page, asp-action, asp-controller, and asp-route‑* so routes remain DRY, version-proof, and testable—never hard-code /Products/Details/42.
Don’t – duplicate markup everywhere:
<!-- Index.cshtml -->
@foreach (var product in Model) {
<div class="card">
<h3>@product.Name</h3>
<p>@product.Price.ToString("C")</p>
</div>
}Do – factor into a partial:
<!-- _ProductCard.cshtml -->
@model ProductVm
<div class="card">
<h3>@Model.Name</h3>
<p>@Model.Price.ToString("C")</p>
</div><!-- Index.cshtml -->
@foreach (var product in Model) {
@await Html.PartialAsync("_ProductCard", product)
}Don’t – repeat attributes & Bootstrap classes:
<button class="btn btn-primary" disabled="@(isBusy ? "disabled" : null)">
Save
</button>Do – wrap in a Tag Helper:
[HtmlTargetElement("pb-button")]
public class ButtonTagHelper : TagHelper
{
public string Variant { get; set; } = "primary";
public bool Disabled { get; set; }
public override void Process(TagHelperContext ctx, TagHelperOutput output)
{
output.TagName = "button";
output.Attributes.Add("class", $"btn btn-{Variant}");
if (Disabled) output.Attributes.Add("disabled", "disabled");
}
}<pb-button variant="primary" disabled="@isBusy">Save</pb-button>Don’t – hit the database in a Partial View:
@* _RecentPosts.cshtml *@
@inject BlogDb db
@foreach (var post in db.Posts.OrderByDescending(p => p.Published).Take(5)) {
<a asp-page="/Blog/Details" asp-route-id="@post.Id">@post.Title</a>
}Do – create an async View Component:
public class RecentPostsViewComponent : ViewComponent
{
private readonly BlogDb _db;
public RecentPostsViewComponent(BlogDb db) => _db = db;
public async Task<IViewComponentResult> InvokeAsync(int count = 5)
{
var posts = await _db.Posts
.OrderByDescending(p => p.Published)
.Take(count)
.ToListAsync();
return View(posts); // Views/Shared/Components/RecentPosts/Default.cshtml
}
}@await Component.InvokeAsync("RecentPosts", new { count = 5 })Don’t – calculate discounts in the view:
@{ var discounted = Model.Price * 0.9M; }
<p>@discounted.ToString("C")</p>Do – prepare data in ViewModel:
public record ProductVm(string Name, decimal Price, decimal DiscountedPrice);<p>@Model.DiscountedPrice.ToString("C")</p>Don’t – hard‑code URLs:
<a href="/Products/Details/@prod.Id">View</a>Do – use Tag Helpers:
<!-- MVC controllers -->
<a asp-controller="Products" asp-action="Details" asp-route-id="@prod.Id">View</a>
<!-- Razor Pages -->
<a asp-page="/Products/Details" asp-route-id="@prod.Id">View</a>These helpers respect route templates, areas, localization slugs, and future refactors—no broken links.
- Mention rule IDs (e.g., “R6”) when asking follow-up questions.
- Provide code snippets; ask “Which rules am I breaking?”
- Request a summarized cheat sheet of just the rule titles for quick recall.
© 2025 Petabridge Engineering