Der lise Blog:
Einsichten, Ansichten, Aussichten

Schichtentrennung bei einer .NET Core Web API

Im Softwareentwickler-Alltag kommt es des Öfteren vor, dass man die Aufgabe hat, eine Web API zu entwickeln. Dazu kann man eine Reihe von Technologien und Methoden nutzen, die zum Ziel führen. In diesem Blogartikel wollen wir zeigen, wie man dies mit .NET Core, Entity Framework Core, dem Repository Pattern und Services machen kann, die die Geschäftslogik enthalten. Ziel ist es, eine saubere Schichtentrennung zu realisieren und die Anwendung leicht erweiterbar und testbar zu implementieren.

Anwendungs-Szenario

Zur Veranschaulichung nehmen wir folgendes Anwendungs-Szenario: Ein Softwarehaus bietet seinen Mitarbeitern verschiedene kostenlose Lebensmittel an, um sie fit und bei Laune zu halten. Das sind typischerweise Kaffee, Mett oder Bier. Mit der Web API sollen diese Lebensmittel verwaltet werden, sodass die Mitarbeiter einen Überblick haben, was es gibt und sich entsprechend etwas aussuchen. Da während der Arbeitszeit natürlich kein Bier getrunken werden darf, gibt es die Einschränkung, dass dieses erst ab 17 Uhr zu Feierabend getrunken werden darf.

Umsetzung

Nun werden die einzelnen Komponenten der Web API beleuchtet, sodass der Weg der Daten vom Endpunkt bis zur Datenbank bzw. umgekehrt nachvollzogen werden kann. Siehe Abb. 1

Datenmodell

Das Anwendungs-Szenario und somit auch die Applikation sind bewusst simpel gehalten und dementsprechend besteht das Datenmodell aus nur einer Entität nämlich der Klasse Product (siehe Abb. 2) mit einer Id, einem Namen und einer Boolschen Eigenschaft, die angibt, ob das Lebensmittel erst nach Feierabend zur Verfügung steht.

 

public class Product

{

       public int Id { get; set; }

       public string Name { get; set; }

       public bool RestrictedAccess { get; set; }

}

Abb. 2

 

Datenzugriff

Und schon sind wir beim Datenzugriff. Für dieses Beispiel gehen wir davon aus, dass die Daten in einer Datenbank verwaltet werden. Das bedeutet, dass die Lebensmittel als Products in dieser Datenbank liegen. Nun müssen wir die Möglichkeit haben, Daten abzufragen bzw. zu schreiben. Da uns aber erstmal egal ist, woher die Daten kommen und wir keine harte Abhängigkeit von der Datenbank haben möchten, wird das Repository Pattern eingesetzt. Dazu wird ein generisches Interface implementiert, das die Methoden zum Lesen und Schreiben der Daten bereitstellt. In Abb. 3 ist ein Ausschnitt zu sehen. Dadurch, dass das Interface generisch ist, kann es für beliebige Entitäten, also auch andere Klassen außer Product genutzt werden.

 

public interface IRepository<TEntity> where TEntity : class

{

TEntity Add(TEntity entity);

                  TEntity Get(int id);

                  IEnumerable<TEntity> Find(Expression<Func<TEntity, bool>> predicate);

                  […]

}

Abb. 3

 

Die Implementierung der Schnittstelle erfolgt in der ebenfalls generischen Klasse DatabaseRepository<TEntity>. Dies ist also eine konkrete Implementierung für den Zugriff auf eine Datenbank, weshalb in diesem Repository Entity Framework Core zum Einsatz kommt. In dem Ausschnitt in Abb. 4 sieht man die Implementierung für die Add-Methode und den Konstruktor, der den Datenbankkontext entgegennimmt.

 

public class DatabaseRepository<TEntity> : IRepository<TEntity> where TEntity : class

{

       protected readonly DemoDbContext DatabaseContext;


       public DatabaseRepository(DemoDbContext dbContext)

       {

              DatabaseContext = dbContext;

       }


       public TEntity Add(TEntity entity)

       {

              var result = DatabaseContext.Set<TEntity>().Add(entity);

              DatabaseContext.SaveChanges();

              return result.Entity;

       }



