بسیاری از ما با معماری تمیز آشنا هستیم، امروزه به این رویکرد معماری لایه ای سنتی گفته می شود.
یک معماری سنتی لایهای یا پیازی کد را بر اساس technical concern در لایههای مختلف سازماندهی میکند. در این رویکرد، هر لایه مسئولیت فردی در سیستم دارد.
سپس لایه ها به یکدیگر وابسته هستند و می توانند با یکدیگر همکاری کنند.
در برنامه های وب، لایه های زیر ممکن است اعمال شوند:
معماری برش عمودی یک الگوی معماری است که کدها را به جای الگوهای فنی بر اساس ویژگی ها سازماندهی می کند.
همانطور که در تصویر می بینید، ایده معماری برش عمودی در مورد گروه بندی کدها بر اساس عملکردهای تجاری است و همه کدهای مربوطه را در کنار هم قرار می دهد.
در معماری لایه ای هدف این است که به لایه های افقی فکر کنیم، اما در معماری عمودی باید به لایه های عمودی فکر کنیم، در این مورد باید همه چیز را به عنوان یک ویژگی قرار دهیم، یعنی نیازی به لایه های مشترک مانند repositories, services, infrastructure, و حتی controllers نداریم و فقط باید روی ویژگی ها تمرکز کنیم.
حالا بیایید نگاهی بیندازیم که چگونه میتوانیم از این رویکرد پیروی کنیم، من یک minimal API ساده در NET 8 ایجاد خواهم کرد.
در اینجا راه حل ما شبیه خواهد بود:
راه حل معماری برش عمودی
ما باید بسته های زیر را نصب کنیم
dotnet add package MediatR dotnet add Microsoft.EntityFrameworkCore dotnet add Microsoft.EntityFrameworkCore.Design dotnet add Microsoft.EntityFrameworkCore.SqlServer
بیایید با ایجاد یک موجودیت شروع کنیم.
public class Book { public long Id { get; set; } public string Name { get; set; } = string.Empty; public string Description { get; set; } = string.Empty; }
باید dbContext را در پوشه پایگاه داده اضافه کنیم
public class ApplicationDbContext:DbContext { public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options):base(options) { } public DbSet<Book> Books { get; set; } }
زمان آن است که سرویس ها را پیکربندی کنیم و همچنین middleware را در API خود اضافه کنیم
appsettings.Development.json
{ "Logging" : { "LogLevel" : { "Default" : "Information" , "Microsoft.AspNetCore" : "Warning" } } , "ConnectionStrings" : { "Database" : "Server=localhost;Database=VSA;Integrated" Security=true;MultipleActiveResultSets=true;TrustServerCertificate=Yes;" } }
Program.cs
var builder = WebApplication.CreateBuilder(args); builder.Services.AddDbContext<ApplicationDbContext>(opt => { opt.UseSqlServer(builder.Configuration.GetConnectionString("Database")); }); builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Program).Assembly)); var app = builder.Build(); app.Run();
بنابراین، ما فقط dbContext را برای استفاده از سرور SQL و همچنین سرویس MediatR را ثبت کردیم.
مرحله بعدی این است که ویژگی را در معماری خود اعمال کنیم، در این مورد باید هر ویژگی را از هم جدا کنیم، در این مثال یک ویژگی کتاب ایجاد می کنم که همه کتاب ها را ایجاد کرده و دریافت می کند.
بیایید با ایجاد ویژگی کتاب شروع کنیم، زیرا من از الگوی MediatR استفاده خواهم کرد، الگوی CQRS را تطبیق خواهم داد که پرس و جو و دستورات را از هم جدا می کند.
public static class CreateBook { public sealed class CreateBookCommand:IRequest<long> { public string Name { get; set; } = string.Empty; public string Description { get; set; } = string.Empty; } internal sealed class Handler : IRequestHandler<CreateBookCommand, long> { private readonly ApplicationDbContext _dbContext; public Handler(ApplicationDbContext dbContext) { _dbContext = dbContext; } public async Task<long> Handle(CreateBookCommand request, CancellationToken cancellationToken) { var book = new Book { Name = request.Name, Description = request.Description }; await _dbContext.AddAsync(book, cancellationToken); await _dbContext.SaveChangesAsync(cancellationToken); return book.Id; } } public static void AddEndpoint(this IEndpointRouteBuilder app) { app.MapPost("api/books", async (CreateBookRequest request, ISender sender) => { var bookId = await sender.Send(request); return Results.Ok(bookId); }); } }
قراردادها
public class CreateBookRequest { public string Name { get; set; } = string.Empty; public string Description { get; set; } = string.Empty; }
همانطور که می بینید، همه ویژگی را در کلاس static اضافه کردیم، یک ورودی ایجاد می کند که از IRequest برای ایجاد یک دستور استفاده می کند، سپس IRequest را به کنترل کننده آن اضافه می کند و کتاب را ایجاد می کند و در نهایت نقطه پایانی را برای فراخوانی توسط مشتری.
برای اعمال نقطه پایانی که به این میان افزار در خط لوله نیاز داریم، به سادگی می توانیم آن را در کلاس program.cs اضافه کنیم.
var app = builder.Build(); CreateBook.AddEndpoint(app); ...
بیایید یک پرس و جو ایجاد کنیم تا همه کتاب ها را انتخاب کرده و به مشتری برگردانیم.
public static class GetAllBooks { public sealed class Query : IRequest<List<Book>> { } internal sealed class QueryHandler : IRequestHandler<Query, List<Book>> { private readonly ApplicationDbContext _dbContext; public QueryHandler(ApplicationDbContext dbContext) { _dbContext = dbContext; } public async Task<List<Book>> Handle(Query request, CancellationToken cancellationToken) => await _dbContext.Books.ToListAsync(cancellationToken: cancellationToken); } public static void AddEndpoint(this IEndpointRouteBuilder app) { app.MapGet("api/books", async (ISender sender) => { var books=await sender.Send(new GetAllBooks.Query()); return Results.Ok(books); }); } }
همچنین، باید Endpoint را در Pipeline اضافه کنیم:
var app = builder.Build(); CreateBook.AddEndpoint(app); GetAllBooks.AddEndpoint(app); ...
همانطور که می بینید، برای هر ویژگی که نیاز داریم میان افزار endpoint را اضافه کنیم، در اینجا بسته دیگری وجود دارد که می توانیم نصب کرده و از آن برای خلاص شدن از اضافه کردن میان افزار جدید استفاده کنیم.
dotnet add package Carter
برای استفاده از کارتر، باید ویژگی ها را اصلاح کنیم
CreateBook.cs
public static class CreateBook { public sealed class CreateBookCommand:IRequest<long> { public string Name { get; set; } = string.Empty; public string Description { get; set; } = string.Empty; } internal sealed class Handler : IRequestHandler<CreateBookCommand, long> { private readonly ApplicationDbContext _dbContext; public Handler(ApplicationDbContext dbContext) { _dbContext = dbContext; } public async Task<long> Handle(CreateBookCommand request, CancellationToken cancellationToken) { var book = new Book { Name = request.Name, Description = request.Description }; await _dbContext.AddAsync(book, cancellationToken); await _dbContext.SaveChangesAsync(cancellationToken); return book.Id; } } } public class CreateBookEndpoint : ICarterModule { public void AddRoutes(IEndpointRouteBuilder app) { app.MapPost("api/books", async (CreateBookRequest request, ISender sender) => { var bookId = await sender.Send(request); return Results.Ok(bookId); }); } }
GetAllBooks.cs public static class GetAllBooks { public sealed class Query : IRequest<List<Book>> { } internal sealed class QueryHandler : IRequestHandler<Query, List<Book>> { private readonly ApplicationDbContext _dbContext; public QueryHandler(ApplicationDbContext dbContext) { _dbContext = dbContext; } public async Task<List<Book>> Handle(Query request, CancellationToken cancellationToken) => await _dbContext.Books.ToListAsync(cancellationToken: cancellationToken); } } public class GetAllBooksEndpoint : ICarterModule { public void AddRoutes(IEndpointRouteBuilder app) { app.MapGet("api/books", async (ISender sender) => { var books=await sender.Send(new GetAllBooks.Query()); return Results.Ok(books); }); } }
و آخرین مرحله ثبت Carter در Middleware است
var app = builder.Build(); app.MapCarter();
ما فقط باید میان افزار MapCarter را در خط لوله اضافه کنیم و تمام است، نیازی به اضافه کردن ویژگی جدید در خط لوله نیست.
نظر شما در مورد این ویژگی جدید چیست؟ آیا آن را به عنوان یک افزودنی ارزشمند برای پروژه های خود می بینید یا قصد دارید به معماری سنتی لایه ای/پیازی پایبند باشید؟