【问题标题】:Blazor + SQLTableDependency + SignalR: Notify specific groups from OnChange eventBlazor + SQLTableDependency + SignalR:从 OnChange 事件通知特定组
【发布时间】:2020-09-03 15:19:43
【问题描述】:

我有一个 Blazor 应用程序,它使用 SQLTableDependency 检测数据库更改,然后通过 SignalR 通知所有客户端有关更改。这可行,但我需要一种能够检测更改并仅通知特定 SignalR 组的方法。因为 SQLTableDependency 不关心谁在数据库中插入、更改或删除了一条记录,所以我也不确定如何知道哪个组发送更新。请参阅下文,了解有关我的应用程序以及我想要完成的任务的更多详细信息。

我们为每个客户建立一个新的组织。一个组织有自己的资产列表,并且可以有多个用户。

Organization.cs

    public class Organization
    {
    public int OrganizationId { get; set; }

    public string OrganizationName { get; set; }

    public List<Asset> Assets { get; set; }

    public List<ApplicationUser> Users { get; set; }

    public bool IsDisabled { get; set; }

   }

资产.cs

public class Asset
{
    public int AssetId { get; set; }

    public string SerialNumber { get; set; }

    public int OrganizationId { get; set; }

    public virtual Organization Organization { get; set; }

    public DateTime DateAdded { get; set; }
}

ApplicationUser.cs

 public class ApplicationUser 
{
    public string FirstName { get; set; }

    public string LastName { get; set; }

    public int OrganizationId { get; set; }

    public virtual Organization Organization { get; set; }

    public List<Connection> Connections { get; set; }

    public string Timezone { get; set; }

}

Connection.cs - 我将每个 SignalR 连接存储在数据库中。

    public class Connection
    {
    public string ConnectionId { get; set; }

    public string UserName { get; set; }

    public bool Connected { get; set; }

    public string Group { get; set; }

    public DateTime ConnectionTime { get; set; }

    }

资产服务.cs

    public class AssetService : IAssetService
{
    private readonly IServiceScopeFactory _serviceScopeFactory;

    public AssetService(IServiceScopeFactory serviceScopeFactory)
    {
        _serviceScopeFactory = serviceScopeFactory;
    }
  
         public async Task<Asset> AddAssetAsync(Asset asset, string currentUserName)
    {
        try
        {
            using (var scope = _serviceScopeFactory.CreateScope())
            {
                var db = scope.ServiceProvider.GetService<DataContext>();

                if (asset.Device != null)
                {
                    db.Entry(asset.Device).State = EntityState.Modified;
                }
                asset.DateAdded = DateTime.UtcNow;
                await db.Assets.AddAsync(asset);
                await db.SaveChangesAsync();
                return asset;
            }
        }
        catch (System.Exception ex)
        {
           throw ex;
        }
    }
}

AssetHub.cs - SignalR 集线器

 public class ChatHub : Hub
{
    private readonly UserManager<ApplicationUser> _userManager;
    private readonly IServiceScopeFactory _serviceScopeFactory;

    public ChatHub(UserManager<ApplicationUser> userManager, IServiceScopeFactory serviceScopeFactory)
    {
        _userManager = userManager;
        _serviceScopeFactory = serviceScopeFactory;
    }

    public async Task SendAssetToGroup(string userName, string location, Asset asset)
    {

        if (!string.IsNullOrWhiteSpace(userName))
        {
            var user = await _userManager.Users.Include(x => x.Connections).SingleAsync(x => x.UserName == userName);

            if (user != null)
            {
                var group = $"{user.AccountId}-{location}";

                await Clients.Group(group).SendAsync("AssetUpdate", user.Email, asset);
            }
        }
    }

    public override async Task OnConnectedAsync()
    {
        var httpContext = Context.GetHttpContext();
        var location = httpContext.Request.Query["location"];

        using (var scope = _serviceScopeFactory.CreateScope())
        {
            var db = scope.ServiceProvider.GetService<ApplicationDbContext>();
            if (!string.IsNullOrWhiteSpace(userName))
            {

                var user = await db.Users.Include(x => x.Connections).SingleAsync(x => x.UserName == httpContext.User.Identity.Name);

                if (user != null)
                {
                    var group = $"{user.OrganizationId}-{location}";
                    var connection = new Connection { Connected = true, ConnectionId = Context.ConnectionId, Group = group, UserName = user.UserName };

                    await Groups.AddToGroupAsync(connection.ConnectionId, group);

                    user.Connections.Add(connection);

                    db.Users.Update(user);
                }
            }
           
            await db.SaveChangesAsync();
        }
        await base.OnConnectedAsync();
    }

    public override async Task OnDisconnectedAsync(Exception exception)
    {
        if (!string.IsNullOrWhiteSpace(Context.ConnectionId))
        {
            using (var scope = _serviceScopeFactory.CreateScope())
            {
                var db = scope.ServiceProvider.GetService<ApplicationDbContext>();

                var connection = await db.Connections.Where(x => x.ConnectionId == 
                Context.ConnectionId).FirstOrDefaultAsync();

                if (connection != null)
                {
                    await Groups.RemoveFromGroupAsync(connection.ConnectionId, connection.Group);
                    db.Connections.Remove(connection);
                    await db.SaveChangesAsync();
                }
            }
        }

        await base.OnDisconnectedAsync(exception);
    }
}

