From layers to vertical slices

Simplify your code and focus on your features

Jon Hilton

https://practicaldotnet.io

https://jonhilton.net

@jonhilt

"The Matrix" Copyright © 1999 Warner Bros

Could you just... ?

@jonhilt

@jonhilt

@jonhilt

@jonhilt

@jonhilt

"We need to find Mr Anderson..."

@jonhilt

"The Matrix" Copyright © 1999 Warner Bros

First Name Last Name(s) Last Slept Threat Level
Jon Hilton Today Harmless
Brian Blessed 2 Days Ago A bit loud
Not The One ? ?
The lady In the Red Dress Never Simulation

@jonhilt

?

ASP.NET/

Presentation

PersonController

ListAll

@jonhilt

ASP.NET

PersonController

ListAll

ListAll

PersonService

ListAll

PersonRepository

Business Logic

Data Access

@jonhilt

@jonhilt

public class PersonController : Controller
{
    private readonly PersonService personService;

    public PersonController(PersonService personService)
    {
        this.personService = personService;
    }

    [HttpGet]
    public async Task<IActionResult> List()
    {
        var allPeople = personService.ListAll();
        return View(allPeople);
    }
}

@jonhilt

public class PersonService
{        
    public List<Person> ListAll()
    {
        return new List<Person>
        {
            new Person
            {
                FirstName = "Neo", 
                LastName = "Anderson", 
                LastSlept = new DateTime(2021, 1, 1, 10, 15, 0),
                ThreatLevel = "Inconsequential"
            }
        };
    }

}

@jonhilt

@model System.Collections.Generic.List<NeoFindr.Controllers.Person>


<table class="table table-striped">
    <thead>
    <tr>
        <th>First Name</th>
        <th>Last Name</th>
        <th>Last Slept</th>
        <th>Threat Level</th>
    </tr>
    </thead>
    <tbody>
    @foreach (var person in Model)
    {
        <tr>
            <td>@person.FirstName</td>
            <td>@person.LastName</td>
            <td>@person.LastSlept</td>
            <td>@person.ThreatLevel</td>
        </tr>
    }
    </tbody>
</table>

@jonhilt

@jonhilt

"Conceptually, a Repository encapsulates the set of objects persisted in a data store and the operations performed over them, providing a more object-oriented view of the persistence layer.

 

Repository also supports the objective of achieving a clean separation and one-way dependency between the domain and data mapping layers."

Patterns of Enterprise Application Architecture by Martin Fowler et al

@jonhilt

@jonhilt

public class PersonService
{        
    public List<Person> ListAll()
    {
        return new List<Person>
        {
            new Person
            {
                FirstName = "Neo", 
                LastName = "Anderson", 
                LastSlept = new DateTime(2021, 1, 1, 10, 15, 0),
                ThreatLevel = "Inconsequential"
            }
        };
    }

}

@jonhilt

public class PersonRepository
{
    public List<Person> ListAll()
    {
        return new List<Person>
        {
            new Person
            {
                FirstName = "Neo",
                LastName = "Anderson",
                LastSlept = new DateTime(2021, 1, 1, 10, 15, 0),
                ThreatLevel = "Inconsequential"
            }
        };
    }
}
public class PersonService
{
    public PersonService(PersonRepository personRepository)
    {
        this.personRepository = personRepository;
    }
	
    public List<Person> ListAll()
    {
        return personRepository.ListAll();
    }
}

"adding this layer helps minimise duplicate query logic..."

Patterns of Enterprise Application Architecture by Martin Fowler et al

@jonhilt

"There are too many people to work through; we need a way to search"

@jonhilt

"The Matrix" Copyright © 1999 Warner Bros

First Name Last Name(s) Last Slept Threat Level
The One And Only Today ?
Maybe The One 1 Hour Ago Always asleep
Not The One ? ?
Thomas Anderson Yesterday Presumably Harmless

Search term

Showing 4 of 8 results...

The one

@jonhilt

ASP.NET/

Presentation

PersonController

ListAll

ListAll

PersonService

ListAll

PersonRepository

Business Logic

Data Access

@jonhilt

Search

Search

Developers are LAZY...

(in a good way!)

Developers are LAZY...

People

https://www.behaviormodel.org/

ASP.NET/

Presentation

PersonController

ListAll

ListAll

PersonService

ListAll

PersonRepository

Business Logic

Data Access

Search

Search

@jonhilt

@jonhilt

public class PersonController : Controller
{
    private readonly PersonService personService;

    public PersonController(PersonService personService)
    {
        this.personService = personService;
    }
    
    [HttpGet]
    public async Task<IActionResult> List()
    {
        var allPeople = personService.ListAll();
        return View(allPeople);
    }

    [HttpGet]
    public async Task<IActionResult> Search(string searchTerm)
    {
        var results = personService.Search(searchTerm);
        return View("List", results);
    }
}

@jonhilt

public class PersonService
{
    private PersonRepository personRepository;

    public PersonService(PersonRepository personRepository)
    {
        this.personRepository = personRepository;
    }
    
