C# 可观测序列

C# 可观测序列,c#,timer,cron,system.reactive,observable,C#,Timer,Cron,System.reactive,Observable,我想使用反应扩展(RX)和NContab创建一个可观察序列。该序列与类似于Observable.Timer()的序列不同,因为周期和到期时间是不固定的。在阅读之后,似乎Observable.Generate()才是正确的选择。我想到了两个变体:一个在边界内运行,另一个永远运行。这些实现有意义吗 public static IObservable<DateTime> Cron(string cron) { var schedule = CrontabSchedule.Parse

我想使用反应扩展(RX)和NContab创建一个可观察序列。该序列与类似于
Observable.Timer()
的序列不同,因为周期和到期时间是不固定的。在阅读之后,似乎
Observable.Generate()
才是正确的选择。我想到了两个变体:一个在边界内运行,另一个永远运行。这些实现有意义吗

public static IObservable<DateTime> Cron(string cron)
{
    var schedule = CrontabSchedule.Parse(cron);
    return Observable.Generate(DateTime.Now, d=>true, d => DateTime.Now, d => d,
        d => new DateTimeOffset(schedule.GetNextOccurrence(d)));
}

public static IObservable<DateTime> Cron(string cron, DateTime start, DateTime end)
{
    var schedule = CrontabSchedule.Parse(cron);
    return Observable.Generate(start, d => d < end, d => DateTime.Now, d => d,
        d => new DateTimeOffset(schedule.GetNextOccurrence(d)));
}

首先,
scheduler.Now.DateTime
不会在使用
TestScheduler
的单元测试中提供实时性。它将根据一些预定义的开始时间为您提供虚拟时间。您可能应该使用
AdvanceTo
将时钟初始化为与crontab测试数据相对应的值

对于这个示例测试,这可能不是您的问题。您的问题很可能是您正在以
勾选的粒度编写测试。这很少奏效。因为,对于
TestScheduler
,当在Tick
t
上发生调度另一个操作以“立即”执行的调度操作时,下一个操作在Tick
t+1
之前不会实际执行。如果该操作安排了另一个操作,那么它将在勾号
t+2
等之前不会执行。因此,除非您完全理解
Generate
如何安排其工作,否则您希望避免编写勾号级别的测试

相反,尝试在您正在测试的代码所支持的粒度上进行测试。在这种情况下,我认为这是分钟…所以写你的测试提前59秒,看看什么都没有发生,然后再提前2秒,看看你是否得到了你想要的。或者,更好的方法是使用
TestScheduler.CreateObserver
方法前进一次

var scheduler = new TestScheduler();

// set the virtual clock to something that corresponds with my test crontab data
scheduler.AdvanceTo(someDateTimeOffset);

// now your tests...
var cron = "* * * * *";
var observer = scheduler.CreateObserver();

var disp = ObservableCron.Cron(cron, scheduler).Subscribe(observer);

// advance the clock by 61 seconds
scheduler.AdvanceBy(TimeSpan.FromSeconds(61).Ticks);

// check the contents of the observer
Assert.IsTrue(observer.Messages.Count > 0);

这看起来是一系列问题的结合。首先,我使用的
Observable.Generate
重载使用
Func
参数来确定下一次触发的时间。我传递了一个新的
DateTimeOffset
,它基于调度程序的本地时间而不是Utc,这导致了新的“DateTimeOffset”发生了变化。请参阅以获取解释。正确的功能如下所示:

public static IObservable<int> Cron(string cron, IScheduler scheduler)
{
    var schedule = CrontabSchedule.Parse(cron);
    return Observable.Generate(0, d => true, d => d + 1, d => d,
        d => new DateTimeOffset(schedule.GetNextOccurrence(scheduler.Now.UtcDateTime)), scheduler);
}
publicstaticiobservable Cron(字符串Cron,IScheduler调度器)
{
var schedule=CrontabSchedule.Parse(cron);
返回可观察的。生成(0,d=>true,d=>d+1,d=>d,
d=>newdatetimeoffset(schedule.GetNextOccurrence(scheduler.Now.UtcDateTime)),scheduler);
}
就测试而言,我想出了一些更好地证明意图的方法:

