N-tier architecture in .NET: a layered BLL/DAL example in C# and its problems

.NET Architecture & Design Patterns November 13, 2026 csharp

Layer = logical separation in the code.

csharp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// ======================================================================
// Q: Show me an N-tier application in .NET. What's the problem with it?
//
// Layer = logical separation in the code. Tier = physical separation (a
// different process or server). The classic 3-tier setup: browser, application
// server, database server. In practice, "N-tier" usually means the layers.
// - Problem 1: the business logic depends on the DAL. Changing the database or
//   ORM affects the BLL.
// - Problem 2: unit testing OrderManager without a database is hard (concrete
//   repository).
// - Problem 3: anemic entities: the rules are scattered across managers, and
//   Status is changed from everywhere.
// - Advantage: simple, everyone understands it. For CRUD without special rules
//   it is enough.
// ======================================================================

// Classic N-tier layering: Web -> BLL -> DAL
Shop.Web  →  Shop.BLL  →  Shop.DAL  →  SQL
(controllers)  (managers)    (DbContext, entities, repositories)

// DAL: EF entities and repositories
public class OrderRepository(ShopDbContext db)
{
    public Task<OrderEntity?> GetAsync(int id) =>
        db.Orders.Include(o => o.Lines).FirstOrDefaultAsync(o => o.Id == id);
    public Task SaveAsync() => db.SaveChangesAsync();
}

// BLL: depends directly on the DAL and the EF entities
public class OrderManager(OrderRepository repo)
{
    public async Task CancelAsync(int id)
    {
        var order = await repo.GetAsync(id) ?? throw new NotFoundException();
        if (order.Status == "Shipped") throw new BusinessException("Already shipped");
        order.Status = "Cancelled";      // anemic entity: anyone can change it
        await repo.SaveAsync();
    }
}

// Presentation
[HttpPost("{id}/cancel")]
public Task Cancel(int id) => orderManager.CancelAsync(id);