【问题标题】:MediatR library: following the DRY principleMediatR 库:遵循 DRY 原则
【发布时间】:2021-08-01 18:43:04
【问题描述】:

我在 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)。

我有另一个想法。我可以为每种广告类型创建一个单独的命令(例如,CreateTextAdCommandCreateHtmlAdCommandCreateBannerAdCommandCreateVideoAdCommand)。此解决方案遵循开放封闭原则(当我创建一个新的广告类型时,我需要为这个广告类型创建一个新命令 - 我不需要更改现有代码)。

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 原则。我该如何解决这个问题?

【问题讨论】:

  • 我已经用一种跨管道共享数据的机制更新了我的答案。

标签: c# .net dry cqrs mediatr


【解决方案1】:

如果您坚持第二种方法,您可以利用 MediatR 的“行为”(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
}
/*...*/

您可以将其组合或替换为通用抽象基类,以避免重复通用属性。这取决于你。

现在我们为我们的行为创建处理程序:

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 时,它都会自动调用上面的处理程序,完成后它会调用行中的下一个行为,或者如果没有,则调用实际的处理程序。

您的特定处理程序,假设 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&lt;Guid&gt; Handle() 方法中,您现在可以随意使用广告,而无需循环回数据库再次检索它。

作者详情:https://jimmybogard.com/sharing-context-in-mediatr-pipelines/

【讨论】:

  • 感谢您的想法。但这里有一个问题 - 在CreateAdBehavior 内部创建一个广告,使其属性AdType 等于AdType.HtmlAd。这意味着,所有广告都将具有 AdType.HtmlAd 类型。我说的对吗?
  • 如果将type属性拉到ICreateAdCommand接口,可以在行为中设置type为request.AdType。
  • 是的,@JonasHøgh 所说的是一种合适的方法。我已经相应地更新了我的答案。
  • 谢谢,它有效!但是,如果管道中的另一个行为将写入items["newlyCreatedAd"] 另一个值怎么办?它看起来有点脏。也许还有另一种方法可以将值从行为传递给命令?
  • 很遗憾没有。您必须非常小心,不要在一个行为中两次使用相同的变量名。公平地说,这不应该太难;)但我很高兴它对你有用。随意将其标记为已解决
猜你喜欢
  • 2020-04-08
  • 1970-01-01
  • 1970-01-01
  • 2020-09-11
  • 1970-01-01
  • 2017-03-18
  • 1970-01-01
  • 2014-10-11
  • 1970-01-01
相关资源
最近更新 更多