在.Net Core 2中模拟Hangfire RecurringJob依赖

时间:2018-03-06 16:55:07

标签: c# dependency-injection asp.net-core-2.0 hangfire

考虑以下控制器:

public class SubmissionController : Controller
{ 
    public SubmissionController()
    { }

    public IActionResult Post()
    {
        RecurringJob.AddOrUpdate(() => InitiateSubmission(), Cron.Minutely);

        return Ok("Periodic submission triggered");
    }
}

Hangfire是否为RecurringJob类提供了抽象注入依赖项?我做了一些研究,唯一可用的抽象是IBackgroundJobClient,它没有选择安排重复工作。

我需要验证作业是否已在单元测试中添加。

1 个答案:

答案 0 :(得分:6)

如果您检查RecurringJob类的source code,您会看到其静态方法导致调用RecurringJobManager类:

public static class RecurringJob
{
    private static readonly Lazy<RecurringJobManager> Instance = new Lazy<RecurringJobManager>(
        () => new RecurringJobManager());

    //  ...

    public static void AddOrUpdate(
        Expression<Action> methodCall,
        string cronExpression,
        TimeZoneInfo timeZone = null,
        string queue = EnqueuedState.DefaultQueue)
    {
        var job = Job.FromExpression(methodCall);
        var id = GetRecurringJobId(job);

        Instance.Value.AddOrUpdate(id, job, cronExpression, timeZone ?? TimeZoneInfo.Utc, queue);
    }

    //  ...
}

RecurringJobManager实现IRecurringJobManager接口,您可以将其用于依赖注入和UT模拟。

但是RecurringJob具有从lambda获取作业并构建作业ID的内部逻辑:

var job = Job.FromExpression(methodCall);
var id = GetRecurringJobId(job);

Job.FromExpression()是一种可以安全使用的公共方法。但是GetRecurringJobId是一个私有方法,定义如下:

private static string GetRecurringJobId(Job job)
{
    return $"{job.Type.ToGenericTypeString()}.{job.Method.Name}";
}

GetRecurringJobId基本上以SubmissionController.InitiateSubmission的形式返回作业方法的名称。它基于内部类TypeExtensions,具有Type的扩展方法。您不能直接使用此类,因为它是内部的,因此您应该复制该逻辑。

如果您遵循这种方法,您的最终解决方案将是:

TypeExtensions (从Hangfire sources复制):

static class TypeExtensions
{
    public static string ToGenericTypeString(this Type type)
    {
        if (!type.GetTypeInfo().IsGenericType)
        {
            return type.GetFullNameWithoutNamespace()
                .ReplacePlusWithDotInNestedTypeName();
        }

        return type.GetGenericTypeDefinition()
            .GetFullNameWithoutNamespace()
            .ReplacePlusWithDotInNestedTypeName()
            .ReplaceGenericParametersInGenericTypeName(type);
    }

    private static string GetFullNameWithoutNamespace(this Type type)
    {
        if (type.IsGenericParameter)
        {
            return type.Name;
        }

        const int dotLength = 1;
        // ReSharper disable once PossibleNullReferenceException
        return !String.IsNullOrEmpty(type.Namespace)
            ? type.FullName.Substring(type.Namespace.Length + dotLength)
            : type.FullName;
    }

    private static string ReplacePlusWithDotInNestedTypeName(this string typeName)
    {
        return typeName.Replace('+', '.');
    }

    private static string ReplaceGenericParametersInGenericTypeName(this string typeName, Type type)
    {
        var genericArguments = type.GetTypeInfo().GetAllGenericArguments();

        const string regexForGenericArguments = @"`[1-9]\d*";

        var rgx = new Regex(regexForGenericArguments);

        typeName = rgx.Replace(typeName, match =>
        {
            var currentGenericArgumentNumbers = int.Parse(match.Value.Substring(1));
            var currentArguments = string.Join(",", genericArguments.Take(currentGenericArgumentNumbers).Select(ToGenericTypeString));
            genericArguments = genericArguments.Skip(currentGenericArgumentNumbers).ToArray();
            return string.Concat("<", currentArguments, ">");
        });

        return typeName;
    }

    public static Type[] GetAllGenericArguments(this TypeInfo type)
    {
        return type.GenericTypeArguments.Length > 0 ? type.GenericTypeArguments : type.GenericTypeParameters;
    }
}

<强> RecurringJobManagerExtensions:

public static class RecurringJobManagerExtensions
{
    public static void AddOrUpdate(this IRecurringJobManager manager, Expression<Action> methodCall, Func<string> cronExpression, TimeZoneInfo timeZone = null, string queue = EnqueuedState.DefaultQueue)
    {
        var job = Job.FromExpression(methodCall);
        var id = $"{job.Type.ToGenericTypeString()}.{job.Method.Name}";

        manager.AddOrUpdate(id, job, cronExpression(), timeZone ?? TimeZoneInfo.Utc, queue);
    }
}

已注入IRecurringJobManager的控制器:

public class SubmissionController : Controller
{
    private readonly IRecurringJobManager recurringJobManager;

    public SubmissionController(IRecurringJobManager recurringJobManager)
    {
        this.recurringJobManager = recurringJobManager;
    }

    public IActionResult Post()
    {
        recurringJobManager.AddOrUpdate(() => InitiateSubmission(), Cron.Minutely);

        return Ok("Periodic submission triggered");
    }

    public void InitiateSubmission()
    {
        // ...
    }
}

嗯,这种方法可行,但我并不喜欢它。它基于一些内部Hangfire的东西,可以在将来改变。

这就是我建议使用其他方法的原因。您可以添加新的外观界面(例如IRecurringJobFacade),它将模仿您将要使用的RecurringJob方法。此接口的实现只会调用相应的RecurringJob方法。然后将此IRecurringJobFacade注入控制器,并可以在UT中轻松模拟它。这是一个示例:

<强> IRecurringJobFacade:

public interface IRecurringJobFacade
{
    void AddOrUpdate(Expression<Action> methodCall, Func<string> cronExpression);

    //  Mimic other methods from RecurringJob that you are going to use.
    // ...
}

<强> RecurringJobFacade:

public class RecurringJobFacade : IRecurringJobFacade
{
    public void AddOrUpdate(Expression<Action> methodCall, Func<string> cronExpression)
    {
        RecurringJob.AddOrUpdate(methodCall, cronExpression);
    }
}

已注入IRecurringJobFacade的控制器:

public class SubmissionController : Controller
{
    private readonly IRecurringJobFacade recurringJobFacade;

    public SubmissionController(IRecurringJobFacade recurringJobFacade)
    {
        this.recurringJobFacade = recurringJobFacade;
    }

    public IActionResult Post()
    {
        recurringJobFacade.AddOrUpdate(() => InitiateSubmission(), Cron.Minutely);

        return Ok("Periodic submission triggered");
    }

    public void InitiateSubmission()
    {
        // ...
    }
}

正如您所看到的,这种方法更简单,最重要的是它更可靠,因为它不会像往常那样深入了解Hangfire内部并只调用RecurringJob方法。

当代码无法直接模拟时(静态方法或不基于接口的类),通常会使用这种外观接口。我在练习中使用的其他一些示例:模拟System.IO.FileDateTime.NowSystem.Timers.Timer等。