在最近的开发中,我遇到了这样一个需求:一次 HTTP 请求中,需要根据业务场景判断用户数据是否完整。如果数据不完整,逻辑流程需要暂停,提示用户补充必要信息。等用户补全数据后,系统再继续执行剩下的操作。
这个需求在传统的 ASP.NET 应用中并不常见,因为 HTTP 请求通常是无状态且一次性处理完毕的,中途“暂停再继续”的流程并不容易实现。接下来我将分享我是如何在 ASP.NET 中实现这个功能的。后端代码:
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Caching.Distributed;
using System.Collections.Concurrent;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDistributedMemoryCache();
builder.Services.AddSingleton<PendingRequestManager>();
builder.Services.AddControllers();
builder.Services.AddSignalR();
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(policy =>
{
policy.AllowAnyOrigin();
});
});
var app = builder.Build();
app.UseCors();
app.MapControllers();
app.MapHub<NotifyHub>("/notifyHub");
app.UseStaticFiles();
app.MapPost("consume", async (string userId, PendingRequestManager manager, IDistributedCache cache, IHubContext<NotifyHub> hubContext, CancellationToken cancellationToken) =>
{
var tcs = manager.Create(userId);
await hubContext.Clients.Group(userId).SendAsync("NeedSupplement", "服务器返回:请补充数据哦!!!!");
// 挂起请求,等待补充,最多300秒
var finished = await Task.WhenAny(tcs.Task, Task.Delay(TimeSpan.FromSeconds(300)));
var message = await cache.GetStringAsync(userId, cancellationToken);
Console.WriteLine(message);
if (finished != tcs.Task)
{
manager.Remove(userId);
return Results.BadRequest("服务器返回:等待超时");
}
return Results.Ok("服务器返回:处理成功!");
});
app.MapPost("/supplement", async (string userId, DataMessage dataMessage, IDistributedCache cache, PendingRequestManager manager, CancellationToken cancellationToken) =>
{
await cache.SetStringAsync(userId, dataMessage.Data, new DistributedCacheEntryOptions { SlidingExpiration = TimeSpan.FromSeconds(2) }, cancellationToken);
// 实际上你应该验证数据并补充
var tcs = manager.Get(userId);
if (tcs != null)
{
tcs.SetResult(true);
manager.Remove(userId);
}
return Results.Ok("服务器返回:发送补充成功");
});
app.Run();
public class PendingRequestManager
{
private ConcurrentDictionary<string, TaskCompletionSource<bool>> _pendingRequests = new();
public TaskCompletionSource<bool> Create(string userId)
{
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
_pendingRequests[userId] = tcs;
return tcs;
}
public TaskCompletionSource<bool>? Get(string userId)
{
_pendingRequests.TryGetValue(userId, out var tcs);
return tcs;
}
public void Remove(string userId)
{
_pendingRequests.TryRemove(userId, out _);
}
}
public class NotifyHub : Hub
{
public override Task OnConnectedAsync()
{
var userId = Context.GetHttpContext()?.Request.Query["userId"];
if (!string.IsNullOrEmpty(userId))
{
Groups.AddToGroupAsync(Context.ConnectionId, userId);
}
return base.OnConnectedAsync();
}
}
public class DataMessage
{
public string Data { get; set; }
}
前端代码:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>SignalR补数据Demo</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/7.0.5/signalr.min.js"></script>
</head>
<body>
<button id="consume">开始处理数据</button>
<br />
<textarea id="data" style="display:none;" rows="4" cols="50">我是浏览器上补的数据Data</textarea>
<br />
<button id="supplement" style="display:none;">开始补数据</button>
<div id="msg"></div>
<script>
const userId = "user1";
let connection = new signalR.HubConnectionBuilder()
.withUrl("/notifyHub?userId=" + userId)
.build();
connection.on("NeedSupplement", function (message) {
document.getElementById('msg').innerText = message;
document.getElementById('supplement').style.display = "";
document.getElementById('data').style.display = "";
});
connection.start().then(function () {
console.log("SignalR Connected.");
}).catch(function (err) {
return console.error(err.toString());
});
document.getElementById('consume').onclick = function() {
fetch('/consume?userId=' + userId, {
method: 'POST'
})
.then(resp => resp.json())
.then(data => {
document.getElementById('msg').innerText = JSON.stringify(data);
});
};
document.getElementById('supplement').onclick = function() {
fetch('/supplement?userId=' + userId, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
data: document.getElementById('data').value,
})
})
.then(resp => resp.json())
.then(data => {
document.getElementById('msg').innerText = JSON.stringify(data);
document.getElementById('supplement').style.display = "none";
document.getElementById('data').style.display = "none";
});
};
</script>
</body>
</html>
TaskCompletionSource是什么?
- TaskCompletionSource<T> 是 .NET 提供的一个可以手动控制 Task 何时完成的对象。
- 用它你可以在某处 await 一个 Task,而在别的地方(甚至别的方法/线程)决定 Task 什么时候完成(SetResult/SetException/SetCanceled)。
- 适用于异步等待外部事件/条件的场景。
声明:来自硅基-桂迹,仅代表创作者观点。链接:https://eyangzhen.com/1893.html