You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ASP.NET Core后台服务调用EF Core LINQ查询的安全性与问题排查

医疗Web应用后台任务与LINQ查询问题

注:本问题与之前某问题相关,因原问题评论未解决问题,故提出新问题,非重复提问。

背景概述

在基于ASP.NET Core、SQL Server、EF Core的医疗类Web应用中,通过LINQ查询获取所有需通知客户的周期性体检提醒数据,查询代码如下:

_context.Tests
.Where(t => t.Visit.User.SMSAccount != null
       && t.Visit.User.SMSAccount.AutoReminder
       && t.Visit.Customer.Consent
       && (t.Visit.Customer.ContactBy == "Τηλέφωνο κινητό" || t.Visit.Customer.ContactBy == "Email και Κινητό")
       && !string.IsNullOrEmpty(t.Visit.Customer.Mobile)
       && !t.Visit.Pet.PassedAway
       && t.Reminder.HasValue
       && !t.ReminderSMSSent
       && DateTime.Now.Date.AddDays(t.Visit.User.SMSAccount.DaysBefore.Value) == t.Reminder)
.Include(t => t.Visit.User.SMSAccount)
.Include(t => t.Visit.Customer)
.Include(t => t.Visit.Pet)
.AsNoTracking();

该任务由每日正午运行的BackgroundService执行,客户会在提醒日期前DaysBefore天收到通知(例如:提醒日期为2024-01-01,DaysBefore设为7时,通知会在2023-12-25发送)。

后台服务实现

该BackgroundService作为ASP.NET Core Web应用的一部分部署在Windows Server 2012 R2的IIS上,通过创建作用域获取Scoped服务TestService执行上述查询,服务代码如下:

public class SmsSenderService : BackgroundService
{
  public SmsSenderService(IServiceProvider serviceProvider)
  {
    this.logger = logger;
    this.serviceProvider = serviceProvider;
  }

  protected override async Task ExecuteAsync(CancellationToken stoppingToken)
  {
    do
    {  
      // ... 等待至正午                
      if (timeToWait == 0)
      {
        using var scope = this.serviceProvider.CreateScope();
        var testService = scope.ServiceProvider.GetRequiredService<TestService>();
        var reminders = await testService.GetSmsRemindersAsync();

        numberOfHours = 24;  // 等待至下次执行的时长
      }

      await Task.Delay(TimeSpan.FromHours(numberOfHours), stoppingToken);            
    }
    while (!stoppingToken.IsCancellationRequested);       
  }  
}  

在Startup.cs的ConfigureServices中注册该后台服务:

services.AddHostedService<SmsSenderService>();

核心问题

  1. 这种实现方式是否适合执行影响所有应用用户的全局任务?另外,该后台服务在Web服务器上的触发时机是什么?是否存在因IIS进程闲置等情况导致服务不运行的可能?
  2. 当前设计是否会影响LINQ查询的行为?我发现查询偶尔无法返回全部数据,但在LinqPad中模拟时能正常获取所有数据(模拟代码及生成的SQL见下文)。部署到Web服务器后偶尔出现数据缺失,可能的原因是什么?

LinqPad模拟代码

void Main()
{
    var dt = new DateTime(2024,1,24);
    
    var data = Tests
    .Where(t => t.Visit.User.UserSMSAccounts != null 
            && t.Visit.User.UserSMSAccounts.FirstOrDefault().AutoReminder
            && t.Visit.Customer.Consent
            && (t.Visit.Customer.ContactBy == "Τηλέφωνο κινητό" || t.Visit.Customer.ContactBy == "Email και Κινητό")
            && !string.IsNullOrEmpty(t.Visit.Customer.Mobile)
            && !t.Visit.Pet.PassedAway
            && t.Reminder.HasValue
            && !t.ReminderSMSSent 
            && dt.Date.AddDays(t.Visit.User.UserSMSAccounts.FirstOrDefault().DaysBefore.Value) == t.Reminder.Value);    
    
    var results = ConvertToReminderSmsList(data);   
    results.Dump();
}

