Ich werde auch versuchen, die Kommentare des ersten Teils zu berücksichtigen.
Zwiebelarchitektur
Angenommen, wir entwerfen eine Anwendung, um aufzuzeichnen, welche Bücher wir gelesen haben. Aus Gründen der Genauigkeit möchten wir jedoch auch aufzeichnen, wie viele Seiten gelesen wurden. Wir wissen, dass dies ein persönliches Programm ist, das wir auf unserem Smartphone benötigen, z. B. ein Bot für Telegramme und möglicherweise für den Desktop. Wählen Sie daher diese Architekturoption:
(Tg Bot, Telefon-App, Desktop) => Asp.net Web Api => Datenbank
Erstellen Sie in Visual Studio ein Projekt vom Typ Asp.net Core, wobei Sie weiter den Projekttyp Web Api auswählen.
Wie unterscheidet es sich von dem üblichen?
Erstens erbt die Controller-Klasse von der ControllerBase-Klasse, die als Basisklasse für MVC ohne Unterstützung für die Rückgabe von Ansichten (HTML-Code) konzipiert ist.
Zweitens sollen REST-Services implementiert werden, die alle Arten von HTTP-Anforderungen abdecken, und als Antwort auf Anforderungen erhalten Sie json mit einer expliziten Angabe des Antwortstatus. Außerdem werden Sie sehen, dass der Standard-Controller mit dem Attribut [ApiController] gekennzeichnet ist, das nützliche Optionen speziell für die API enthält.
Jetzt müssen Sie entscheiden, wie die Daten gespeichert werden sollen. Da ich weiß, dass ich nicht mehr als 12 Bücher pro Jahr lese, reicht mir die CSV-Datei, die die Datenbank darstellt.
Also erstelle ich eine Klasse, die das Buch beschreibt:
Book.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace WebApiTest
{
public class Book
{
public int id { get; set; }
public string name { get; set; }
public string author { get; set; }
public int pages { get; set; }
public int readedPages { get; set; }
}
}
Und dann beschreibe ich die Klasse für die Arbeit mit der Datenbank:
CsvDB.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WebApiTest
{
public class CsvDB
{
const string dbPath = @"C:\\csv\books.csv";
private List<Book> books;
private void Init()
{
if (books != null)
return;
string[] lines = File.ReadAllLines(dbPath);
books = new List<Book>();
foreach(var line in lines)
{
string[] cells = line.Split(';');
Book newBook = new Book()
{
id = int.Parse(cells[0]),
name = cells[1],
author = cells[2],
pages = int.Parse(cells[3]),
readedPages = int.Parse(cells[4])
};
books.Add(newBook);
}
}
public int Add(Book item)
{
Init();
int nextId = books.Max(x => x.id) + 1;
item.id = nextId;
books.Add(item);
return nextId;
}
public void Delete(int id)
{
Init();
Book selectedToDelete = books.Where(x => x.id == id).FirstOrDefault();
if(selectedToDelete != null)
{
books.Remove(selectedToDelete);
}
}
public Book Get(int id)
{
Init();
Book book = books.Where(x => x.id == id).FirstOrDefault();
return book;
}
public IEnumerable<Book> GetList()
{
Init();
return books;
}
public void Save()
{
StringBuilder sb = new StringBuilder();
foreach(var book in books)
sb.Append($"{book.id};{book.name};{book.author};{book.pages};{book.readedPages}");
File.WriteAllText(dbPath, sb.ToString());
}
public bool Update(Book item)
{
var selectedBook = books.Where(x => x.id == item.id).FirstOrDefault();
if(selectedBook != null)
{
selectedBook.name = item.name;
selectedBook.author = item.author;
selectedBook.pages = item.pages;
selectedBook.readedPages = item.readedPages;
return true;
}
return false;
}
}
}
Dann ist die Sache klein, um die API hinzuzufügen, um mit ihr interagieren zu können:
BookController.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
namespace WebApiTest.Controllers
{
[ApiController]
[Route("[controller]")]
public class BookController : ControllerBase
{
private CsvDB db;
public BookController()
{
db = new CsvDB();
}
[HttpGet]
public IEnumerable<Book> GetList() => db.GetList();
[HttpGet("{id}")]
public Book Get(int id) => db.Get(id);
[HttpDelete("{id}")]
public void Delete(int id) => db.Delete(id);
[HttpPut]
public bool Put(Book book) => db.Update(book);
}
}
Und dann müssen Sie nur noch die Benutzeroberfläche hinzufügen, was praktisch wäre. Und alles funktioniert!
Cool! Aber nein, die Frau hat darum gebeten, dass sie auch Zugang zu so einer bequemen Sache hat.
Welche Schwierigkeiten erwarten uns? Zuerst müssen Sie jetzt eine Spalte für alle Bücher hinzufügen, in der die Benutzer-ID angegeben ist. Vertrauen Sie mir, es wird mit einer CSV-Datei nicht angenehm sein. Außerdem müssen Sie jetzt die Benutzer selbst hinzufügen! Und selbst jetzt ist eine Art Logik erforderlich, damit meine Frau nicht sieht, dass ich Dontsovas dritte Sammlung anstelle des versprochenen Tolstoi zu Ende lese.
Versuchen wir, dieses Projekt auf die erforderlichen Anforderungen auszudehnen: Die
Möglichkeit, ein Benutzerkonto zu erstellen, mit dem eine Liste seiner Bücher geführt und die Anzahl der gelesenen Bücher hinzugefügt werden kann.
Ehrlich gesagt wollte ich ein Beispiel schreiben, aber die Anzahl der Dinge, die ich nicht tun wollte, hat den Wunsch zunichte gemacht:
Schaffung eines Controllers, der für die Autorisierung und das Senden von Daten an den Benutzer verantwortlich ist;
Erstellung eines neuen Entitätsbenutzers sowie eines Handlers dafür;
Logik entweder in den Controller selbst schieben, wodurch sie aufgebläht würde, oder in eine separate Klasse;
Umschreiben der Logik der Arbeit mit der "Datenbank", weil jetzt oder zwei CSV-Dateien, oder in die Datenbank gehen ...
Als Ergebnis haben wir einen großen Monolithen erhalten, dessen Ausdehnung sehr „schmerzhaft“ ist. Es hat eine große Anzahl von engen Links in der Anwendung. Ein fest gebundenes Objekt hängt von einem anderen Objekt ab. Dies bedeutet, dass das Ändern eines Objekts in einer eng gekoppelten Anwendung häufig das Ändern einer Reihe anderer Objekte erfordert. Dies ist nicht schwierig, wenn die Anwendung klein ist, aber es ist zu schwierig, Änderungen an einer Anwendung für Unternehmen vorzunehmen.
Schwache Bindungen bedeuten, dass zwei Objekte unabhängig sind und ein Objekt das andere verwenden kann, ohne davon abhängig zu sein. Diese Art von Beziehung zielt darauf ab, die Abhängigkeiten zwischen den Komponenten des Systems zu verringern, um das Risiko zu verringern, dass Änderungen an einer Komponente Änderungen an einer anderen Komponente erfordern.
Daher werden wir versuchen, unsere Anwendung im Onion-Stil zu implementieren, um die Vorteile dieser Methode aufzuzeigen.
Zwiebelarchitektur ist die Aufteilung einer Anwendung in Schichten. Darüber hinaus gibt es eine unabhängige Ebene, die im Zentrum der Architektur steht.
Die Zwiebelarchitektur basiert stark auf der Abhängigkeitsinversion. Die Benutzeroberfläche interagiert über Schnittstellen mit der Geschäftslogik.
Prinzip der Abhängigkeitsinversion
(Dependency Inversion Principle) , , . :
. .
. .
. .
. .
Ein klassisches Projekt in diesem Stil besteht aus vier Ebenen:
- Domänenobjektebene (Kern)
- Repository-Ebene (Repo)
- Service Level
- Front-End-Schicht (Web- / Unit-Test) (API)
Alle Schichten sind auf das Zentrum (Kern) gerichtet. Das Zentrum ist unabhängig.
Domänenobjektebene
Dies ist der zentrale Teil der Anwendung, der die Objekte beschreibt, die mit der Datenbank arbeiten.
Erstellen wir ein neues Projekt in der Lösung, das den Ausgabetyp "Klassenbibliothek" hat. Ich habe es WebApiTest.Core genannt. Erstellen
wir eine BaseEntity-Klasse, die gemeinsame Eigenschaften von Objekten hat.
BaseEntity.cs
public class BaseEntity
{
public int id { get; set; }
}
Off Top
, «id», , dateAdded, dateModifed ..
Als Nächstes erstellen wir eine Book-Klasse, die von BaseEntity erbt
Book.cs
public class Book: BaseEntity
{
public string name { get; set; }
public string author { get; set; }
public int pages { get; set; }
public int readedPages { get; set; }
}
{
public string name { get; set; }
public string author { get; set; }
public int pages { get; set; }
public int readedPages { get; set; }
}
Für unsere Anwendung wird dies vorerst ausreichen, also gehen wir zum nächsten Level über.
Repository-Ebene
Fahren wir nun mit der Implementierung der Repository-Ebene fort. Erstellen Sie ein Klassenbibliotheksprojekt mit dem Namen WebApiTest.Repo.
Wir werden Dependency Injection verwenden , um Parameter durch den Konstruktor zu übergeben, um sie flexibler zu gestalten. Daher erstellen wir eine gemeinsame Repository-Schnittstelle für Entitätsoperationen, damit wir eine lose gekoppelte Anwendung entwickeln können. Das folgende Code-Snippet ist für die IRepository-Schnittstelle.
IRepository.cs
public interface IRepository <T> where T : BaseEntity
{
IEnumerable<T> GetAll();
int Add(T item);
T Get(int id);
void Update(T item);
void Delete(T item);
void SaveChanges();
}
Implementieren wir nun eine Repository-Klasse, um Datenbankoperationen für eine Entität auszuführen, die IRepository implementiert. Dieses Repository enthält einen Konstruktor mit einem pathToBase-Parameter. Wenn wir also eine Instanz des Repositorys erstellen, übergeben wir den Dateipfad, damit die Klasse weiß, woher die Daten stammen.
CsvRepository.cs
public class CsvRepository<T> : IRepository<T> where T : BaseEntity
{
private List<T> list;
private string dbPath;
private CsvConfiguration cfg = new CsvConfiguration(CultureInfo.InvariantCulture)
{
HasHeaderRecord = false,
Delimiter = ";"
};
public CsvRepository(string pathToBase)
{
dbPath = pathToBase;
using (var reader = new StreamReader(pathToBase)) {
using (var csv = new CsvReader(reader, cfg)) {
list = csv.GetRecords<T>().ToList(); }
}
}
public int Add(T item)
{
if (item == null)
throw new Exception("Item is null");
var maxId = list.Max(x => x.id);
item.id = maxId + 1;
list.Add(item);
return item.id;
}
public void Delete(T item)
{
if (item == null)
throw new Exception("Item is null");
list.Remove(item);
}
public T Get(int id)
{
return list.SingleOrDefault(x => x.id == id);
}
public IEnumerable<T> GetAll()
{
return list;
}
public void SaveChanges()
{
using (TextWriter writer = new StreamWriter(dbPath, false, System.Text.Encoding.UTF8))
{
using (var csv = new CsvWriter(writer, cfg))
{
csv.WriteRecords(list);
}
}
}
public void Update(T item)
{
if(item == null)
throw new Exception("Item is null");
var dbItem = list.SingleOrDefault(x => x.id == item.id);
if (dbItem == null)
throw new Exception("Cant find same item");
dbItem = item;
}
Wir haben die Entität und den Kontext entwickelt, die für die Arbeit mit der Datenbank erforderlich sind.
Service Level
Jetzt erstellen wir die dritte Schicht der Zwiebelarchitektur, die Serviceschicht. Ich habe es WebApiText.Service genannt. Diese Ebene interagiert sowohl mit Webanwendungen als auch mit Repository-Projekten.
Wir erstellen eine Schnittstelle namens IBookService. Diese Schnittstelle enthält die Signatur aller Methoden, auf die die externe Ebene des Book-Objekts zugreift.
IBookService.cs
public interface IBookService
{
IEnumerable<Book> GetBooks();
Book GetBook(int id);
void DeleteBook(Book book);
void UpdateBook(Book book);
void DeleteBook(int id);
int AddBook(Book book);
}
Jetzt implementieren wir es in der BookService-Klasse
BookService.cs
public class BookService : IBookService
{
private IRepository<Book> bookRepository;
public BookService(IRepository<Book> bookRepository)
{
this.bookRepository = bookRepository;
}
public int AddBook(Book book)
{
return bookRepository.Add(book);
}
public void DeleteBook(Book book)
{
bookRepository.Delete(book);
}
public void DeleteBook(int id)
{
var book = bookRepository.Get(id);
bookRepository.Delete(book);
}
public Book GetBook(int id)
{
return bookRepository.Get(id);
}
public IEnumerable<Book> GetBooks()
{
return bookRepository.GetAll();
}
public void UpdateBook(Book book)
{
bookRepository.Update(book);
}
}
Externe Schnittstellenebene
Jetzt erstellen wir die letzte Schicht der Zwiebelarchitektur, in unserem Fall die externe Schnittstelle, mit der externe Anwendungen (Bot, Desktop usw.) interagieren. Um diese Ebene zu erstellen, bereinigen wir unser WebApiTest.Api-Projekt, indem wir die Book-Klasse entfernen und den BooksController bereinigen. Dieses Projekt bietet eine Möglichkeit für Operationen mit der Entitätsdatenbank sowie einen Controller zum Ausführen dieser Operationen.
Da das Konzept der Abhängigkeitsinjektion für eine ASP.NET Core-Anwendung von zentraler Bedeutung ist, müssen wir jetzt alles registrieren, was wir für die Verwendung in der Anwendung erstellt haben.
Abhängigkeitsspritze
In kleinen Anwendungen unter ASP.NET MVC können wir eine Klasse relativ einfach durch eine andere ersetzen, anstatt einen Datenkontext zu verwenden, sondern eine andere. In großen Anwendungen ist dies jedoch bereits problematisch, insbesondere wenn wir Dutzende von Controllern mit Hunderten von Methoden haben. In dieser Situation kann uns ein Mechanismus wie die Abhängigkeitsinjektion helfen.
Wenn früher in ASP.NET 4 und anderen früheren Versionen verschiedene externe IoC-Container zum Installieren von Abhängigkeiten wie Ninject, Autofac, Unity, Windsor Castle und StructureMap verwendet werden mussten, verfügt ASP.NET Core bereits über einen integrierten Abhängigkeitsinjektionscontainer dargestellt durch die IServiceProvider-Schnittstelle. Die Abhängigkeiten selbst werden auch als Dienste bezeichnet, weshalb der Container als Dienstanbieter bezeichnet werden kann. Dieser Container ist dafür verantwortlich, Abhängigkeiten bestimmten Typen zuzuordnen und Abhängigkeiten in verschiedene Objekte einzufügen.
Zu Beginn haben wir eine harte Verknüpfung verwendet, um CsvDB im Controller zu verwenden.
private CsvDB db;
public BookController()
{
db = new CsvDB();
}
Auf den ersten Blick ist daran nichts auszusetzen, aber zum Beispiel hat sich das Datenbankverbindungsschema geändert: Anstelle von Csv habe ich mich für MongoDB oder MySql entschieden. Darüber hinaus müssen Sie möglicherweise eine Klasse dynamisch in eine andere ändern.
In diesem Fall bindet eine feste Verbindung den Controller an eine bestimmte Implementierung des Repositorys. Dieser Code ist schwieriger zu warten und zu testen, wenn Ihre Anwendung wächst. Daher wird empfohlen, nicht mehr starr gekoppelte, sondern lose gekoppelte Komponenten zu verwenden.
Mithilfe verschiedener Techniken zur Abhängigkeitsinjektion können Sie den Lebenszyklus der von Ihnen erstellten Services verwalten. Durch Depedency Injection generierte Dienste können von einem der folgenden Typen sein:
- Transient: . , . ,
- Scoped: . , .
- Singleton: ,
Die entsprechenden Methoden AddTransient (), AddScoped () und AddSingleton () werden verwendet, um jeden Diensttyp im eingebetteten .net-Kerncontainer zu erstellen.
Wir könnten einen Standardcontainer (Dienstanbieter) verwenden, der jedoch die Parameterübergabe nicht unterstützt. Daher muss ich die Autofac-Bibliothek verwenden.
Fügen Sie dazu über NuGet zwei Pakete zum Projekt hinzu: Autofac und Autofac.Extensions.DependencyInjection.
Jetzt ändern wir die ConfigureServices-Methode in der Datei Startup.cs in:
ConfigureServices
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.AddMvc();
var builder = new ContainerBuilder();//
builder.RegisterType<CsvRepository<Book>>()// CsvRepository
.As<IRepository<Book>>() // IRepository
.WithParameter("pathToBase", @"C:\csv\books.csv")// pathToBase
.InstancePerLifetimeScope(); //Scope
builder.RegisterType<BookService>()
.As<IBookService>()
.InstancePerDependency(); //Transient
builder.Populate(services); //
var container = builder.Build();
return new AutofacServiceProvider(container);
}
Auf diese Weise haben wir alle Implementierungen an ihre Schnittstellen gebunden.
Kehren wir zu unserem WebApiTest.Api-Projekt zurück.
Sie müssen lediglich BooksController.cs ändern
BooksController.cs
[Route("[controller]")]
[ApiController]
public class BooksController : ControllerBase
{
private IBookService service;
public BooksController(IBookService service)
{
this.service = service;
}
[HttpGet]
public ActionResult<IEnumerable<Book>> Get()
{
return new JsonResult(service.GetBooks());
}
[HttpGet("{id}")]
public ActionResult<Book> Get(int id)
{
return new JsonResult(service.GetBook(id));
}
[HttpPost]
public void Post([FromBody] Book item)
{
service.AddBook(item);
}
[HttpPut("{id}")]
public void Put([FromBody] Book item)
{
service.UpdateBook(item);
}
[HttpDelete("{id}")]
public void Delete(int id)
{
service.DeleteBook(id);
}
}
Drücken Sie F5, warten Sie, bis sich der Browser öffnet, gehen Sie zu / books und ...
[{"name":"Test","author":"Test","pages":100,"readedPages":0,"id":1}]
Ergebnis:
In diesem Text wollte ich mein gesamtes Wissen über das Onion-Architekturmuster sowie über die Abhängigkeitsinjektion mithilfe von Autofac aktualisieren.
Ich denke das Ziel ist erreicht, danke fürs Lesen;)
n-Tier
n- .
— . . , .
. ( ). , . . , - .
— . . , .
. ( ). , . . , - .