    public List<Person> ListAll()
    {
        return personRepository.ListAll();
    }

    public List<Person> Search(string searchTerm)
    {
        return personRepository.ListAll(searchTerm);
    }
}

@jonhilt

public class PersonRepository
{
    public List<Person> ListAll(string searchTerm = "")
    {
        if (!string.IsNullOrEmpty(searchTerm))
        {
            // pass query to DB...
        }
        else
        {
            // return everything?
        }
        
        return new List<Person>
        {
            new Person
            {
                FirstName = "Neo", 
                LastName = "Anderson", 
                LastSlept = new DateTime(2021, 1, 1, 10, 15, 0),
                ThreatLevel = "Inconsequential"
            }
        };
    }
}

"This search is broken; it keeps showing us agents as well as inhabitants.

Show us the agents separately!!"

@jonhilt

"The Matrix" Copyright © 1999 Warner Bros

"Sentient programs. They can move in and out of any software still hardwired to the system. That means that anyone we haven't unplugged...is potentially an Agent. Inside the Matrix, they are everyone...and they are no one."

First Name Last Name Uptime
Agent Smith 6 years
Agent Jackson 2 years
Agent Thompson 3 months

Agent List

@jonhilt

PersonController

ListAll

ListAll

PersonService

ListAll

PersonRepository

Search

Search

AgentController

ListAll

AgentService

ListAll

@jonhilt

if(person.Uptime != null){
  // do stuff
}

if(person.FirstName == "Agent"){
  // do stuff
}

@jonhilt

PersonController

ListAll

ListAll

PersonService

PersonRepository

Search

AgentController

ListAll

AgentService

Add

Search

ListAll

Add

Add

ListAll

Destroy

Destroy

Destroy

Decommission

Decommission

@jonhilt

BIG classes

Lots o' references

  • Suddenly, people actually means Inhabitants (at service and controller level)
     
  • The person repository is used for both agents and inhabitants (are both, either, or non actually people?)
     
  • Conditional logic to figure out which "mode" we're in (destroying inhabitants or decommissioning agents)

 

Domain Model?

What Domain Model?

@jonhilt

Helpers

Mappers

Managers

Factories

ServiceFactories

Functions

Requests

Responses

Models

ViewModels

DALS

Readers

Repositories

ReadOnlyRepositories

FactoryFactories

Utilities

Death Star II from "Star Wars: Return of the Jedi"

Copyright © 1983 Lucasfilm Ltd.

Let's start over!

@jonhilt

"We need to find Mr Anderson..."

@jonhilt

"The Matrix" Copyright © 1999 Warner Bros

?

@jonhilt

Ask more questions

Inhabitants

The Matrix

Agents

Computer programs

NEVER sleep

Crash occasionally

Good uptime

@jonhilt

First Name Last Name(s) Last Slept Threat Level
Jon Hilton Today Harmless
Brian Blessed 2 Days Ago A bit loud
Not The One ? ?
The lady In the Red Dress Never Simulation

Inhabitants

@jonhilt

@jonhilt

"Traditional" MVC Folder Structure

@jonhilt

?

ASP.NET/

Presentation

InhabitantController

ListAll

ASP.NET/

Presentation

InhabitantController

ListAll

ListAll

Request (query)

Response (model)

Inhabitants

@jonhilt

@jonhilt

@jonhilt

ASP.NET/

Presentation

InhabitantController

ListAll

ListAll

Request (query)

Response (model)

Inhabitants

Search

Search

Term (string)

SearchResults

@jonhilt

@jonhilt

public class Search
{

    public class Query : IRequest<Model>
    {
        public string Term { get; set; }
    }

    public class Model
    {

        public List<SearchResult> Results { get; set; }

        public class SearchResult {
            public string FirstName { get; set; }
            public string LastName { get; set; }
            public DateTime LastSlept { get; set; }
            public string ThreatLevel { get; set; }
            public Guid Id { get; set; }
        }

    }

}

@jonhilt

public class QueryHandler : IRequestHandler<Query, Model>
{
    private readonly FindRContext _dbContext;

    public QueryHandler(FindRContext dbContext)
    {
        _dbContext = dbContext;
    }

    public async Task<Model> Handle(QueryHandler request, CancellationToken cancellationToken)
    {
        if (string.IsNullOrEmpty(request.Term))
            return new Model();

        var results = _dbContext.Inhabitants.Where(
            x => x.FirstName.contains(request.Term) || x.LastName.Contains(request.Term));

        return new Model
        {
            Results = results.Select(results => new Model.SearchResult{
                FirstName =result.FirstName,
                LastName = result.LastName,
                LastSlept = result.LastSlept,
                ThreatLevel = results.CurrentThreatLevel,
                Id = result.Id
            }).ToList();
        }
    }
}

@jonhilt

[HttpGet]
public async Task<IActionResult> Search(Search.Query query){

    var model = await _mediator.Send(query);
    return View(model);
    
}

ASP.NET/

Presentation

InhabitantController

ListAll

ListAll

Request

(command)

Inhabitants

Search

Search

