در معماری vertical slices با features سر و کار داریم؛ برای مثال برنامهی ما دو ویژگی نویسندهها و بلاگها را خواهد داشت و هر ویژگی، کاملا متکی به خود است. برای نمونه هر ویژگی میتواند به همراه یک ماژول باشد که به صورت مستقل، تمام سرویسها، endpoints و میانافزارهای مورد نیاز خودش را ثبت میکند. در این معماری، تمام قسمتهای مورد نیاز جهت کارکرد یک ویژگی، در کنار هم قرار میگیرند تا یافتن آنها و درک ارتباطات بین آنها سادهتر شود.
تعریف ساختار ماژولهای ویژگیهای معماری vertical slices
برای تعریف ساختار ماژولی که کار ثبت تمام نیازمندیهای یک ویژگی را انجام میدهد، مانند ثبت سرویسها، endpoints و میانافزارها، ابتدا پوشهای به نام Contracts را به پروژهی Api اضافه میکنیم؛ با این اینترفیس:
namespace MinimalBlog.Api.Contracts;
public interface IModule
{
IEndpointRouteBuilder RegisterEndpoints(IEndpointRouteBuilder endpoints);
}
ثبت خودکار ماژولهای برنامه در ابتدای اجرای آن
پس از تعریف این قرارداد، اکنون میخواهیم هر ماژولی که در برنامه، اینترفیس فوق را پیاده سازی میکند، در ابتدای اجرای برنامه به صورت خودکار، یافت شده و اطلاعات آن به سیستم اضافه شود. برای این منظور متدهای الحاقی زیر را تعریف میکنیم:
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddApplicationServices(this IServiceCollection services,
WebApplicationBuilder builder)
{
// ...
builder.Services.AddAllModules(typeof(Program));
return services;
}
private static void AddAllModules(this IServiceCollection services, params Type[] types)
{
// Using the `Scrutor` to add all of the application's modules at once.
services.Scan(scan =>
scan.FromAssembliesOf(types)
.AddClasses(classes => classes.AssignableTo<IModule>())
.AsImplementedInterfaces()
.WithSingletonLifetime());
}
}
این کلاس ساختار سادهای دارد؛ ابتدا در متد AddAllModules، اسمبلی جاری جهت یافتن کلاسهای پیاده سازی کنندهی اینترفیس IModule، اسکن میشود؛
با استفاده از کتابخانهی Scrutor.
سپس کلاسهای ثبت شده که هم اکنون جزئی از سیستم تزریق وابستگیهای برنامه هستند، یافت شده و متد RegisterEndpoints آنها فراخوانی میشوند تا دیگر نیازی نباشد به ازای هر ماژول، یکبار ثبت دستی این موارد در کلاس Program انجام شود.
using MinimalBlog.Api.Contracts;
namespace MinimalBlog.Api.Extensions;
public static class ModuleExtensions
{
public static WebApplication RegisterEndpoints(this WebApplication app)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}
var modules = app.Services.GetServices<IModule>();
foreach (var module in modules)
{
module.RegisterEndpoints(app);
}
return app;
}
}
بنابراین در ادامه به کلاس Program مراجعه کرد و متد عمومی کلاس فوق را در آن به صورت app.RegisterEndpoints فراخوانی میکنیم:
using MinimalBlog.Api.Extensions;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddApplicationServices(builder);
var app = builder.Build();
app.ConfigureApplication();
app.RegisterEndpoints();
app.Run();
این چند سطر، کل محتوای فایل Program.cs برنامه را تشکیل میدهند.
ایجاد اولین Feature برنامه؛ ویژگی نویسندگان
برای تعریف اولین ویژگی برنامه که مختص به نویسندگان است، پوشههای جدید Features\Authors را در برنامهی Api ایجاد میکنیم.
- اولین کاری را که در ادامه انجام خواهیم داد، انتقال فایل AuthorDto.cs که در قسمت قبل ایجاد کردیم، به درون این پوشهی جدید است.
- سپس ماژول نویسندگان را به صورت زیر به آن اضافه میکنیم:
namespace MinimalBlog.Api.Features.Authors;
public class AuthorModule : IModule
{
public IEndpointRouteBuilder RegisterEndpoints(IEndpointRouteBuilder endpoints)
{
endpoints.MapGet("/api/authors", async (MinimalBlogDbContext ctx) =>
{
var authors = await ctx.Authors.ToListAsync();
return authors;
});
endpoints.MapPost("/api/authors", async (MinimalBlogDbContext ctx, AuthorDto authorDto) =>
{
var author = new Author();
author.FirstName = authorDto.FirstName;
author.LastName = authorDto.LastName;
author.Bio = authorDto.Bio;
author.DateOfBirth = authorDto.DateOfBirth;
ctx.Authors.Add(author);
await ctx.SaveChangesAsync();
return author;
});
return endpoints;
}
}
در اینجا ماژول نویسندگان را که با پیاده سازی قرارداد IModule تشکیل شدهاست، مشاهده میکنید. در متد RegisterEndpoints آن، دو endpoints تعریف شدهی در کلاس Program برنامه را در قسمت قبل، Cut کرده و به اینجا منتقل کردهایم. بنابراین اکنون کلاس Program، دیگر به همراه تعریف مستقیم هیچ endpoint ای نیست و خلوت شدهاست. هدف از Features هم دقیقا همین است تا هر ویژگی برنامه، متکی به خود بوده و مستقل باشد؛ به همراه تمام تعاریف مورد نیاز جهت کار با آن در یک محل مشخص (مانند انتقال فایل Dto مربوط به آن، به درون همین پوشه). مزیت این روش، درک سادهتر اجزای مرتبط و یافتن سریعتر ارتباطات قسمتهای یک ویژگی خاص است. در آینده اگر مشکلی رخ داد و باگی بروز پیدا کرد، دقیقا میدانیم که محدودهای که باید مورد بررسی قرار گیرد، کجاست و این محدوده، کوچک و متکی به خود است و در بین چندین پروژهی مختلف، پراکنده نشدهاست.
کار نمونه سازی و اجرای متدهای این ماژولها نیز توسط متدهای الحاقی کلاس ModuleExtensions، در ابتدای اجرای برنامه به صورت خودکار انجام میشود و نیازی به شلوغ کردن کلاس Program برای ثبت دستی آنها نیست.
افزودن AutoMapper و MediatR به پروژهی Api
در ادامه برای ساده سازی کار نگاشتهای Dtoهای برنامه به مدلهای دومین آن، از AutoMapper استفاده خواهیم کرد؛ همچنین از MediatR نیز برای پیاده سازی الگوی CQRS که در قسمت بعدی پیگیری خواهد شد. بنابراین در ابتدا بستههای نیوگت این دو را به پروژهی Api اضافه میکنیم:
<Project Sdk="Microsoft.NET.Sdk.Web">
<ItemGroup>
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="11.0.0" />
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="10.0.1" />
</ItemGroup>
</Project>
سپس به کلاس ServiceCollectionExtensions مراجعه کرده و تعاریف ثبت سرویسهای این دو را نیز اضافه میکنیم:
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddApplicationServices(this IServiceCollection services,
WebApplicationBuilder builder)
{
// ...
builder.Services.AddMediatR(typeof(Program));
builder.Services.AddAutoMapper(typeof(Program));
return services;
}
}
اکنون میتوان اولین Profile مربوط به AutoMapper را که کار نگاشت AuthorDto به Author و برعکس را انجام میدهد، به صورت زیر تهیه کنیم:
using AutoMapper;
using MinimalBlog.Domain.Model;
namespace MinimalBlog.Api.Features.Authors;
public class AuthorProfile : Profile
{
public AuthorProfile()
{
CreateMap<AuthorDto, Author>().ReverseMap();
}
}
این فایل نیز درون پوشهی Features\Authors قرار میگیرد.