Layers, Ninject, and Moq — a Kredek workshop on ASP.NET structure

On 12 June I ran a four-hour workshop for KN Kredek at Wrocław University of Technology — Architecture of complex ASP.NET applications. It was organised as a partnership between Kredek and the company where I work; I led the session from the employer side. Polish workshop, Visual Studio on Windows, .NET Framework 4.7.2 and ASP.NET MVC 5. ASP.NET Core is already on the radar for new projects, but most of the audience still lives in MVC 5 solutions at the university and on internships.

The GitHub repo holds the exercise solution and session notes. Starter code is intentionally incomplete; MSTest projects tell you when each module is done. Below is how the four-hour block was laid out and what usually tripped people up in the room.

The workshop opened with an uncomfortable truth for architecture enthusiasts: a default MVC app already separates concerns loosely. Controllers, views, models — for a small site, calling the database from a controller can be fine. Adding BLL, DAL, Interfaces, and Tests projects on day one is how you guarantee nobody finishes the feature.

We drew a line later: when the app grows, when multiple people touch the same controllers, when business rules start leaking into views, when you need to test logic without spinning up IIS — that is when explicit layers earn their rent.

Presentation validates input shape (“name must be at least five characters”). Business logic enforces domain rules (“only one person with this surname in the database”). Data access talks to SQL, files, or external APIs. Mixing those three in one MVC controller works until it does not.

One layer can span several projects. DAL might split SQL from Azure Storage adapters. BLL might split by domain area so a future microservice extraction hurts less. Presentation might be MVC plus Web API side by side.

The Visual Studio solution in the repo follows the naming pattern common in enterprise .NET Framework shops: ExampleWebApp, ExampleWebApp.BLL, ExampleWebApp.DAL, ExampleWebApp.Interfaces, ExampleWebApp.Tests. Interfaces live in their own assembly so BLL does not reference DAL concrete types — only contracts.

Exercise one replaced manual new with Ninject. A NinjectModule binds interfaces to implementations; MVC integration resolves controller dependencies automatically.

The test that caught most people: Module1DependentService generates a new GUID per instance, but when bound in singleton scope every consumer shares the same GUID. Students who bind transient by habit see failing assertions and learn that lifetime is part of the design, not an implementation detail.

We also showed manual kernel usage — useful in tests before the full MVC pipeline is involved:

IKernel kernel = new StandardKernel();
kernel.Load(new SomeNinjectModule());
ISomeService service = kernel.Get<ISomeService>();

Microsoft.Extensions.DependencyInjection is the direction new ASP.NET Core apps take; Ninject was the teaching tool because the room was on MVC 5 and we wanted something that drops into that stack without rewriting the whole app.

Each layer gets its own model types even when property names match. Presentation models carry validation attributes. DAL models match persistence shape. BLL models sit in between for operations that should not know about [Required] or column names.

Adapter pattern — manual mapping in extension methods or static helpers — when shapes diverge. AutoMapper when names align and convention is enough. The exercise had both: implement adapter methods for one pair of types, add an AutoMapper CreateMap for another.

AutoMapper is not a substitute for thinking. It removes boilerplate; it does not remove the decision about where a field belongs.

Repository hides query construction from upper layers. Guidelines from the session:

  • Do not create one repository per table by reflex — group by business cohesion.
  • Do not return IQueryable<T> from repositories; that leaks data access upward.
  • Repositories stage changes in memory; something else commits.

Unit of Work coordinates multiple repositories into a single transaction. The exercise had two repositories each calling Save() independently — tests failed until students wired a shared unit of work so both operations produced one save. The classic failure mode: order header persisted, line items rolled back, customer billed for an empty order.

Entity Framework’s DbContext is unit of work in practice; DbSet<T> behaves like a repository. The workshop used a small DatabaseClass instead of EF so the pattern was visible without migration noise.

Last exercise: Module5Repository.DoSomething() throws NotImplementedException — deliberate “your teammate did not finish.” Tests for Module5Service should pass using Mock<IModule5Repository> without touching production code.

That mirrors real team work more than happy-path samples. You test the service’s behaviour against a contract, not against a database someone forgot to mock.

Testability recap from the slides — hard to test when the method creates DbContext inside the body; testable when context arrives via constructor injection. Moq only fakes what the unit under test needs, not half the system.

Several students asked whether they should restructure a course project that already “works” in one MVC project. The answer we landed on: if it ships next month and nobody else touches it, probably not. If three of you will extend it all summer, split layers before the controllers become the entire application.

For small projects, keep it simple. For the project that outgrows one MVC site, invest in structure before refactoring costs a sprint you do not have.