       [...]

}


Abb. 4

 

Mit Hilfe der Schnittstelle und der konkreten Implementierung, kann nun mit der Datenbank kommuniziert werden. Wir haben bis jetzt also die Datenhaltung, das Datenmodell und den Datenzugriff abgedeckt. Wichtig ist, dass bis zu diesem Zeitpunkt noch keine Logik implementiert wurde.

Geschäftslogik

Die nächsthöhere Schicht ist die Geschäftslogik. In dieser Schicht gibt es einen ProductService, der das Interface IProductService implementiert (siehe Abb. 5). Der ProductService enthält die Logik, die steuert, welche Lebensmittel zur Verfügung stehen. Dies geschieht mittels der Konstante RestrictedAccessHour, falls es noch nicht 17 Uhr ist. Hier wird auch deutlich, dass das die Produkt Repository-Schnittstelle per Dependency Injection im Konstruktor übergeben wird, wodurch der ProductService losgelöst vom eigentlichen Datenzugriff ist. Man könnte die Lebensmittel auch in einer XML-Datei ablegen und auf die Datenbank verzichten und müsste dazu nur ein XML-Datei-Repository implementieren und stattdessen dieses in den ProductService übergeben, ohne diesen anpassen zu müssen.

 

public class ProductService : IProductService

{

       private const int RestrictedAccessHour = 17;

       private readonly IRepository<Product> _productRepository;



       public ProductService(IRepository<Product> productRepository)

       {

              _productRepository = productRepository;

       }


       public IEnumerable<Product> GetFilteredProducts()

       {

              return DateTime.Now.Hour < RestrictedAccessHour ? _productRepository.Find(item => !item.RestrictedAccess) : _productRepository.GetAll();

       }


       public Product GetProduct(int id)

       {

              var product = _productRepository.Get(id);


              return DateTime.Now.Hour < RestrictedAccessHour && product.RestrictedAccess ? throw new CustomException(System.Net.HttpStatusCode.BadRequest, ErrorCode.TooEarlyForThisProduct) : product;

       }

}

Abb. 5

 

API Schicht

Nun fehlt nur noch die API-Schicht, in der die Endpunkte der API bereitgestellt werden. .NET Core nutzt hier sogenannte Controller, die die entsprechenden HTTP Requests entgegennehmen. Vielmehr sollte ein Endpunkt in einem Controller auch nicht machen. Er nimmt den Request entgegen und leitet ihn an den entsprechenden Service in der Geschäftslogikschicht weiter. Hier also den ProductService, dessen Interface ebenfalls per Dependency Injection in den Controller übergeben wird, wodurch der Controller wiederum von der Geschäftslogik losgelöst ist. In Abb. 6 ist ein Ausschnitt des Controller Codes abgebildet und man sieht, dass der Controller den Request entgegennimmt, den Service nach den Produkten fragt und nach einem Mapping diese Produkte an den Client als Antwort ausliefert.

 

public class ProductController

{

       private readonly IProductService _productService;

       private readonly IMapper _mapper;


       public ProductController(IProductService productService, IMapper mapper)

       {

              _productService = productService;

              _mapper = mapper;

       }


       [Route("Products")]

       [HttpGet]

       public IActionResult GetProducts()

       {

              var filteredProducts =_productService.GetFilteredProducts();


              var mappedProducts = _mapper.Map<IEnumerable<DomainModel.Product>>(filteredProducts);


              return new OkObjectResult(mappedProducts);

       }

       [...]

}

Abb. 6

 

Fazit

Wir sehen anhand eines einfachen Beispiels, wie man in einer Web API die einzelnen Schichten gut voneinander trennen kann und auf diesem Weg eine lose Kopplung erreicht. Dies hilft sehr bei der Erweiterbarkeit einer Applikation und ebenso beim Testen der Komponenten.

Nun ist also sichergestellt, dass die Mitarbeiter mit Lebensmitteln versorgt werden, es das wohlverdiente Bier aber erst zu Feierabend gibt, um damit die letzten Bissen Mett herunterzuspülen. Prost!

Diesen Artikel weiterempfehlen

 Teilen  Teilen  Teilen  Teilen