[TestMethod]
public void TestCronInterval()
{
    var scheduler = new TestScheduler();
    var end = scheduler.Now.UtcDateTime.AddMinutes(10);
    const string cron = "*/5 * * * *";
    var i = 0;
    var seconds = 0;
    var sub = ObservableCron.Cron(cron, scheduler).Subscribe(x => i++);
    while (i < 2)
    {
        seconds++;
        scheduler.AdvanceBy(TimeSpan.TicksPerSecond);
    }
    Assert.IsTrue(seconds == 600);
    Assert.AreEqual(end, scheduler.Now.UtcDateTime);
    sub.Dispose();
}
[TestMethod]
公共void TestCronInterval()
{
var scheduler=newtestscheduler();
var end=scheduler.Now.UtcDateTime.AddMinutes(10);
常量字符串cron=“*/5****”;
var i=0;
var秒=0;
var sub=observecron.Cron(Cron,scheduler).Subscribe(x=>i++);
而(i<2)
{
秒++;
调度程序.AdvanceBy(TimeSpan.TicksPerSecond);
}
Assert.IsTrue(秒==600);
AreEqual(end,scheduler.Now.UtcDateTime);
sub.Dispose();
}

我使用此解决方案时,没有使用调度程序,而是使用:

public static IObservable to observable(此MicronsScheduleObservable配置)
{
验证(配置);
var schedule=configuration.Expression;
DateTimeOffset?next=null;
返回可观察。生成(
DateTimeOffset。现在,
i=>正确,
i=>(next=schedule.GetNextOccurrence(i,configuration.TimeZone))??DateTimeOffset.Now,
i=>下一步,
i=>i
).
其中(i=>i.HasValue)。
选择(i=>i.Value);
}
公共接口MicronsScheduleObservable配置:
IObservableConfiguration
{
/// 
///具有以下格式的Cron计划:https://github.com/HangfireIO/Cronos#cron-格式
/// 
///非空
字符串计划{get;}
/// 
///格式
/// 
CronFormat格式{get;}
/// 
///使用
/// 
///非空
///使用“”解析失败
CronExpression{get;}
/// 
///时区,用于使用
/// 
///非空
时区信息时区{get;}
}

它们对我来说很有意义。这真的是你的问题吗?更新。我在测试函数时遇到问题。当IScheduler参与进来时,我似乎错过了一些东西。正是我所寻找的,我已经使用了两个lib,而你将它们组合在一起。
[TestMethod]
public void TestCronInterval()
{
    var scheduler = new TestScheduler();
    var end = scheduler.Now.UtcDateTime.AddMinutes(10);
    const string cron = "*/5 * * * *";
    var i = 0;
    var seconds = 0;
    var sub = ObservableCron.Cron(cron, scheduler).Subscribe(x => i++);
    while (i < 2)
    {
        seconds++;
        scheduler.AdvanceBy(TimeSpan.TicksPerSecond);
    }
    Assert.IsTrue(seconds == 600);
    Assert.AreEqual(end, scheduler.Now.UtcDateTime);
    sub.Dispose();
}
public static IObservable<DateTimeOffset> ToObservable(this ICronScheduleObservableConfiguration configuration)
{
    Validate(configuration);
    var schedule = configuration.Expression;
    DateTimeOffset? next = null;
    return Observable.Generate(
        DateTimeOffset.Now,
            i => true,
            i => (next = schedule.GetNextOccurrence(i, configuration.TimeZone)) ?? DateTimeOffset.Now,
            i => next,
            i => i
        ).
        Where(i => i.HasValue).
        Select(i => i.Value);
}

public interface ICronScheduleObservableConfiguration :
    IObservableConfiguration
{
    /// <summary>
    /// Cron schedule with format: https://github.com/HangfireIO/Cronos#cron-format
    /// </summary>
    /// <value>Non-empty</value>
    string Schedule { get; }
    /// <summary>
    /// <see cref="Schedule"/> format
    /// </summary>
    CronFormat Format { get; }
    /// <summary>
    /// Parsed <see cref="Schedule"/> using <see cref="Format"/>
    /// </summary>
    /// <value>non-null</value>
    /// <exception cref="CronFormatException">Parsing with <see cref="CronExpression.Parse(string, CronFormat)"/> failed</exception>
    CronExpression Expression { get; }
    /// <summary>
    /// Time zone used for computing schedule times with <see cref="CronExpression.GetNextOccurrence(DateTimeOffset, TimeZoneInfo, bool)"/>
    /// </summary>
    /// <value>non-null</value>
    TimeZoneInfo TimeZone { get; }
}