Destroy

Destroy

Destroy!

public class InhabitantController : Controller
{
    private readonly IMediator _mediator;

    public InhabitantController(IMediator mediator)
    {
        _mediator = mediator;
    }

    ---

    [HttpPost]
    public async Task<IActionResult> Destroy(Destroy.Command command)
    {
        await _mediator.Send(command);
        return RedirectToAction(nameof(Search));
    }
}

Destroy!

public class Destroy
{
    public class Command : IRequest
    {
        public Guid Id { get; set; }
    }

    public class CommandHandler : AsyncRequestHandler<Command>
    {        
        protected override async Task Handle(Command request, 
           CancellationToken cancellationToken)
        {

        }
    }
}

Destroy!

public class Destroy
{
    public class Command : IRequest
    {
        public Guid Id { get; set; }
    }

    public class CommandHandler : AsyncRequestHandler<Command>
    {
        private readonly FindRContext findRContext;

        public CommandHandler(FindRContext dbContext)
        {
            findRContext = dbContext;
        }
        
        protected override async Task Handle(Command request, 
          CancellationToken cancellationToken)
        {
            var inhabitant = await findRContext.Inhabitants.FindAsync(request.Id);
            findRContext.Inhabitants.Remove(inhabitant);
            await findRContext.SaveChangesAsync(cancellationToken);
        }
    }
}

Encapsulate by feature

InhabitantController

ListAll

ListAll

Search

Search

Destroy

Destroy

InhabitantController

InhabitantController

@jonhilt

Refactor

by feature

@jonhilt

Destroy

Inhabitant
Destroy()

public void Destroy(){	
    ThreatLevel = "Extinguished";
    Expired = DateTime.Now;
    
    DomainEvents.Raise(new InhabitantDestroyedEvent
      { 
          InhabitantId = Id,
          DestroyedOn = Expired
      });    
}

Handler

Aggregate

Extend and Refactor

Handling Side Effects

Avoid links between features

(specific, business logic)

@jonhilt

PilotController

List

Pilot

Search

Retire

List

Search

Retire

AttendantController

ListBy
Rank

Add

Update
Address

List

Add

Update
Address

Flight

Attendant

Address

@jonhilt

Pilot (aggregate)

Dapper

Query

Dapper

Query

Dapper

Query

Dapper

Query

EF Core

public class Flight
{
    public List<Guid> Attendants { get; private set; }
    public Guid Pilot { get; private set; }
    public Guid CoPilot { get; private set; }
    
    public DateTime Departed { get; private set; }
    
    public void AssignAttendant(Guid Id)
    {
        this.Attendants.Add(Id);
    }
    
    public void Depart()
    {
        if (Pilot == null || CoPilot == null)
            throw new InsufficientFlightCrewException();
            
        Departed = DateTime.Now();
    }
}

Missing Models

What about tests?

@jonhilt

PersonController

ListAll

ListAll

PersonService

PersonRepository

Search

AgentController

ListAll

AgentService

Add

Search

ListAll

Add

Add

ListAll

Destroy

Destroy

Destroy

Decommission

Decommission

@jonhilt

PersonController

PersonService

PersonRepository

Destroy

Destroy

Destroy

Test?

Test in isolation

Test?

PersonController

Destroy

Destroy

Inhabitants

Unit Tests

  • Great for business logic
  • Can test aggregates
  • (no side effects!)

PersonController

Destroy

Destroy

Inhabitants

When destroying an inhabitant

They should be completely expunged from the system

Integration/Scenario Tests

The names used are those of the business domain (feature)

@jonhilt

@jonhilt

public class WhenDestroyingAnInhabitant : IntegrationTestBase
{
    [Fact]
    public async void TheyShouldBeCompletelyExpunged()
    {
        var inhabitantId = Guid.NewGuid();
        await SliceFixture.InsertAsync(new Inhabitant {Id = inhabitantId});

        await SliceFixture.SendAsync(new Destroy.Command {Id = inhabitantId});

        var savedInhabitant = await SliceFixture.FindAsync<Inhabitant>(inhabitantId);
        savedInhabitant.ShouldBeNull();
    }
}

5 Practical Tips

  1. Ask more questions! (to understand what you're trying to build)

  2. Avoid organising by layers or technical concerns (especially early on)

  3. Embrace Command Query Separation

  4. Leverage MediatR (start your features off on the right foot)

  5. Refactor on a per feature basis

  6. (BONUS) Use Domain Events to decouple parts of your system

There is no "best" design.

There is only the "best design given our current understanding"

Jimmy Bogard -  "Strengthening your domain"

https://bit.ly/NoBestDesign

Examples in the wild

Humanitarian Toolbox - AllReady

Jimmy Bogard - Contoso University

https://github.com/jbogard/ContosoUniversityDotNetCore

Tackling Business Complexity in a Microservice with DDD and CQRS Patterns

@jonhilt

jonhilton.net/slices

 

jon@jonhilton.net

@jonhilt

From layers to vertical slices 2.0

By jonhilt

From layers to vertical slices 2.0

  • 435