MediatR 库:遵循 DRY 原则
MediatR library: following the DRY principle
我在我的 ASP.NET Core
应用程序中使用库 MediatR
。
我有以下实体 Ad
:
public class Ad
{
public Guid AdId { get; set; }
public AdType AdType { get; set; }
public double Cost { get; set; }
public string Content { get; set; }
// ...
}
public enum AdType
{
TextAd,
HtmlAd,
BannerAd,
VideoAd
}
我想介绍制作新广告的功能。为此,我创建了以下命令:
public class CreateAdCommand : IRequest<Guid>
{
public AdType AdType { get; set; }
public double Cost { get; set; }
public string Content { get; set; }
public class Handler : IRequestHandler<CreateAdCommand, Guid>
{
private readonly MyDbContext _context;
public Handler(MyDbContext context)
{
_context = context;
}
public async Task<Guid> Handle(CreateAdCommand request, CancellationToken cancellationToken)
{
var ad = new Ad {AdType = request.AdType, Cost = request.Cost, Content = request.Content};
_context.Ads.Add(ad);
_context.SaveChangesAsync();
return ad.AdId;
}
}
}
这段代码效果很好。但这里有一个大问题:每个广告类型在广告创建过程中都有一些额外的逻辑(例如,在创建 TextAd
类型的广告时,我们需要在广告内容中找到关键字)。最简单的解决方案是:
public async Task<Guid> Handle(CreateAdCommand request, CancellationToken cancellationToken)
{
var ad = new Ad {AdType = request.AdType, Cost = request.Cost, Content = request.Content};
_context.Ads.Add(ad);
_context.SaveChangesAsync();
switch (request.AdType)
{
case AdType.TextAd:
// Some additional logic here...
break;
case AdType.HtmlAd:
// Some additional logic here...
break;
case AdType.BannerAd:
// Some additional logic here...
break;
case AdType.VideoAd:
// Some additional logic here...
break;
}
return ad.AdId;
}
这个解决方案违反了开闭原则(当我创建一个新的广告类型时,我需要在 CreateAdCommand
中创建一个新的 case
)。
我有另一个想法。我可以为每个广告类型创建一个单独的命令(例如,CreateTextAdCommand
、CreateHtmlAdCommand
、CreateBannerAdCommand
、CreateVideoAdCommand
)。此解决方案遵循开闭原则(当我创建一个新的广告类型时,我需要为此广告类型创建一个新命令 - 我不需要更改现有代码)。
public class CreateTextAdCommand : IRequest<Guid>
{
public double Cost { get; set; }
public string Content { get; set; }
public class Handler : IRequestHandler<CreateTextAdCommand, Guid>
{
private readonly MyDbContext _context;
public Handler(MyDbContext context)
{
_context = context;
}
public async Task<Guid> Handle(CreateTextAdCommand request, CancellationToken cancellationToken)
{
var ad = new Ad {AdType = AdType.TextAd, Cost = request.Cost, Content = request.Content};
_context.Ads.Add(ad);
await _context.SaveChangesAsync();
// Some additional logic here ...
return ad.AdId;
}
}
}
public class CreateHtmlAdCommand : IRequest<Guid>
{
public double Cost { get; set; }
public string Content { get; set; }
public class Handler : IRequestHandler<CreateHtmlAdCommand, Guid>
{
private readonly MyDbContext _context;
public Handler(MyDbContext context)
{
_context = context;
}
public async Task<Guid> Handle(CreateHtmlAdCommand request, CancellationToken cancellationToken)
{
var ad = new Ad {AdType = AdType.HtmlAd, Cost = request.Cost, Content = request.Content};
_context.Ads.Add(ad);
await _context.SaveChangesAsync();
// Some additional logic here ...
return ad.AdId;
}
}
}
// The same for CreateBannerAdCommand and CreateVideoAdCommand.
该方案遵循开闭原则,但违反了DRY原则。我该如何解决这个问题?
如果您坚持使用第二种方法,则可以利用 MediatR 'Behaviors' (https://github.com/jbogard/MediatR/wiki/Behaviors)。它们就像管道一样,您可以在其中将常见行为卸载到常用处理程序中。
为此,创建一个标记界面
interface ICreateAdCommand {}
现在让每个 concreate 命令继承它
public class CreateTextAdCommand : ICreateAdCommand
{
public readonly string AdType {get;} = AdType.Text
}
public class CreateHtmltAdCommand : ICreateAdCommand
{
public readonly string AdType {get;} = AdType.Html
}
/*...*/
您可以将其合并或替换为通用抽象基础 class,以避免重复通用属性。这由你决定。
现在我们为我们的行为创建处理程序:
public class CreateAdBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TReq : ICreateAdCommand
{
public CreateAdBehavior()
{
//wire up dependencies.
}
public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
{
var ad = new Ad {AdType = request.AdType, Cost = request.Cost, Content = request.Content};
_context.Ads.Add(ad);
await _context.SaveChangesAsync();
//go on with the next step in the pipeline
var response = await next();
return response;
}
}
现在连接这个行为。在 asp.net 核心中,这将在您的 startup.cs
中
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(CreateAdBehavior<,>));
在这个阶段,每次你的 IRequests
实现 ICreateAdCommand
,它都会自动调用上面的处理程序,完成后它会调用下一个行为,或者如果有none,实际处理程序。
您的特定处理程序,假设一个 HtmlAd 现在大致如下所示:
public class CreateHtmlAdCommand : IRequest<Guid>
{
public class Handler : IRequestHandler<CreateHtmlAdCommand, Guid>
{
private readonly MyDbContext _context;
public Handler(MyDbContext context)
{
_context = context;
}
public async Task<Guid> Handle(CreateHtmlAdCommand request, CancellationToken cancellationToken)
{
// Some additional logic here ...
}
}
}
** 更新 **
如果您想跨管道拖动数据,您可以利用实际的请求对象。
public abstract class IRequestWithItems
{
public IDictionary<string, object> Items {get;} = new Dictionary<string,object>();
}
现在,在您的 CreateAdBehavior 中,您可以创建广告并将其存储在字典中,以便在下一个处理程序中检索它:
var ad = { ... }
await _context.SaveChangesAsync();
items["newlyCreatedAd"] = ad;
并且在实际的 Task<Guid> Handle()
方法中,您现在可以随意使用广告,而无需返回数据库再次检索它。
来自作者的详细信息:https://jimmybogard.com/sharing-context-in-mediatr-pipelines/
我在我的 ASP.NET Core
应用程序中使用库 MediatR
。
我有以下实体 Ad
:
public class Ad
{
public Guid AdId { get; set; }
public AdType AdType { get; set; }
public double Cost { get; set; }
public string Content { get; set; }
// ...
}
public enum AdType
{
TextAd,
HtmlAd,
BannerAd,
VideoAd
}
我想介绍制作新广告的功能。为此,我创建了以下命令:
public class CreateAdCommand : IRequest<Guid>
{
public AdType AdType { get; set; }
public double Cost { get; set; }
public string Content { get; set; }
public class Handler : IRequestHandler<CreateAdCommand, Guid>
{
private readonly MyDbContext _context;
public Handler(MyDbContext context)
{
_context = context;
}
public async Task<Guid> Handle(CreateAdCommand request, CancellationToken cancellationToken)
{
var ad = new Ad {AdType = request.AdType, Cost = request.Cost, Content = request.Content};
_context.Ads.Add(ad);
_context.SaveChangesAsync();
return ad.AdId;
}
}
}
这段代码效果很好。但这里有一个大问题:每个广告类型在广告创建过程中都有一些额外的逻辑(例如,在创建 TextAd
类型的广告时,我们需要在广告内容中找到关键字)。最简单的解决方案是:
public async Task<Guid> Handle(CreateAdCommand request, CancellationToken cancellationToken)
{
var ad = new Ad {AdType = request.AdType, Cost = request.Cost, Content = request.Content};
_context.Ads.Add(ad);
_context.SaveChangesAsync();
switch (request.AdType)
{
case AdType.TextAd:
// Some additional logic here...
break;
case AdType.HtmlAd:
// Some additional logic here...
break;
case AdType.BannerAd:
// Some additional logic here...
break;
case AdType.VideoAd:
// Some additional logic here...
break;
}
return ad.AdId;
}
这个解决方案违反了开闭原则(当我创建一个新的广告类型时,我需要在 CreateAdCommand
中创建一个新的 case
)。
我有另一个想法。我可以为每个广告类型创建一个单独的命令(例如,CreateTextAdCommand
、CreateHtmlAdCommand
、CreateBannerAdCommand
、CreateVideoAdCommand
)。此解决方案遵循开闭原则(当我创建一个新的广告类型时,我需要为此广告类型创建一个新命令 - 我不需要更改现有代码)。
public class CreateTextAdCommand : IRequest<Guid>
{
public double Cost { get; set; }
public string Content { get; set; }
public class Handler : IRequestHandler<CreateTextAdCommand, Guid>
{
private readonly MyDbContext _context;
public Handler(MyDbContext context)
{
_context = context;
}
public async Task<Guid> Handle(CreateTextAdCommand request, CancellationToken cancellationToken)
{
var ad = new Ad {AdType = AdType.TextAd, Cost = request.Cost, Content = request.Content};
_context.Ads.Add(ad);
await _context.SaveChangesAsync();
// Some additional logic here ...
return ad.AdId;
}
}
}
public class CreateHtmlAdCommand : IRequest<Guid>
{
public double Cost { get; set; }
public string Content { get; set; }
public class Handler : IRequestHandler<CreateHtmlAdCommand, Guid>
{
private readonly MyDbContext _context;
public Handler(MyDbContext context)
{
_context = context;
}
public async Task<Guid> Handle(CreateHtmlAdCommand request, CancellationToken cancellationToken)
{
var ad = new Ad {AdType = AdType.HtmlAd, Cost = request.Cost, Content = request.Content};
_context.Ads.Add(ad);
await _context.SaveChangesAsync();
// Some additional logic here ...
return ad.AdId;
}
}
}
// The same for CreateBannerAdCommand and CreateVideoAdCommand.
该方案遵循开闭原则,但违反了DRY原则。我该如何解决这个问题?
如果您坚持使用第二种方法,则可以利用 MediatR 'Behaviors' (https://github.com/jbogard/MediatR/wiki/Behaviors)。它们就像管道一样,您可以在其中将常见行为卸载到常用处理程序中。
为此,创建一个标记界面
interface ICreateAdCommand {}
现在让每个 concreate 命令继承它
public class CreateTextAdCommand : ICreateAdCommand
{
public readonly string AdType {get;} = AdType.Text
}
public class CreateHtmltAdCommand : ICreateAdCommand
{
public readonly string AdType {get;} = AdType.Html
}
/*...*/
您可以将其合并或替换为通用抽象基础 class,以避免重复通用属性。这由你决定。
现在我们为我们的行为创建处理程序:
public class CreateAdBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TReq : ICreateAdCommand
{
public CreateAdBehavior()
{
//wire up dependencies.
}
public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
{
var ad = new Ad {AdType = request.AdType, Cost = request.Cost, Content = request.Content};
_context.Ads.Add(ad);
await _context.SaveChangesAsync();
//go on with the next step in the pipeline
var response = await next();
return response;
}
}
现在连接这个行为。在 asp.net 核心中,这将在您的 startup.cs
中 services.AddTransient(typeof(IPipelineBehavior<,>), typeof(CreateAdBehavior<,>));
在这个阶段,每次你的 IRequests
实现 ICreateAdCommand
,它都会自动调用上面的处理程序,完成后它会调用下一个行为,或者如果有none,实际处理程序。
您的特定处理程序,假设一个 HtmlAd 现在大致如下所示:
public class CreateHtmlAdCommand : IRequest<Guid>
{
public class Handler : IRequestHandler<CreateHtmlAdCommand, Guid>
{
private readonly MyDbContext _context;
public Handler(MyDbContext context)
{
_context = context;
}
public async Task<Guid> Handle(CreateHtmlAdCommand request, CancellationToken cancellationToken)
{
// Some additional logic here ...
}
}
}
** 更新 **
如果您想跨管道拖动数据,您可以利用实际的请求对象。
public abstract class IRequestWithItems
{
public IDictionary<string, object> Items {get;} = new Dictionary<string,object>();
}
现在,在您的 CreateAdBehavior 中,您可以创建广告并将其存储在字典中,以便在下一个处理程序中检索它:
var ad = { ... }
await _context.SaveChangesAsync();
items["newlyCreatedAd"] = ad;
并且在实际的 Task<Guid> Handle()
方法中,您现在可以随意使用广告,而无需返回数据库再次检索它。
来自作者的详细信息:https://jimmybogard.com/sharing-context-in-mediatr-pipelines/