AssetTableChangeService.cs - 这是我需要帮助的地方。当 SQLTableDependency 检测到 Assets 表发生变化时,我需要能够调用 AssetHub 中的 SendAssetToGroup 方法。由于用户不属于组织,因此我不想将更新推送到所有组织,我只想将更新仅发送给特定组织组之外的用户。

 public class AssetTableChangeService : IAssetTableChangeService
{
    private const string TableName = "Assets";
    private SqlTableDependency<Asset> _notifier;
    private IConfiguration _configuration;

    public event AssetChangeDelegate OnAssetChanged;

    public StockTableChangeService(IConfiguration configuration)
    {
        _configuration = configuration;

        // SqlTableDependency will trigger an event 
        // for any record change on monitored table  
        _notifier = new SqlTableDependency<Asset>(
             _configuration.GetConnectionString("DefaultConnection"),
             TableName);
        _notifier.OnChanged += AssetChanged;
        _notifier.Start();
    }

    private void AssetChanged(object sender, RecordChangedEventArgs<Asset> e)
    {

        OnAssetChanged.Invoke(this, new AssetChangeEventArgs(e.Entity, e.EntityOldValues));
    }

    public void Dispose()
    {
        _notifier.Stop();
        _notifier.Dispose();
    }

所以流程应该是这样的。

  1. 用户登录 - 通过 SignalR 建立连接
  2. 连接信息存储在数据库中。
  3. 根据用户连接的页面和 OrganizationId 将连接添加到 SignalR 组。
  4. 用户从 UI 创建新资产。
  5. 在资产服务中调用 AddAsset 方法。
  6. 资产被插入到数据库中。
  7. SQLTableDependency 检测到更改,然后调用 AssetChanged 处理程序方法。
  8. AssetChanged 处理程序方法调用 OnAssetChanged 事件。
  9. AssetHub 需要订阅 OnAssetChanged 事件。
  10. 触发 OnAssetChanged 事件时,AssetHub 中的处理程序方法需要调用 SendAssetToGroup 方法。
  11. 当用户从资产页面导航到另一个页面时,会从数据库中删除 SignalR 连接,并从组中删除该连接。

在第 9 步和第 10 步之前,我已经完成了所有工作。无论如何,是否有可能使这成为可能,因为 SQLTableDependency 不关心谁进行了更改,所以我无法查找更新需要的连接组也被推。有什么想法吗?

【问题讨论】:

  • 你没有提到这是 Blazor-ServerSide 还是 Blazor-WASM - 这很重要

标签: c# .net-core signalr blazor signalr-hub


【解决方案1】:

当 UI 使用一个类时,例如:Student

UI 组件加入了一个名为“Student”或“BlahNamespace.Student”的组。 如果它只是名称的列表,如果它是一个实体,则将名称和另一个组连接起来,并将 ID 作为连接的字符串 "BlahNamespace.Student:201" 在您的情况下,如果数据库从实体知道这一点,您也可以附加组织的名称以获得更精细的粒度。

服务器可以根据操作需要通知组。

我将集线器注入 API 控制器来实现这一点。

就我个人而言,我不会使用信号器服务来传输数据,保持它的轻量级,只是“发出信号”改变。然后客户可以决定如何处理。这样,数据只能通过 API 以一种方式访问​​,并具有所有已配置的安全性。

【讨论】:

  • 当我们的客户端连接到 SignalR 集线器时,它会传递一个查询字符串,其中包含用户在应用程序中的位置。例如,如果用户在 Assets 组件上。它会将“资产”作为位置传递。然后 OnConnectedAsync() 方法连接一个名为 group 的字符串,即 OrganizationId + "-" + location。然后将连接添加到该组。您是说跳过 SQLTableDependency,并从 AddAsset 方法调用事件吗?然后该事件会告诉 SignalR 向所有客户端发出信号以提取更新的记录?我只是想确定我和你在同一个页面上吗?
  • @JaronJohnson 我让每个组件都连接到集线器,而不是每个用户一个连接,但该组件只加入它感兴趣的组。因此,如果服务器更新该组中的项目,它仅在该组中广播,然后只有具有可见组件的用户(订阅该组)才会从 api 获得更新信号(如果他们也选择的话)。
  • @JaronJohnson 如果一个组件是学生列表,它将连接到我的 SyncHub 并加入“学生”组。如果组件代表一个项目,它将加入组“Students:WhateverIdItHas”。服务器在更新学生时仅在“学生”组和 SyncHub 上的“学生:WhateverIDItHas”组中广播。这样只有订阅组的组件才能看到广播。
  • 在您的情况下,显示多个资产的组件可以使用 $"{OrganizationId}:Assets" 的组名。如果它显示单个资产,则名称为 $"Assets:{AssetId}"。
  • 好的,这是有道理的,我会做一些测试,看看我是否能做到这一点。我会告诉你!谢谢!
猜你喜欢
  • 1970-01-01
  • 2021-04-24
  • 2021-11-20
  • 2020-05-16
  • 1970-01-01
  • 1970-01-01
  • 2021-09-09
  • 2021-01-16
  • 2020-05-10
相关资源
最近更新 更多