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