I once inherited a .NET application. I opened the solution, saw the familiar folder structure (Models, Controllers, Views) and thought I knew what I was dealing with. ASP.NET MVC. Straightforward.
Then I tried to change something, and nothing worked the way it should.
Model binding? Broken. Strongly-typed views? Not really. Validation attributes? Nowhere to put them. The Razor IntelliSense was a mess. Every small change felt like defusing a bomb with no wiring diagram.
It took me a while to understand what had happened. The application had the file structure of MVC, but the architecture of something else entirely. Underneath the familiar folders, ADO.NET was running the show, and it had brought all its baggage with it.
How MVC Is Supposed to Work
ASP.NET MVC is built on a contract. You hold up your end, and the framework gives you model binding, validation, strongly-typed views, testable controllers, and clean separation of concerns. All for free.
Here's the flow MVC expects:
flowchart LR
A[Browser Request] --> B[Controller]
B --> C[Model / Service Layer]
C --> D[(Database)]
D --> C
C --> B
B --> E[ViewModel]
E --> F[Razor View]
F --> G[HTML Response]
The controller orchestrates. The model layer fetches data. A ViewModel carries exactly what the view needs. The view renders HTML. Each layer has one job, and the data flows in one direction.
The contract looks like this:
// A clean ViewModel — a POCO with validation
public class CustomerViewModel
{
[Required]
[StringLength(100)]
public string Name { get; set; }
[EmailAddress]
public string Email { get; set; }
public List<OrderSummary> RecentOrders { get; set; }
}
Your controller creates or receives these ViewModels. Your view declares its model type with @model CustomerViewModel and gets IntelliSense, type safety, and tag helpers. When a form posts back, MVC's model binder populates the ViewModel from the request, runs the validation attributes, and hands you a clean object.
It's a well-oiled machine, but only if you feed it the right fuel.
What I Actually Found
Here's what the actual data flow looked like:
flowchart LR
A[Browser Request] --> B[Controller]
B --> C[ViewModel]
C -->|Inherits Model| D[Model]
D -->|Executes Sprocs| E[(Database)]
E --> D
D --> C
C --> F[Razor View]
F --> G[jQuery Overrides]
G --> H[HTML Response]
style C fill:#e63946,color:#fff
style G fill:#e63946,color:#fff
The ViewModel inherits the Model. The ViewModel executes stored procedures directly. jQuery intercepts and overrides MVC's client-side behaviour. Every layer is tangled into the next.
Let me break down each problem.
ViewModels Executing Stored Procedures
This was the one that really threw me. The Model actually looks like what a ViewModel should be. Simple properties, nothing more.
// The Model — looks like a ViewModel, complete with display annotations
public class CustomerModel
{
[Display(Name = "Customer Name")]
[Parameter(DataType = ParameterDataType.String)]
public string Name { get; set; }
[Display(Name = "Email Address")]
[Parameter(DataType = ParameterDataType.String)]
public string Email { get; set; }
[Parameter(DataType = ParameterDataType.Int)]
public int CustomerId { get; set; }
}
It's even got [Display] attributes and [Parameter] annotations, the kind of thing you'd expect on a ViewModel. It's doing the ViewModel's job. But then the actual ViewModel comes along, inherits the Model, and bolts on all the database access:
// The ViewModel — inherits the Model, then adds sproc execution
public class CustomerViewModel : CustomerModel
{
public void LoadCustomer()
{
using DbDataReader reader = new DataAccessor()
.GetResults("spGetCustomer", this, ParameterType.PrimaryKey);
if (reader.Read())
{
Name = reader["Name"].ToString();
Email = reader["Email"].ToString();
}
}
public void SaveCustomer()
{
using DbDataReader reader = new DataAccessor()
.GetResults("spSaveCustomer", this, ParameterType.AllFields);
}
}
Read that again. The ViewModel passes this to the DataAccessor. It's handing itself, the object bound to the Razor view, to the data access layer, which uses the [Parameter] attributes on the inherited Model properties to build the sproc parameters. The Model's annotations aren't just for display; they're doing double duty as the database parameter mapping.
That ParameterType.PrimaryKey tells the DataAccessor to look at this, find the properties marked as primary key parameters, and use their values for the sproc call. The ViewModel is simultaneously the data carrier, the parameter source, and the sproc executor. One object, three responsibilities that should never have met.
The naming is backwards. The Model is the dumb data bag, which is what a ViewModel should be. The ViewModel is the data access layer, which is what a Model should never be. And because the view is bound to the ViewModel, anything with a reference to it can execute stored procedures against the database.
In proper MVC, the ViewModel carries data. Here, it's the most powerful object in the application.
jQuery Overriding MVC Patterns
MVC has built-in support for client-side validation, form submission, and partial page updates through unobtrusive JavaScript. But none of that was being used. Instead, jQuery was wired up to handle everything manually:
// jQuery overriding MVC's form behaviour
$('#customerForm').on('submit', function() {
// Manual validation — MVC's validation is bypassed entirely
if ($('#Name').val() === '') {
alert('Name is required');
return false;
}
$.ajax({
url: '/Customer/Save',
type: 'POST',
data: $(this).serialize(),
success: function(result) {
window.location.href = '/Customer/Details/' + result.id;
}
});
return false;
});
The return false at the end stops the normal form submission, and the jQuery validation runs before MVC's unobtrusive validation ever gets a chance. The jQuery acts as a parallel framework that shadows MVC's pipeline, handling submission, validation, and routing all by itself. MVC's features aren't broken, they're just never reached.
Validation Theatre
This was the subtlest problem, and probably the most dangerous. The application had [Required] attributes on some ViewModel properties and .validation-message spans in the Razor markup, so it looked like MVC validation was in place.
But it wasn't doing anything. Two things conspired to hide this:
- jQuery handled all validation: the manual
$.ajaxsubmissions meant MVC's server-sideModelState.IsValidwas never checked, and the client-side unobtrusive validation was never triggered. - CSS hid the evidence: the stylesheet contained rules that made MVC's validation messages invisible:
.field-validation-error,
.validation-summary-errors {
display: none;
}
The validation infrastructure existed in the markup, but it was both bypassed by jQuery and hidden by CSS. If you ever removed the jQuery validation or the CSS override, you'd discover that the MVC validation either didn't work (because the ViewModels inherit Models rather than being clean POCOs) or showed errors nobody had ever seen.
It's validation theatre. The appearance of safety, with none of the actual protection.
The Full Picture
When you stack all these layers together, you get an application where every MVC pattern has been adopted in name but undermined in practice:
flowchart TD
subgraph "What the folder structure promises"
M1[Models] -.-> C1[Controllers] -.-> V1[Views]
end
subgraph "What actually happens"
V2[View] -->|inherits| VM[ViewModel]
VM -->|inherits| MO[Model]
MO -->|executes| SP[(Stored Procs)]
JQ[jQuery] -->|overrides| V2
JQ -->|bypasses| MB[Model Binding]
JQ -->|bypasses| VA[Validation]
CSS[CSS] -->|hides| VE[Validation Errors]
end
style JQ fill:#e63946,color:#fff
style CSS fill:#e63946,color:#fff
style VM fill:#e63946,color:#fff
The folders say MVC. The inheritance chain says "everything is everything." jQuery says "I'll handle it." CSS says "nothing to see here."
The "It Works" Trap
The dangerous thing about this pattern is that the application runs. Pages render. Data shows up. Forms submit. If you're not familiar with how MVC is supposed to work, it all looks fine.
But every change is expensive:
- Adding a field means editing the stored procedure, the Model, the ViewModel that inherits it, the jQuery validation, and the Razor markup, and hoping you caught every string reference.
- Refactoring is terrifying because there are no compile-time checks between the data layer and the UI.
- Unit testing controllers is nearly impossible when ViewModels inherit database access.
- New developers spend hours confused about why standard MVC patterns don't work.
The app has the appearance of structure without the substance of it. The folders say MVC, but the code says "we moved from WebForms and kept our habits."
How This Happens
I don't think anyone sets out to build this deliberately. It usually goes like this:
- A team has years of experience with ADO.NET, WebForms, or classic ASP.NET.
- A new project (or rewrite) adopts ASP.NET MVC because it's the modern choice.
- The team creates the folder structure (Models, Controllers, Views) because that's what the template gives you.
- When it's time to get data, they reach for what they know. Out come
SqlConnection,SqlCommand, and stored procedures. The Models become data access classes. - ViewModels inherit from Models because "they need the same data plus a few extra properties." The inheritance feels efficient.
- jQuery gets added for form handling because the team knows jQuery. MVC's unobtrusive JavaScript is unfamiliar.
- When MVC's validation messages appear unexpectedly, CSS hides them rather than fixing the root cause.
- Over time, each workaround reinforces the others. The MVC patterns that would prevent this are never adopted because the team has a "working" approach.
The result is an application that's structurally MVC but architecturally ADO.NET-with-jQuery-and-HTML-templates. It's wearing MVC's clothes but speaking a completely different language.
The Way Out
The good news is you don't have to rewrite everything. The fix starts with breaking the inheritance chain. Make ViewModels standalone.
// Step 1: Define what the view actually needs — no inheritance
public class CustomerDetailsViewModel
{
[Required]
public string Name { get; set; }
[EmailAddress]
public string Email { get; set; }
public List<OrderLine> RecentOrders { get; set; }
}
// Step 2: Map from the data access layer in the controller
public ActionResult CustomerDetails(int id)
{
var model = new CustomerModel();
DataTable dt = model.GetCustomer(id);
var viewModel = new CustomerDetailsViewModel
{
Name = dt.Rows[0]["Name"].ToString(),
Email = dt.Rows[0]["Email"].ToString(),
RecentOrders = MapOrders(dt)
};
return View(viewModel);
}
<!-- Step 3: The view is now strongly typed -->
@model CustomerDetailsViewModel
<h1>@Model.Name</h1>
<p>@Model.Email</p>
Then, incrementally:
- Break the ViewModel inheritance: one class at a time, make ViewModels standalone POCOs.
- Remove jQuery form overrides: let MVC handle submission and validation natively.
- Delete the CSS that hides validation: let the framework show errors as intended.
- Check
ModelState.IsValidin every POST action; it's probably not being checked.
Each step is small and testable. The ADO.NET data access layer can stay exactly as it is for now. You're just stopping it from leaking through every layer.
Over time, you might replace the sproc-executing Models with a repository pattern, Entity Framework, or Dapper. But that's a separate decision. The immediate win is giving MVC its pipeline back, and that starts with a ViewModel that doesn't know how to talk to a database.
The File Structure Isn't the Architecture
The biggest lesson from this experience is that MVC is not a folder layout. It's a set of conventions and contracts.
You can have Models/, Controllers/, and Views/ folders and still not be doing MVC. If your ViewModels inherit data access, your jQuery shadows the framework, and your CSS hides the evidence, you've got a WebForms application in an MVC trench coat.
The folder structure is scaffolding. The architecture is in how the pieces communicate. Controllers orchestrate, ViewModels carry data, views render. When those responsibilities blur, when a ViewModel can execute a stored procedure or jQuery can bypass validation, you lose every advantage MVC was designed to give you.
Next time you open a .NET project and see that familiar folder structure, look deeper before you trust it. The folders might be lying to you.