Terug naar lijst

Toen ik zelf begon met programmeren pakte ik alles heel pragmatisch aan; als het werkt en de code enigszins leesbaar is, is het voldoende. Toentertijd waren controllers van 150+ regels niets geks voor mij. Door de jaren heen heb ik mij verdiept in code practices en het onderdeel ‘seperation of concerns’ komt daar heel duidelijk in naar voren. Tegenwoordig lukt het om controllers te schrijven die ergens tussen 10 tot 30 regels bevatten. Dit komt natuurlijk niet doordat ik code magisch magisch laat verdwijnen, maar door de applicatie gestructureerd op te bouwen en door code niet écht in de controller thuis hoort te verplaatsen naar een aparte classe. In deze blog laat ik zien hoe ik dat voor twee verschillende situaties aanpak; het ophalen van de gegevens en het mappen van de gegevens.

Ik begin met een voorbeeld van hoe het niet moet. Onderstaand stuk code bevat alle code die een gemiddelde edit actie pagina ook bevat. Deze code is voor deze blog bewust heel simpel gehouden, het draait hier hoofdzakelijk om het ophalen van een AppleTree object en deze te mappen van/naar een viewmodel.

public IActionResult Edit()
{
    AppleTree tree = _context.Trees
        .Include(t => t.Roots)
        .Include(t => t.Branches.Select(b => b.Leaves))
        .Include(t => t.Branches.Select(b => b.Apples))
        .OrderBy(t => t.Age)
        .FirstOrDefault(t => t.Color == "Brown");

    if (tree == null)
    {
        return NotFound();
    }

    AppleTreeEditViewModel model = new AppleTreeEditViewModel
    {
        Name = tree.Name,
        Location = tree.Location,
        Age = tree.Age
        // etc
    };

    return View(model);
}

[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Edit(AppleTreeEditViewModel model)
{ 
    if (!ModelState.IsValid)
    {
        return View(model);
    }

    AppleTree tree = _context.Trees
        .Include(t => t.Roots)
        .Include(t => t.Branches.Select(b => b.Leaves))
        .Include(t => t.Branches.Select(b => b.Apples))
        .OrderBy(t => t.Age)
        .FirstOrDefault(t => t.Color == "Brown");

    tree.Name = model.Name;
    tree.Location = model.Location;
    tree.Age = model.Age;
    // etc

    _context.Entry(tree).State = EntityState.Modified;
    _context.SaveChanges();

    return RedirectToAction("Details");
}

Ophalen van de Tree

Zelf maak ik gebruik van een servicelaag met daarachter eventueel een repository laag. Voor dit voorbeeld zou ik in mijn TreeService een functie ‘GetYoungestBrownTree’ maken, deze functie ziet er dan als volgt uit:

public Tree GetYoungestBrownTree()
{
    return _context.Trees
        .Include(t => t.Roots)
        .Include(t => t.Branches.Select(b => b.Leaves))
        .Include(t => t.Branches.Select(b => b.Apples))
        .OrderBy(t => t.Age)
        .FirstOrDefault(t => t.Color == "Brown");
}

Wanneer je vervolgens een instantie van je service aanmaakt in de controller kan de functie als volgt worden aangeroepen:

[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Edit(AppleTreeEditViewModel model)
{
    AppleTree tree = _treeService.GetYoungestBrownTree();

    tree.Name = model.Name;
    tree.Location = model.Location;
    tree.Age = model.Age;
    // etc

    _context.Entry(tree).State = EntityState.Modified;
    _context.SaveChanges();

    return RedirectToAction("Details");
}

Deze aanpak biedt drie voordelen:

  • De code wordt afgezonderd naar een centrale plaats
  • Dit houdt de controller compact en overzichtelijk
  • Deze code kan nu worden hergebruikt

De mapping

Een ander deel dat vrijwel altijd een grote hoeveelheid ruimte in beslag neemt is het mappen tussen de entiteit(en) en het viewmodel. Door ook deze stukken code naar aparte classes te verplaatsen kun je dezelfde drie pluspunten behalen zoals bij de servicelaag. Voor dit scenario maak ik een statische classe aan om de mappers in te plaatsen. De mapping functie zelf maak ik ook statisch. Door de classes statisch te maken hoef je deze niet te instantiëren en kun je dus direct gebruiken.

public static class TreeMapper 
{ 
    public static AppleTreeEditViewModel MapAppleTreeEdit(AppleTree tree)
    {
        return new AppleTreeEditViewModel
        {
            Name = tree.Name,
            Location = tree.Location,
            Age = tree.Age
            // etc
        };
    }

    public static AppleTree MapAppleTreeEdit(AppleTree tree, AppleTreeEditViewModel model)
    {
        tree.Name = model.Name;
        tree.Location = model.Location;
        tree.Age = model.Age;
        // etc

        return tree;
    }
}

Conclusie

Door beide methodes toe te passen kun je de code in je controllers aanzienlijk verminderen om zo je controllers overzichtelijk te houden. Natuurlijk zijn controllers in veel gevallen een stuk complexer dan in dit voorbeeld, maar hoe dan ook zijn deze methodes bijna altijd toe te passen. Als afsluiting van deze blog volgt de aangepaste code uit het begin van deze blog.

public IActionResult Edit()
{
    AppleTree tree = _treeService.GetYoungestBrownTree();

    if (tree == null)
    {
        return NotFound();
    }

    AppleTreeEditViewModel model = TreeMapper.MapAppleTreeEdit(tree);

    return View(model);
}

[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Edit(AppleTreeEditViewModel model)
{
    if (!ModelState.IsValid)
    {
        return View(model);
    }

    AppleTree tree = _treeService.GetYoungestBrownTree();

    tree = TreeMapper.MapAppleTreeEdit(tree, model);

    _treeService.Update(tree);

    return RedirectToAction("Details");
}