24 - BLL
Business Logic Layer
Refresher
Refresher of architecture so far
- DOMAIN
- SQL – Database/Tables
- EF – DbContext/DbSets
- DAL – UOW/Repositories
- MVC – Controllers/Views (Or rest-api endpoints)
Repository
Using repository/UOW directly in controller
- Speedy development
- Easy to read
To think about
- Business logic in controller
- Multiple repositories used
- Maintenance in bigger team, in bigger project, over several major iterations
More issues with repositories
- What are we returning? Domain models?
- How to control the amount of data returned?
- We have done the MVC web, now we need to do the Rest services – duplicate business logic in controllers
- Validations, logging, versioning – where these go? Controllers?
Clean controllers
- Over time functionality starts to pile up
- Logic just grows and grows
- Controller starts to contain more and more business rules
- Maintainability is lost
- No more clean controllers
Example — fat controller doing everything:
public class PersonController : Controller
{
private readonly IAppUOW _uow;
public PersonController(IAppUOW uow) => _uow = uow;
[HttpPost]
public async Task<IActionResult> Create(PersonCreateViewModel vm)
{
if (!ModelState.IsValid) return View(vm);
// validation — business rule buried in controller
if (await _uow.PersonRepo.ExistsWithEmailAsync(vm.Email))
return View(vm); // duplicate check
// mapping — controller knows about entity structure
var entity = new Person
{
FirstName = vm.FirstName,
LastName = vm.LastName,
Email = vm.Email
};
// data access
_uow.PersonRepo.Add(entity);
await _uow.SaveChangesAsync();
// side effect — controller sends email
await _emailSender.SendWelcome(entity.Email);
return RedirectToAction(nameof(Index));
}
}
Problems: validation, mapping, data access, side effects — all in one method. Now imagine duplicating this for the REST API controller.
BLL and clean controllers
- BLL – Business Logic Layer
- Important layer between presentation layer and DAL/UOW/Repositories
- Generally, controllers only use BLL/Services
- UOW/Repo is responsible for data, BLL/Service for everything else
- End result: clean controllers, reusable business logic
The same operation with BLL — thin controller:
public class PersonController : Controller
{
private readonly IPersonService _personService;
public PersonController(IPersonService personService) => _personService = personService;
[HttpPost]
public async Task<IActionResult> Create(PersonCreateViewModel vm)
{
if (!ModelState.IsValid) return View(vm);
await _personService.CreateAsync(vm.FirstName, vm.LastName, vm.Email);
return RedirectToAction(nameof(Index));
}
}
Controller does three things: receive input, call service, pick result. Business logic lives in IPersonService, reusable by both MVC and REST API controllers.
DAO, Repository, Service
Sometimes you need even more granularity in your data access – Data Access Objects are often used as low-level, single technology based data source. And Repository is used as aggregate over different data sources – database and rest-api based data sources
BLL – Services
- One service can use as many repos as needed
- Also access other services
- Data validation and access is moved from controllers to the service layer
- Data caching is more effective
BLL - Service
What should be in service layer?
- Using multiple repos to compose one response
- Doing something other than direct repo usage
- Storing uploaded data to filesystem
- Requests from external sources
- Business rule validation
- Coordinating transactions (calling UOW.SaveChangesAsync)
BLL - Service interface and implementation
Define what the service can do — interface:
public interface IPersonService
{
Task<IEnumerable<PersonBllDto>> GetAllAsync();
Task<PersonBllDto?> GetByIdAsync(Guid id);
Task<PersonBllDto> CreateAsync(string firstName, string lastName, string email);
Task UpdateAsync(Guid id, string firstName, string lastName);
Task DeleteAsync(Guid id);
}
Implement it using UOW/repos:
public class PersonService : IPersonService
{
private readonly IAppUOW _uow;
public PersonService(IAppUOW uow)
{
_uow = uow;
}
public async Task<IEnumerable<PersonBllDto>> GetAllAsync()
{
var entities = await _uow.PersonRepo.GetAllAsync();
return entities.Select(PersonBllDtoFactory.CreateDto);
}
public async Task<PersonBllDto?> GetByIdAsync(Guid id)
{
var entity = await _uow.PersonRepo.GetByIdAsync(id);
return entity == null ? null : PersonBllDtoFactory.CreateDto(entity);
}
public async Task<PersonBllDto> CreateAsync(string firstName, string lastName, string email)
{
// business rule — service owns the validation
if (await _uow.PersonRepo.ExistsWithEmailAsync(email))
throw new BusinessRuleException("Email already in use");
var entity = new Person
{
FirstName = firstName,
LastName = lastName,
Email = email
};
_uow.PersonRepo.Add(entity);
await _uow.SaveChangesAsync(); // service coordinates the transaction
return PersonBllDtoFactory.CreateDto(entity);
}
public async Task DeleteAsync(Guid id)
{
await _uow.PersonRepo.RemoveAsync(id);
await _uow.SaveChangesAsync();
}
// ...
}
Register in DI container (Program.cs):
builder.Services.AddScoped<IPersonService, PersonService>();
Now both MVC controller and REST API controller use the same IPersonService — no duplicated business logic.
What does NOT belong in BLL
HttpContext,Request,Response— those are presentation concernsIActionResult,ViewResult— service returns data, not HTTP responses- References to
Microsoft.AspNetCore.*namespaces - Direct UI/view logic
The service layer should be framework-agnostic. If you can't unit test your service without spinning up ASP.NET Core — something is wrong.
Returning data and domain models
- Using Domain models everywhere
- Fast
- Everything done in Domain models is instantly accessible/replicated everywhere
- Problems
- Everything is visible
- How to control the amount of data returned?
- Changing Domain also changes field names in service
- Problems in serializing (circular references etc.)
- Just too much data is transferred in every layer – starting with db
DTO – Data Transfer Object 1
- Goal
- Reduce data complexity and data quantity in between clients and rest service
- Control over data
- Flexibility
- Do not confuse DTOs with Domain classes
- DTOs only contain minimum number of properties, typically no computed fields or methods
DTO – Data Transfer Object 2
- As FLAT as possible, no unnecessary relationships with other classes
- Possibility to add info needed by rest client
- DTO does not have to match 1:1 with Domain object
- Gives you 100% control over what is going on in your rest service
- Helps, when domain models and database changes, but rest services must stay the same
- Mainly only properties!
DTO – Data Transfer Object 3
How many layers of objects do we need?
Typically at least 3!
- Database/Domain entities (sometimes split further into separate DB entities and Domain objects)
- DTOs for internal usage – from repos/DAL to BLL and between services
- DTOs for external usage – from public REST controllers to clients (ViewModels in MVC regular web world)
And sometimes even more...
- Internal objects can be changed easily, everything is under your own control
- External objects should almost never change — use versioning.
DTO – Mapping
Mapping between domain entities and DTOs can be done with:
- Manual mapping / Factory methods — explicit, easy to debug, full control (recommended for this course)
- AutoMapper — convention-based, less code, but hides complexity. See lecture 23 for details. If mapping gets complex, switch to manual mapping.