// 可在此定义其他方法、字段、类和命名空间

IEnumerable<ReminderSms> ConvertToReminderSmsList(IEnumerable<Tests> tests)
{
    List<ReminderSms> reminderSmsList = new List<ReminderSms>();
    foreach (Tests test in tests)
    {
        reminderSmsList.Add(ConvertToReminderSms(test));
    }
    return reminderSmsList;
}

ReminderSms ConvertToReminderSms(Tests test)
{
    var reminderSms = new ReminderSms
    {
        Id = test.Id,
        ApiKey = test.Visit.User.UserSMSAccounts.FirstOrDefault().APIKEY,
        SmsType = test.Visit.User.UserSMSAccounts.FirstOrDefault().SMSType,
        Sender = test.Visit.User.UserSMSAccounts.FirstOrDefault().Sender,
        Recipient = test.Visit.Customer.Mobile
    };

    StringBuilder smsMessage = new StringBuilder();
    smsMessage.Append(test.Visit.User.UserSMSAccounts.FirstOrDefault().Message);
    reminderSms.Message = smsMessage.ToString();
    return reminderSms;
}

class ReminderSms
{
    public int Id { get; set; }

    public string Sender { get; set; }

    public string Recipient { get; set; }

    public string Message { get; set; }

    public string ApiKey { get; set; }

    public string SmsType { get; set; }
}

生成的对应SQL

-- Region Parameters
-- @__dt_Date_0='2023-10-22T00:00:00'
-- EndRegion
SELECT [t].[Id], [t].[Comments], [t].[LimitSet], [t].[Reminder], [t].[ReminderCustomerNotified], [t].[ReminderSMSSent], [t].[Results], [t].[Stamp], [t].[Status], [t].[Type], [t].[VisitId]
FROM [Tests] AS [t]
INNER JOIN [Visits] AS [v] ON [t].[VisitId] = [v].[Id]
INNER JOIN [AspNetUsers] AS [a] ON [v].[UserId] = [a].[Id]
INNER JOIN [Customers] AS [c] ON [v].[CustomerId] = [c].[Id]
LEFT JOIN [Pets] AS [p] ON [v].[PetId] = [p].[Id]
WHERE ((((((((
    SELECT TOP(1) [s].[AutoReminder]
    FROM [SMSAccounts] AS [s]
    WHERE [a].[Id] = [s].[UserId]) = CAST(1 AS bit)) AND ([c].[Consent] = CAST(1 AS bit))) AND (([c].[ContactBy] = N'Τηλέφωνο κινητό') OR ([c].[ContactBy] = N'Email και Κινητό'))) AND [c].[Mobile] IS NOT NULL) AND ([p].[PassedAway] <> CAST(1 AS bit))) AND [t].[Reminder] IS NOT NULL) AND ([t].[ReminderSMSSent] <> CAST(1 AS bit))) AND ((DATEADD(day, CAST(CAST((
    SELECT TOP(1) [s0].[DaysBefore]
    FROM [SMSAccounts] AS [s0]
    WHERE [a].[Id] = [s0].[UserId]) AS float) AS int), @__dt_Date_0) = [t].[Reminder]) OR (DATEADD(day, CAST(CAST((
    SELECT TOP(1) [s0].[DaysBefore]
    FROM [SMSAccounts] AS [s0]
    WHERE [a].[Id] = [s0].[UserId]) AS float) AS int), @__dt_Date_0) IS NULL AND [t].[Reminder] IS NULL))

更新信息:DbContext配置

后台服务使用应用的DbContext,其在Startup.cs的ConfigureServices中配置如下:

services.AddDbContextPool<AppDbContext>(options => options.UseSqlServer(
    Configuration.GetConnectionString("ConnectionString"),
    sqlServerOptions => sqlServerOptions.CommandTimeout(90))
);

通过构造函数依赖注入将DbContext传入仓储类。

流程序列图

流程序列图


内容的提问来源于stack exchange,提问作者dpant

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 17:03:10