Layers: Models, Services, Controllers
Apps are split into layers (controllers, services, data access and models), each with one job, each handed the layer below it through its constructor.
Journeyman · C# · OOP · Updated
After this lesson you can
- Explain the job of each layer (controller, service, data access, model) and where a piece of code belongs
Key takeaways
- Controllers talk to the web, services hold the business rules, DAOs read and write the database, and models carry data between them.
- A business rule, such as the 10% Beverages sale, belongs in a service like
PriceService, not in the controller or the DAO. - Each layer is handed the layer below it through its constructor: the controller gets the service, and the service gets the DAO.
- Layers can be tested alone and swapped: give
PriceServicea fake DAO to test the sale rule without a database.
As an app grows, putting everything in a controller turns it into a tangle: web code, business rules and SQL all in one class. Instead, apps are built in layers, each with one job, each depending only on the layer below it.
The layers
| Layer | Its job | Example |
|---|---|---|
| Controller | Talks to the web: reads the request, picks the response | ProductsController |
| Service | Holds the business rules: what the app is supposed to do | PriceService: what a product costs today |
| Data access | Reads and writes the database, nothing more | ProductDao |
| Model | Carries data between all of them | Product |
Each layer is handed the one below it, through its constructor:
ProductsController --needs--> PriceService --needs--> ProductDao --reads--> SQL Server
\ | |
\---------- all of them pass Product models around ---/
A service between the controller and the DAO
Say Northwind runs a sale: everything in the Beverages category (category 1) is 10% off. Where does that rule go?
- Not in the DAO. The DAO's job is getting rows, not deciding prices.
- Not in the controller. The controller's job is the web; and the rule would be needed again by the next controller, and the next.
- In a service. One class,
PriceService, holds the rule, and anything that needs a price asks it.
Here's the shape, with a small in-memory DAO standing in for the database:
ProductDao productDao = new ProductDao();
PriceService priceService = new PriceService(productDao);
foreach (int id in new[] { 1, 3, 11 })
{
Product product = productDao.GetById(id)!;
Console.WriteLine( quot;{product.ProductName}: {priceService.PriceToday(id):0.00}");
}
public class Product
{
public int ProductId { get; set; }
public string ProductName { get; set; } = "";
public int CategoryId { get; set; }
public decimal UnitPrice { get; set; }
}
// Data access: gets products, and nothing else.
public class ProductDao
{
private readonly List<Product> products = new List<Product>
{
new Product { ProductId = 1, ProductName = "Chai", CategoryId = 1, UnitPrice = 18.00m },
new Product { ProductId = 3, ProductName = "Aniseed Syrup", CategoryId = 2, UnitPrice = 10.00m },
new Product { ProductId = 11, ProductName = "Queso Cabrales", CategoryId = 4, UnitPrice = 21.00m }
};
public virtual Product? GetById(int productId)
{
return products.Find(p => p.ProductId == productId);
}
}
// Service: the business rule, in one place.
public class PriceService
{
private const int SaleCategoryId = 1;
private const decimal SaleDiscount = 0.10m;
private readonly ProductDao productDao;
public PriceService(ProductDao productDao)
{
this.productDao = productDao;
}
public decimal PriceToday(int productId)
{
Product product = productDao.GetById(productId)!;
if (product.CategoryId == SaleCategoryId)
{
return product.UnitPrice * (1 - SaleDiscount);
}
return product.UnitPrice;
}
}
Chai: 16.20
Aniseed Syrup: 10.00
Queso Cabrales: 21.00
(products.Find(p => p.ProductId == productId) finds the first product whose id matches. The => is a lambda, a small inline method: LINQ covers them properly.)
In a web app, the controller asks for the service in its constructor, the service asks for the DAO, and the DAO asks for the connection string. Each layer gets the one below from outside.
Why it pays
- Each class is small, with one job. A price rule changes in one file, the service.
- Each layer can be tested alone. Give
PriceServicea fake DAO with known products, and test the sale rule without a database. - Layers can be swapped. Move from ADO.NET to Dapper, and only the data access layer changes; services and controllers never notice.
Check yourself
1 question to try on the lesson page.
So what
Controllers for the web, services for the rules, DAOs for the data, models to carry it: when you know which layer a piece of code belongs to, you know which file to open, and which class to test.
Key terms
- Layered Architecture
- Business Logic