DTO – Factory
Problem – initializing DTOs all over your code reduces maintainability
- Different methods start to do it differently
- Maintainability is lost, method A changes DTO creation logic, you will forget to update method B
Solution — Factory pattern. Create a class that centralizes DTO creation:
public static class PersonBllDtoFactory
{
public static PersonBllDto CreateDto(Person entity)
{
return new PersonBllDto
{
Id = entity.Id,
FirstName = entity.FirstName,
LastName = entity.LastName,
Email = entity.Email,
ContactCount = entity.Contacts?.Count ?? 0
};
}
public static Person CreateEntity(PersonBllDto dto)
{
return new Person
{
Id = dto.Id,
FirstName = dto.FirstName,
LastName = dto.LastName,
Email = dto.Email
};
}
}
All mapping code in one location — when the domain model changes, you update the factory, not every method that creates DTOs.
BLL enables testing
With business logic extracted into services behind interfaces, unit testing becomes straightforward — no HTTP pipeline, no database needed:
[Fact]
public async Task CreateAsync_DuplicateEmail_ThrowsException()
{
var mockUow = new Mock<IAppUOW>();
var mockRepo = new Mock<IPersonRepo>();
mockRepo.Setup(r => r.ExistsWithEmailAsync("test@test.com"))
.ReturnsAsync(true);
mockUow.Setup(u => u.PersonRepo).Returns(mockRepo.Object);
var service = new PersonService(mockUow.Object);
await Assert.ThrowsAsync<BusinessRuleException>(
() => service.CreateAsync("Juku", "Tamm", "test@test.com"));
}
Without BLL, this validation logic would be inside the controller — much harder to test.
OVERVIEW
Is this structure always necessary?
- No, it depends on the complexity of application
Doesn't it take too much time to implement?
- If the lifecycle of your app is longer, then everything is much faster after initial release
- NB! Every web service might have 1+N clients. The less we change our public services, the better.
- It's scary/complex only on the first try (ok, first few tries)
Why do we need it?
- To change our internal logic without touching public services.
- Development cycle is more effective in bigger teams.
- First DTOs and services for front-end team – and then we can work in parallel tracks with them on the back-end.
Self preparation QA
Be prepared to explain topics like these:
- What is the "fat controller" antipattern and how does BLL solve it? — Fat controllers contain validation, mapping, data access, and side effects all in one method. BLL extracts business logic into services, making controllers thin. The same service is reused by both MVC and REST API controllers.
- Why should the service layer be framework-agnostic? — Services should not reference
HttpContext,IActionResult, orMicrosoft.AspNetCore.*. Framework-agnostic services are testable, reusable, and portable. - Why use at least three layers of DTOs? — Domain entities (database), BLL DTOs (internal), and public DTOs (API/ViewModels). Internal objects can change freely; external objects should almost never change to avoid breaking clients.
- Why use a DTO factory instead of inline mapping? — A factory centralizes DTO creation. When the domain model changes, you update the factory, not every method that creates DTOs.
- What belongs in the service layer vs. what does not? — Belongs: business rule validation, coordinating repositories, side effects, transaction coordination. Does NOT belong: HttpContext access, IActionResult returns, view logic.
- Why does BLL enable testing? — Business logic behind interfaces can be tested by mocking the UoW/repository. No HTTP pipeline, no database, no network needed.