在非Web代码中模拟请求范围

时间:2017-04-12 20:04:45

标签: c# asp.net-mvc-5 static singleton

后台:我需要系统的一部分能够将各种状态消息推送到某个数据结构,以便调用者可以使用它们,而无需将数据结构明确地传递给方法,并且呼叫者的需求可能不同。

详细信息:我的应用程序有两个(可以想象得多)头,一个ASP.NET MVC 5网站和一个Windows服务。通常情况下,虽然Web应用程序的组合根将是Web站点本身,但我使用的是单独的组合根,这些"前端" connect to - 这允许他们共享一个共同的配置,因为几乎所有的依赖注入都是100%相同的。此外,为了进行测试,我决定将大部分代码保留在网站之外,因为真正的单元测试控制器存在问题。

所以我的代码需要能够在任何Web请求的上下文之外运行。同样,服务在计划中执行的任何操作都需要能够作为网站上的按需作业运行。因此,我的应用程序中的大部分繁重代码都不在网站或服务中。

现在,回到状态消息的需要:

  1. 将记录某些状态消息,但在作为服务运行时可能会记录更多状态消息。将日志项排队并在最后保存它们是可以的。
  2. 当一个作业从网站上按需运行时,可能会记录更少的内容,因为用户可以处理的任何问题都将直接显示给用户,出于调试目的,我们只关心直接错误发生了。需要立即将新消息推送到网站(可能通过websockets)。
  3. 此外,作业可以在调试或详细模式下运行,因此一次(例如在Web上)生成的信息或警告消息比另一次(来自无头服务)的情况更多。代码生成消息根本不应该担心这些细节,除非会影响生产性能的内容被置于调试模式的编译器指令中。)
  4. 此外,某些代码会将错误,警告或信息推送到从请求返回的对象中。这些都很容易处理。但是其他错误,警告或信息(例如阻止所请求的对象被取回的错误)需要在正常返回值之外冒泡。
  5. 现在我使用的东西似乎不太理想:我的所有方法都必须接受一个他们可以修改的参数,以便冒出这些错误。例如:

    public IReadOnlyCollection<UsableItem> GetUsableItems(
       ReadOnlyHashSet<string> itemIds,
       List<StatusMessage> statusMessages
    ) {
        var resultItems = _itemService.Get(itemIds);
        var resultItemsByHasFrobDuplicate = resultItems
           .GroupBy(i => i.FrobId)
           .ToLookup(grp => grp.Count() > 1, grp => grp.ToList());
        statusMessages
           .AddRange(
              resultItemsByHasFrobDuplicate[true]
                 .Select(items => $@"{items[0].FrobId
                    } is used by multiple items {string.Join(",", items.Select(i => i.usableItemId))
                    }")
           );
        return resultItemsByHasFrobDuplicate[false]
           .Select(grp => grp.First())
           .ToList()
           .AsReadOnly();
     }
    

    所以你可以在这里看到,虽然通常项目可以在方法的返回值中(并且这些项目甚至可以在它们上面放置自己的状态消息),但其他项目不能 - 调用代码无法处理重复并期望一组UsableItem个没有重复FrobId值的对象。重复的情况是意外的,需要冒泡到用户或日志。

    通过删除statusMessages参数并执行更类似于CurrentScope.PushMessage(message)的操作,并且知道这些消息将根据其严重性或其他规则(真实的)正确处理,将大大提高代码消息是具有多个属性的对象。)

    哦,我在上面的代码中遗漏了一些东西。我真正需要做的是:

    _itemService.Get(itemIds, statusMessages); // -- take the darn parameter everywhere
    

    哎呀。那不太理想。

    我立即认为MiniProfiler.CurrentStep类似,但它可以在任何地方使用,但它的范围限定为当前请求。但我不明白它是如何静态的,但是在不同请求之间隔离任何ToLookup次呼叫,这样用户就不会在其输出中获得另一个用户的步骤。另外,它不仅适用于MVC吗?当没有MVC,只有非网络代码时,我需要这个。

    任何人都可以建议一种方法来改进我的代码,而不必在方法之后将列表传递给方法吗?一些适用于单元测试的东西也很重要,因为我需要能够在单元测试中设置一种方法来捕捉模拟中的冒泡错误(或者如果不是这样的话,就可以什么也不做要测试的系统的所需部分。

    P.S。我并不介意对上面的{{1}}模式进行机智的批评,以便分离重复项。我经常使用这种技术,并且会对更好的方式感兴趣。

1 个答案:

答案 0 :(得分:0)

我认为你只是以错误的方式看待这个问题。这些都不涉及或实际上与请求相关。你只需要一些可以注入的服务来推送消息。 如何这样做是无关紧要的,依赖注入的全部意义在于具有依赖性的类不应该知道或关心。

为您的消息服务创建一个界面:

public interface IMessagingService
{
    void PushMessage(string message);
}

然后,您应该更改包含GetUsableItems的类,以将消息传递服务注入构造函数。一般来说,方法注入(当前通过将List<StatusMessages>传递给方法而进行的操作)是不受欢迎的。

public class MyAwesomeClass
{
    protected readonly IMessagingService messenger;

    public MyAwesomeClass(IMessagingService messenger)
    {
        this.messenger = messenger;
    }

然后,在你的方法中:

messenger.PushMessage("My awesome message");

此接口的实现可能会根据它是否在Web应用程序或Windows服务中注入而有所不同。您的Web应用程序可能会有一个只使用自己的代码来推送消息的实现,而Windows服务可能需要一个利用HttpClient向您的Web应用程序发出请求的实现。设置您的DI容器,为正确的应用程序注入正确的实现,并且您已完成。