我有一个用C#+ ASP.NET Core(v2.2)编写的网站,并公开了此API:
POST /api/account/signup
POST /api/account/send-greeting
我的业务策略是在注册后的15分钟之内向用户发送问候语(POST /api/account/send-greeting
。
因此,我需要某种方式来注册此新事件。我虽然有两个选择:
这两种解决方案都有明显的缺点:
对我来说,这项任务听起来很明显。我不确定是否存在更好的解决方案,可以是: 您向他发送消息的外部服务,例如
POST /api/register-to-timed-callback?when=15m&target-url=http://example.com/api/account/send-greeting
我错过了什么?如何以最简单,最有效的方式解决此问题?
答案 0 :(得分:1)
您可以基于IHostedService
创建排队的后台服务。然后,当用户通过后台服务注册并处理该队列时,可以将项目添加到队列中。将项目从队列中拉出时,您会根据时间检查是否已准备好发送。如果是这样,您将命中send-greeting
端点,否则,您将重新排队该项目。 docs提供了此类服务的示例。
队列:
public interface IBackgroundTaskQueue
{
void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem);
Task<Func<CancellationToken, Task>> DequeueAsync(
CancellationToken cancellationToken);
}
public class BackgroundTaskQueue : IBackgroundTaskQueue
{
private ConcurrentQueue<Func<CancellationToken, Task>> _workItems =
new ConcurrentQueue<Func<CancellationToken, Task>>();
private SemaphoreSlim _signal = new SemaphoreSlim(0);
public void QueueBackgroundWorkItem(
Func<CancellationToken, Task> workItem)
{
if (workItem == null)
{
throw new ArgumentNullException(nameof(workItem));
}
_workItems.Enqueue(workItem);
_signal.Release();
}
public async Task<Func<CancellationToken, Task>> DequeueAsync(
CancellationToken cancellationToken)
{
await _signal.WaitAsync(cancellationToken);
_workItems.TryDequeue(out var workItem);
return workItem;
}
}
托管服务:
public class QueuedHostedService : BackgroundService
{
private readonly ILogger _logger;
public QueuedHostedService(IBackgroundTaskQueue taskQueue,
ILoggerFactory loggerFactory)
{
TaskQueue = taskQueue;
_logger = loggerFactory.CreateLogger<QueuedHostedService>();
}
public IBackgroundTaskQueue TaskQueue { get; }
protected async override Task ExecuteAsync(
CancellationToken cancellationToken)
{
_logger.LogInformation("Queued Hosted Service is starting.");
while (!cancellationToken.IsCancellationRequested)
{
var workItem = await TaskQueue.DequeueAsync(cancellationToken);
try
{
await workItem(cancellationToken);
}
catch (Exception ex)
{
_logger.LogError(ex,
$"Error occurred executing {nameof(workItem)}.");
}
}
_logger.LogInformation("Queued Hosted Service is stopping.");
}
}
该代码直接来自文档。它主要支持您的用例,但需要进行一些调整才能使您始终如一。首先,由于有一个时间成分(即,您只想处理队列中已存在15分钟的项目),因此需要将ConcurrentQueue<T>
的类型参数设为可以对日期时间和放进去可以是ValueTuple
或专门为此目的创建的实际对象:由您决定。例如:
ConcurrentQueue<(DateTimeOffset added, Func<CancellationToken, Task> task)>
然后,如果没有经过足够的时间,则需要修改出队逻辑以重新出队:
public async Task<Func<CancellationToken, Task>> DequeueAsync(
CancellationToken cancellationToken)
{
await _signal.WaitAsync(cancellationToken);
_workItems.TryDequeue(out var workItem);
if (DateTimeOffset.UtcNow.AddMinutes(-15) < workItem.added)
{
_workItems.Enqueue(workItem);
return ct => ct.IsCancellationRequested ? Task.FromCanceled(ct) : Task.CompletedTask;;
}
return workItem;
}
还没来的时候,返回的值基本上只是一个伪lambda来满足约束。您可能会返回类似null的内容,但是在处理该函数之前,还需要修改后台服务的ExecuteAsync
方法以添加null检查。
还值得注意的是,示例代码被设计为通用的,并允许您将任何内容排队。因此,由于需要特定于时间的更改,因此应使用更特定的命名:例如ITimedBackgroundTaskQueue
,TimedBackgroundTaskQueue
和TimedQueuedHostedService
。鉴于文档中的示例接口/类实际上将集成到ASP.NET Core 3.0中,这一点尤其正确。