منوی چند سطحی واکنشگرا
TDD در دات نت، قسمت اول
dependency injection به زبان ساده
طراحی Responsive در وب
public class ProductViewModel { public int ProductId { get; set; } public string Name { get; set; } public string Description { get; set; } public string ImageUrl { get; set; } public decimal UnitPrice { get; set; } }
public record ProductDTO(int Id, string Name, string Description);
public record ProductDTO { public int Id { get; init; } public string Name { get; init; } } var dto = new ProductDTO { Id = 1, Name = "some name" };
public class Product : DataObject<Product> { public Product(int id) { Id = id; InitializeFromDatabase(); } private void InitializeFromDatabase() { DataHelpers.LoadFromDatabase(this); } public int Id { get; private set; } // other properties and methods }
public class Product { public Product(int id) { Id = id; } private Product() { // required for EF } public int Id { get; private set; } // other properties and methods }
- برای اجرای وظایف خود به فریم ورک ثالثی وابسته نیست.
- به کلاس پایهای ( Base class) نیاز ندارد.
- وابستگی به متد استاتیکی ندارد.
- می تواند در هر جایی از پروژه، نمونه سازی شود.
- اصل Persistence Ignorant را بیشتر رعایت کرده، نه بطور کامل؛ چون یک سازنده دارد که به کتابخانهی ثالثی نیازمند است (سازندهی بدون پارامتر که مورد نیاز EF میباشد).
برای ایجاد «خواص الحاقی» قبلا در سایت مطلب ایجاد «خواص الحاقی» تهیه شدهاست. در این مطلب قصد داریم راه حل ارائه شدهی در مطلب مذکور را با یک TypeDescriptionProvider سفارشی ترکیب کرده تا به صورت یکدست، از طریق TypeDescriptor بتوان به آن خواص نیز دسترسی داشته باشیم.
فرض کنید در یک سیستم Modular Monolith، نیاز جدیدی به دست شما رسیده است که به شرح زیر میباشد:
نیاز داریم در گریدی از صفحهی X مربوط به «مؤلفه 1»، ستونی جدید را اضافه کنید و دیتای مربوط به این ستون، توسط «مؤلفه 2» مهیا خواهد شد.
- قبلا «مؤلفه 2» ارجاعی را به «مؤلفه 1» داده است؛ لذا امکان ارجاع معکوس را در این حالت، نداریم.
- «مؤلفه 1» باید بتواند مستقل از «مؤلفه 2» نیز توزیع شده و کار کند؛ لذا این نیاز برای زمانی است که «مؤلفه 2» برای توزیع در Component Model ما وجود داشته باشد.
- نمیخواهیم در آینده برای نیازهای مشابه در همان صفحهی X، تغییر جدیدی را در «مؤلفه 1» داشته باشیم (اضافه کردن خصوصیت مورد نظر به مدل نمایشی یا اصطلاحا ویو-مدل متناظر با گرید در در زمان طراحی، جواب مساله نمیباشد)
- میخواهیم به یک طراحی با Loose Coupling (اتصال سست و ضعیف، وابستگی ضعیف) دست پیدا کنیم.
در این حالت «مؤلفه 1» بدون آگاهی از سایر مؤلفهها، همهی پیاده سازیهای IExtraColumnConenvtion را در زمان اجرا یافته و از آنها برای ایجاد ستونهای جدید، استفاده خواهد کرد.
واسط مذکور به شکل زیر میباشد:
public interface IConvention { } public interface IExtraColumnConvention<T> : IConvention { string Name { get; } string Title { get; } void Populate(IEnumerable<T> list); }
البته این واسط میتواند جزئیات بیشتری را هم شامل شود.
گام اول: طراحی TypeDescriptionProvider
در .NET به دو طریق میتوان به متادیتای یک Type دسترسی داشت:
- استفاده از API Reflection موجود در فضای نام System.Reflection
- کلاس TypeDescriptor
به طور کلی هدف از این کلاس در دات نت، ارائه اطلاعاتی در خصوص یک وهله از جمله: Attributeها، Propertyها، Eventهای آن و غیره، میباشد. هنگام استفاده از Reflection، اطلاعات بدست آمده از Type، به دلیل اینکه بعد از کامپایل نمیتوانند تغییر کنند، لذا قابلیت توسعه پذیری را هم ندارند. در مقابل، با استفاده از کلاس TypeDescriptor این توسعه پذیری را برای وهلههای مختلف میتوانید داشته باشید.
برای مهیا کردن متادیتای سفارشی (در اینجا اطلاعات مرتبط با خصوصیات الحاقی) برای TypeDescriptor، نیاز است یک TypeDescriptionProvider سفارشی را طراحی کنیم.
/// <summary> /// Use this provider when you need access ExtraProperties with TypeDescriptor.GetProperties(instance) /// </summary> public class ExtraPropertyTypeDescriptionProvider<T> : TypeDescriptionProvider where T : class { private static readonly TypeDescriptionProvider Default = TypeDescriptor.GetProvider(typeof(T)); public ExtraPropertyTypeDescriptionProvider() : base(Default) { } public override ICustomTypeDescriptor GetTypeDescriptor(Type instanceType, object instance) { var descriptor = base.GetTypeDescriptor(instanceType, instance); return instance == null ? descriptor : new ExtraPropertyCustomTypeDescriptor(descriptor, instance); } private sealed class ExtraPropertyCustomTypeDescriptor : CustomTypeDescriptor { //... } }
در تکه کد بالا، ابتدا تامین کنندهی پیشفرض مرتبط با نوع جنریک مورد نظر را یافته و به عنوان تامین کنندهی پایه معرفی کردهایم. سپس برای معرفی CustomTypeDescritpr باید متد GetTypeDescriptor را بازنویسی کنیم. در اینجا لازم است برای معرفی متادیتا مرتبط با یک نوع، یک پیاده سازی از واسط ICustomTypeDescriptor را ارائه کنیم:
private sealed class ExtraPropertyCustomTypeDescriptor : CustomTypeDescriptor { private readonly IEnumerable<ExtraPropertyDescriptor<T>> _instanceExtraProperties; public ExtraPropertyCustomTypeDescriptor(ICustomTypeDescriptor defaultDescriptor, object instance) : base(defaultDescriptor) { _instanceExtraProperties = instance.ExtraPropertyList<T>(); } public override PropertyDescriptorCollection GetProperties(Attribute[] attributes) { var properties = new PropertyDescriptorCollection(null); foreach (PropertyDescriptor property in base.GetProperties(attributes)) { properties.Add(property); } foreach (var property in _instanceExtraProperties) { properties.Add(property); } return properties; } public override PropertyDescriptorCollection GetProperties() { return GetProperties(null); } }
public static class ExtraProperties { //... public static IEnumerable<ExtraPropertyDescriptor<T>> ExtraPropertyList<T>(this object instance) where T : class { if (!PropertyCache.TryGetValue(instance, out var properties)) throw new KeyNotFoundException($"key: {instance.GetType().Name} was not found in dictionary"); return properties.Select(p => new ExtraPropertyDescriptor<T>(p.PropertyName, p.PropertyValueFunc, p.SetPropertyValueFunc, p.PropertyType, p.Attributes)); } }
public sealed class ExtraPropertyDescriptor<T> : PropertyDescriptor where T : class { private readonly Func<object, object> _propertyValueFunc; private readonly Action<object, object> _setPropertyValueFunc; private readonly Type _propertyType; public ExtraPropertyDescriptor( string propertyName, Func<object, object> propertyValueFunc, Action<object, object> setPropertyValueFunc, Type propertyType, Attribute[] attributes) : base(propertyName, attributes) { _propertyValueFunc = propertyValueFunc; _setPropertyValueFunc = setPropertyValueFunc; _propertyType = propertyType; } public override void ResetValue(object component) { } public override bool CanResetValue(object component) => true; public override object GetValue(object component) => _propertyValueFunc(component); public override void SetValue(object component, object value) => _setPropertyValueFunc(component, value); public override bool ShouldSerializeValue(object component) => true; public override Type ComponentType => typeof(T); public override bool IsReadOnly => _setPropertyValueFunc == null; public override Type PropertyType => _propertyType; }
[TypeDescriptionProvider(typeof(ExtraPropertyTypeDescriptionProvider<Person>))] private class Person { public string Name { get; set; } public string Family { get; set; } }
[Test] public void Should_TypeDescriptor_GetProperties_Returns_ExtraProperties_And_PredefinedProperties() { //Arrange var rabbal = new Person {Name = "GholamReza", Family = "Rabbal"}; const string propertyName = "Title"; const string propertyValue = "Software Engineer"; //Act rabbal.ExtraProperty(propertyName, propertyValue); var title = TypeDescriptor.GetProperties(rabbal).Find(propertyName, true); //Assert rabbal.ExtraProperty<string>(propertyName).ShouldBe(propertyValue); title.ShouldNotBeNull(); title.GetValue(rabbal).ShouldBe(propertyValue); }
گام دوم: استفاده از IExtraColumnConvention برای نمایش ستونهای الحاقی
public class Column4Convention : IExtraColumnConvention<Product> { public string Name => "Column4"; public string Title => "Column 4" public void Populate(IEnumerable<Product> list) { //TODO: forEach on list and set ExtraProperty // item.ExtraProperty(Name,value) // item.ExtraProperty(Name,(obj)=> value) // item.ExtraProperty(Name,(obj)=> value, (obj,value)=>) } } public class Column2Convention : IExtraColumnConvention<Product> { public string Name => "Column2"; public string Title => "Column 2" public void Populate(IEnumerable<Product> list) { //TODO: forEach on list and set ExtraProperty } } public class Column3Convention : IExtraColumnConvention<Product> { public string Name => "Column3"; public string Title => "Column 3" public void Populate(IEnumerable<Product> list) { //TODO: forEach on list and set ExtraProperty } }
سپس این پیادهسازیها از طریق مکانیزمی مانند معرفی آنها به یک IoC Container، توسط میزبان (مؤلفه 1) قابل دسترسی خواهد بود. در نهایت میزبان، قبل از نمایش محصولات، به شکل زیر عمل خواهد کرد:
var products = _productService.PagedList(page:1, pageSize:10); var columns = _provider.GetServices<IExtraColumnConvention<Product>>(); foreach(var column in columns) { column.Populate(products); }
- if else
- switch case
بله مورد اولی که به ذهن خود من رسید، استفاده از if else هست. شاید خروجی مناسبی را از نظر کدنویسی داشته باشد؛ ولی خوانایی مناسبی را ندارد. حالا چطور اثبات کنیم خوانایی و قابلیت توسعهی پایینی را دارد؟
فرض کنید شما برنامه را نوشتهاید و تحویل مدیر خود دادهاید. بعد از دو ماه به شما گفته میشود که مراحل 1 و 2 را جابجا کنید و یا یک step را اضافه کنید که بعد از مرحله دو (بررسی رمز) است تا یک منطق جدید را دنبال کند. اینجاست که دچار دردسر و اتلاف زمان میشویم؛ چون باید بیزینس را مجددا review کنیم و بدتر از آن کدها را هم تغییر دهیم که امکان رخ دادن خطا به شدت بالا میرود.
- انجام کار در چند مرحله
- حذف پیچیدگیهای پیاده سازی
حالا بیایید با هم با الگوی Chain Of Responsibility، این مثال را پیاده سازی کنیم. منطق کار به صورت زیر است:
به این شکل که مراحل بصورت سلسله مراتبی، تحت successorهای یکدیگر پیش میروند. اگر بخواهم successor را در این مثال توضیح دهم من بهعنوان دانشجو (successor اول) بعد از چک شدن موارد مربوط به دانشجو، درخواست به سمت مسئول مربوطه رفته (successor دوم ) و الی اخر.
public class Customer { public string Password { get; set; } public string Stno { get; set; } public int value { get; set; } public bool Active { get; set; } }
public class RequestContext { public int VamValue { get; set; } public Customer student { get; set; } }
public class ResponseContext { public string Response { get; set; } }
حال طبق شکل بالا باید handler خود را که گرفتن وام است، پیاده سازی نماییم:
public abstract class GetVam { protected readonly GetVam successor; public GetVam(GetVam _getVam) { this.successor = _getVam; } public abstract ResponseContext execute(RequestContext requestContext); }
public class CheckUseractive : GetVam { public CheckUseractive(GetVam _getVam) : base(_getVam) { } public override ResponseContext execute(RequestContext requestContext) { if (requestContext.student.Active == true) { return successor.execute(requestContext); } else { return new ResponseContext { Response = "student is inactive" }; } } }
2-بررسی رمز کاربر :
public class ChechPassword : GetVam { public ChechPassword(GetVam _getVam) : base(_getVam) { } public override ResponseContext execute(RequestContext requestContext) { if (requestContext.student.Password == "123") { return successor.execute(requestContext); } else { return new ResponseContext { Response = "invalid pass", }; } } }
3-بررسی میزان بدهکاری دانشجو :
public class ChechUserBedehkar : GetVam { public ChechUserBedehkar(GetVam _getVam) : base(_getVam) { } public override ResponseContext execute(RequestContext requestContext) { if (requestContext.student.value < requestContext.VamValue) { return successor.execute(requestContext); } else { return new ResponseContext { Response = "you are dont permission" }; } } }
4-و مرحله آخر که در صورتیکه تمامی مراحل قبلی پاس شوند چک کردن مقدار وامی است که به دانشجو باید داده شود :
public class AssignVam : GetVam { public AssignVam(GetVam _getVam) : base(_getVam) { } public override ResponseContext execute(RequestContext requestContext) { return new ResponseContext { Response = "value of vam: " + (requestContext.VamValue - requestContext.student.value).ToString(); }; } }
partial class Program { static void Main(string[] args) { Customer customer = new Customer() { Active = true, Password = "123", Stno = "111", value = 2000 }; RequestContext requestContext = new RequestContext() { student = customer, VamValue = 3000, }; var GetVam = new CheckUseractive(new ChechPassword(new ChechUserBedehkar(new AssignVam(null)))); var res = GetVam.execute(requestContext); Console.Write(res.Response); Console.ReadKey(); } }
Performance Counter Behavior
فرض کنید میخواهید زمان انجام کار یک متد را اندازه گیری کرده و در صورت طولانی بودن زمان انجام آن، لاگی را مبنی بر کند بودن بیش از حد مجاز این متد، ثبت کنید. شاید اولین راهی که برای انجام اینکار به ذهنتان بیاید این باشد که داخل تمام متدهایی که میخواهیم زمان انجام آنها را محاسبه کنیم، چنین کدی را تکرار کنیم:public class SomeClass { private readonly ILogger _logger; public SomeClass(ILogger logger) { _logger = logger; } public void SomeMethod() { Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); // TODO: Do some work here stopwatch.Stop(); if (stopwatch.ElapsedMilliseconds > TimeSpan.FromSeconds(5).Milliseconds) { // This method has taken a long time, So we log that to check it later. _logger.LogWarning($"SomeClass.SomeMethod has taken {stopwatch.ElapsedMilliseconds} to run completely !"); } } }
علاوه بر این تصور کنید روزی تصمیم بگیرید که حداکثر زمان برای Log کردن را از 5 ثانیه به 10 ثانیه تغییر دهید. در این صورت بدلیل اینکه در همه متدها این قطعه کد تکرار شدهاست، مجبور به تغییر تمام کدهای برنامه برای اصلاح این بخش خواهید شد. در اینجا اصل DRY نقض شدهاست.
برای حل این مشکل از Behaviorها استفاده میکنیم. برای پیاده سازی Behaviorها داخل MediatR، کافیست از interface ای بنام IPipelineBehavior ارث بری کنیم:
public class RequestPerformanceBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> { private readonly ILogger<RequestPerformanceBehavior<TRequest, TResponse>> _logger; public RequestPerformanceBehavior(ILogger<RequestPerformanceBehavior<TRequest, TResponse>> logger) { _logger = logger; } public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next) { Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); TResponse response = await next(); stopwatch.Stop(); if (stopwatch.ElapsedMilliseconds > TimeSpan.FromSeconds(5).Milliseconds) { // This method has taken a long time, So we log that to check it later. _logger.LogWarning($"{request} has taken {stopwatch.ElapsedMilliseconds} to run completely !"); } return response; } }
سپس باید Behaviorهای خود را از طریق DI به MediatR معرفی کنیم. داخل Startup.cs به این صورت RequestPerformanceBehavior خود را Register میکنیم:
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(RequestPerformanceBehavior<,>));
در نهایت برای تست کارکرد این Behavior، در کوئری GetCustomerByIdQueryHandler خود 5 ثانیه Delay ایجاد میکنیم تا طول اجرای آن، از Maximum زمان مشخص شده بیشتر و Log انجام شود:
public class GetCustomerByIdQueryHandler : IRequestHandler<GetCustomerByIdQuery, CustomerDto> { private readonly ApplicationDbContext _context; private readonly IMapper _mapper; public GetCustomerByIdQueryHandler(ApplicationDbContext context, IMapper mapper) { _context = context; _mapper = mapper; } public async Task<CustomerDto> Handle(GetCustomerByIdQuery request, CancellationToken cancellationToken) { Customer customer = await _context.Customers .FindAsync(request.CustomerId); if (customer == null) { throw new RestException(HttpStatusCode.NotFound, "Customer with given ID is not found."); } // For testing PerformanceBehavior await Task.Delay(5000, cancellationToken); return _mapper.Map<CustomerDto>(customer); } }
پس از اجرای برنامه و فراخوانی GetCustomerById ، داخل Console این پیغام را خواهید دید:
Transaction Behavior
یکی دیگر از استفادههای Behaviorها میتواند پیاده سازی Transaction و Rollback باشد. فرض کنید میخواهیم افزودن یک مشتری به دیتابیس فقط زمانی صورت گیرد که تمام کارهای داخل Command با موفقیت و بدون رخ دادن Exception انجام شود. برای انجام اینکار میتوان یک TransactionBehavior نوشت تا بدنه Commandها را داخل یک TransactionScope قرار دهد و در صورت وقوع Exception ، عمل Rollback صورت گیرد :
public class TransactionBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> { public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next) { var transactionOptions = new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted, Timeout = TransactionManager.MaximumTimeout }; using (var transaction = new TransactionScope(TransactionScopeOption.Required, transactionOptions, TransactionScopeAsyncFlowOption.Enabled)) { TResponse response = await next(); transaction.Complete(); return response; } } }
سپس این Behavior را داخل DI Container خود Register میکنیم :
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(TransactionBehavior<,>));
در نهایت متد Handle در CreateCustomerCommandHandler را که در قسمتهای قبل ایجاد کردیم، تغییر داده و بعد از SaveChanges مربوط به Entity Framework، یک Exception را صادر میکنیم:
public class CreateCustomerCommandHandler : IRequestHandler<CreateCustomerCommand, CustomerDto> { readonly ApplicationDbContext _context; readonly IMapper _mapper; readonly IMediator _mediator; public CreateCustomerCommandHandler(ApplicationDbContext context, IMapper mapper, IMediator mediator) { _context = context; _mapper = mapper; _mediator = mediator; } public async Task<CustomerDto> Handle(CreateCustomerCommand createCustomerCommand, CancellationToken cancellationToken) { Domain.Customer customer = _mapper.Map<Domain.Customer>(createCustomerCommand); await _context.Customers.AddAsync(customer, cancellationToken); await _context.SaveChangesAsync(cancellationToken); throw new Exception("======= MY CUSTOM EXCEPTION ======="); // Raising Event ... await _mediator.Publish(new CustomerCreatedEvent(customer.FirstName, customer.LastName, customer.RegistrationDate), cancellationToken); return _mapper.Map<CustomerDto>(customer); } }
اگر برنامه را اجرا کنید خواهید دید با اینکه Exception ما بعد از SaveChanges رخ داده است، اما بدلیل استفاده از Transaction Behavior ای که نوشتیم، عملیات Rollback صورت گرفته و داخل دیتابیس رکوردی ثبت نشدهاست.
MediatR دارای 2 اینترفیس IRequestPreProcessor و IRequestPostProcessor نیز هست که اگر نیاز داشته باشید یک عمل فقط قبل یا بعد از انجام یک Command/Query صورت گیرد، میتوانید از آنها استفاده کنید.
نصب و راه اندازی
Install-Package MediatR
بعد از نصب نیاز داریم تا نیازمندیهای این فریمورک را داخل DI Container خود Register کنیم. اگر از DI Container پیشفرض ASP.NET Core استفاده کنیم ، کافیست پکیج متناسب آن با Microsoft.Extensions.DependencyInjection را نصب کرده و بهراحتی نیازمندیهای MediatR را فراهم سازیم:
Install-Package MediatR.Extensions.Microsoft.DependencyInjection
public void ConfigureServices(IServiceCollection services) { services.AddMvc(); services.AddMediatR(); }
* اگر از DI Containerهای دیگری استفاده میکنید، میتوانید با استفاده از توضیحات این لینک MediatR را داخل Container مورد نظرتان Register کنید.
IRequest
پیاده سازی Non-Generic آن برای درخواستهایی است که Response برگشتی ندارند ( معمولا Commandها ) و منتظر جوابی از سمت آنها نیستیم و پیاده سازی Generic آن، نوع Response ای را که بعد از پردازش درخواست برگشت داده میشود، مشخص میسازد.
public class Customer { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public DateTime RegistrationDate { get; set; } }
و Dto متناسب با آن نیز به این صورت تعریف شده است :
public class CustomerDto { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string RegistrationDate { get; set; } }
افزودن مشتری، یک Command است؛ زیرا باعث افزودن رکوردی جدیدی به دیتابیس و تغییر State برنامه میشود. کلاس جدیدی به اسم CreateCustomerCommand ایجاد کرده و از IRequest ارث بری میکنیم و نوع Response برگشتی آن را CustomerDto قرار میدهیم:
public class CreateCustomerCommand : IRequest<CustomerDto> { public CreateCustomerCommand(string firstName, string lastName) { FirstName = firstName; LastName = lastName; } public string FirstName { get; } public string LastName { get; } }
کلاس CreateCustomerCommand نیازمندیهای خود را از طریق Constructor مشخص میسازد. برای ایجاد کردن یک مشتری حداقل چیزی که لازم است، Firstname و Lastname آن است و بعد از ارسال مقادیر مورد نیاز به سازنده این کلاس، مقادیر بدلیل get-only بودن قابل تغییر نیستند.
IRequestHandler
public class CreateCustomerCommandHandler : IRequestHandler<CreateCustomerCommand, CustomerDto> { readonly ApplicationDbContext _context; readonly IMapper _mapper; public CreateCustomerCommandHandler(ApplicationDbContext context, IMapper mapper) { _context = context; _mapper = mapper; } public async Task<CustomerDto> Handle(CreateCustomerCommand createCustomerCommand, CancellationToken cancellationToken) { Customer customer = _mapper.Map<Customer>(createCustomerCommand); await _context.Customers.AddAsync(customer, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map<CustomerDto>(customer); } }
ورودی اول IRequestHandler، کلاسی است که درخواست، آن را پردازش خواهد کرد و پارامتر ورودی دوم، کلاسی است که در نتیجه پردازش بعنوان Response برگشت داده خواهد شد.
همانطور که میبینید در این Handler از DbContext مربوط به Entity Framework برای ثبت اطلاعات داخل دیتابیس و IMapper مربوط به AutoMapper برای نگاشت CreateCustomerCommand به Customer استفاده شده است.
تنظیمات Profile مربوط به AutoMapper ما به این صورت است تا در هنگام نگاشت CreateCustomerCommand ، مقدار RegistrationDate مربوط به Customer برابر با زمان فعلی قرار داده شود و برای نگاشت Customer به CustomerDto نیز ، تاریخ RegistrationDate با فرمتی قابل فهم به کاربران نمایش داده شود :
public class DomainProfile : Profile { public DomainProfile() { CreateMap<CreateCustomerCommand, Customer>() .ForMember(c => c.RegistrationDate, opt => opt.MapFrom(_ => DateTime.Now)); CreateMap<Customer, CustomerDto>() .ForMember(cd => cd.RegistrationDate, opt => opt.MapFrom(c => c.RegistrationDate.ToShortDateString())); } }
در نهایت با inject کردن اینترفیس IMediator به کنترلر خود و فرستادن یک درخواست POST به این اکشن، درخواست ایجاد مشتری را توسط متد Send میدهیم :
[HttpPost] public async Task<IActionResult> CreateCustomer([FromBody] CreateCustomerCommand createCustomerCommand) { CustomerDto customer = await _mediator.Send(createCustomerCommand); return CreatedAtAction(nameof(GetCustomerById), new { customerId = customer.Id }, customer); }
همانطور که میبینید ما در اینجا فقط درخواست، فرستادهایم و وظیفه پیدا کردن Handler این درخواست را فریمورک MediatR برعهده گرفتهاست و ما هیچ جایی بطور مستقیم Handler خود را صدا نزده ایم. ( Hollywood Principle: Don't Call Us, We Call You )
اینترفیس IMediator علاوه بر متد Send ، دارای متد دیگری بنام Publish نیز هست که وظیفه Raise کردن Eventها را برعهده دارد که در مقالات بعدی از آن استفاده خواهیم کرد.
چند نکته :