gpt4 book ai didi

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

转载 作者:行者123 更新时间:2023-11-30 14:06:16 40 4
gpt4 key购买 nike

考虑以下 Controller :

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

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

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

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

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

最佳答案

如果您检查 source code RecurringJob 类,您会看到它的静态方法导致调用 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 接口(interface),您可以在 UT 中使用它进行依赖注入(inject)和模拟。

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

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

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

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);
}
}

注入(inject)IRecurringJobManager的 Controller :

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 内容。

这就是为什么我建议使用另一种方法。您可以添加新的外观接口(interface)(例如 IRecurringJobFacade),它将模仿您将要使用的 RecurringJob 中的方法。该接口(interface)的实现只会调用相应的 RecurringJob 方法。然后,您将此 IRecurringJobFacade 注入(inject)到 Controller 中,并且可以轻松地在 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);
}
}

注入(inject)IRecurringJobFacade的 Controller :

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 方法。

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

关于c# - 在 .Net Core 2 中模拟 Hangfire RecurringJob 依赖,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/49136133/

40 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com