在整个交易过程中始终使用 "now" 的值

Consistently using the value of "now" throughout the transaction

我正在寻找在整个交易过程中使用一致的当前日期和时间值的指南。

我所说的事务泛指应用程序服务方法,此类方法通常执行单个 SQL 事务,至少在我的应用程序中是这样。

环境语境

this question 的答案中描述的一种方法是将当前日期置于环境上下文中,例如DateTimeProvider,并在所有地方使用它而不是 DateTime.UtcNow

然而,这种方法的目的只是使设计可单元测试,而我也想防止因不必要的多次查询 DateTime.UtcNow 而导致的错误,例如:

// In an entity constructor:
this.CreatedAt = DateTime.UtcNow;
this.ModifiedAt = DateTime.UtcNow;

此代码创建的实体的创建日期和修改日期略有不同,而人们希望这些属性在实体创建后立即相等。

此外,环境上下文很难在 Web 应用程序中正确实现,因此我提出了另一种方法:

方法注入 + DeterministicTimeProvider

实现如下:

/// <summary>
/// Provides the current date and time.
/// The provided value is fixed when it is requested for the first time.
/// </summary>
public class DeterministicTimeProvider: IDateTimeProvider
{
    private readonly Lazy<DateTimeOffset> _lazyUtcNow =
        new Lazy<DateTimeOffset>(() => DateTimeOffset.UtcNow);

    /// <summary>
    /// Gets the current date and time in the UTC time zone.
    /// </summary>
    public DateTimeOffset UtcNow => _lazyUtcNow.Value;
}

这是一个好方法吗?缺点是什么?有更好的选择吗?

代码看起来很合理。

缺点 - 对象的生命周期很可能会由 DI 容器控制,因此提供者的用户无法确保它始终被正确配置(每次调用而不是任何更长的生命周期,如 app/singleton).

如果您有代表 "transaction" 的类型,最好将 "Started" 时间放在那里。

很抱歉诉诸权威 的逻辑谬误,但这很有趣:

约翰·卡马克曾经说过:

There are four principle inputs to a game: keystrokes, mouse moves, network packets, and time. (If you don't consider time an input value, think about it until you do -- it is an important concept)"

来源:John Carmack's .plan posts from 1998 (scribd)

(我一直觉得这句话很有趣,因为如果某件事对你来说不对,你应该认真思考直到它看起来对,这只是一个建议主要极客会说。)

所以,这里有一个想法:将时间作为输入。它可能不包含在xml构成网络服务请求,(无论如何你都不希望它,)但是在你将 xml 转换为实际请求对象的处理程序中,获取当前时间并将其作为你的请求对象的一部分.

因此,由于请求对象在处理交易的过程中在您的系统中传递,因此始终可以在请求[=]中找到被视为"the current time"的时间 35=]。所以,不再是"the current time",而是请求时间。 (完全相同或非常接近相同的事实完全无关紧要。)

这样,测试也变得更加容易:您不必模拟时间提供程序接口,时间始终在输入参数中。

此外,通过这种方式,其他有趣的事情也成为可能,例如在与 实际 当前时刻完全无关的时刻追溯应用服务请求时间。想想可能性。 (带彩虹的鲍勃方裤的照片放在这里。)

嗯..这对 CodeReview.SE than for Whosebug 来说是一个更好的问题,但是当然 - 我会咬人的。

Is this a good approach?

如果使用得当,在你描述的场景中,这种做法是合理的。它实现了两个既定目标:

  1. 使您的代码更易于测试。这是我称之为 "Mock the Clock" 的常见模式,在许多设计良好的应用程序中都能找到。

  2. 将时间锁定为单个值。这种情况不太常见,但您的代码确实实现了该目标。

What are the disadvantages?

  1. 由于您正在为每个请求创建另一个新对象,它会为垃圾收集器创建少量的额外内存使用和额外工作。这有点有争议,因为这通常适用于所有具有按请求生命周期的对象,包括控制器。

  2. 在您读取时钟之前,会添加一小部分时间,这是由于在加载对象和执行延迟加载时所做的额外工作造成的。虽然它可以忽略不计 - 可能在几毫秒的数量级。

  3. 由于值被锁定,您(或使用您的代码的其他开发人员)始终存在可能引入细微错误的风险,因为您忘记了该值在下一次请求之前不会更改.您可能会考虑不同的命名约定。例如,将其称为 "requestRecievedTime" 或类似名称,而不是 "now"。

  4. 与上一项类似,您的提供程序也存在加载错误生命周期的风险。您可能会在新项目中使用它而忘记设置实例化,将其作为单例加载。然后为 all 请求锁定这些值。要强制执行此操作,您无能为力,因此请务必对其进行评论。 <summary>标签是个好地方。

  5. 您可能会发现在无法进行构造函数注入的情况下需要当前时间 - 例如静态方法。您要么必须重构以使用实例方法,要么必须将时间或时间提供程序作为参数传递给静态方法。

Are there better alternatives?

是的,参见

您还可以考虑 Noda Time,它通过 IClock 接口以及 SystemClockFakeClock 实现内置了类似的概念。但是,这两种实现都设计为单例。它们有助于测试,但无法实现将每个请求的时间锁定为单个值的第二个目标。不过,您始终可以编写一个执行此操作的实现。

这不是可以通过实时时钟和查询或测试来回答的问题。开发人员可能已经想出了一些到达底层库调用的晦涩方法...

所以不要那样做。依赖注入也不会在这里拯救你;问题是您想要在 'session.'

开始时使用标准时间模式

在我看来,根本问题是你在表达一个想法,并为此寻找一种机制。正确的机制是给它命名,并在名称中说出你的意思,然后只设置一次。 readonly 是在构造函数中仅设置一次的好方法,并让编译器和运行时强制执行您 的意思,即仅设置一次。

// In an entity constructor:
this.CreatedAt = DateTime.UtcNow;