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

如何结合Signal R与EF Core实现WPF MVVM多用户数据同步?

问题背景

我正在开发一款MVVM架构的WPF应用,支持多用户同时使用不同实例。当用户修改本地数据时,所有实例的数据需同步更新,同时要通过EF Core将数据存储到SQL数据库。此前采用客户端轮询服务器获取数据的方式,现在希望改用SignalR替代轮询。

初始实现代码:

app.MapPost("UpdateFruits", async (string message, IHubContext<ChatHub> context) =>
{
    await context.Clients.All.SendAsync("ReceiveMessage","FromServer", message);
    return Results.NoContent();
});

更新后的代码:

app.MapPut("UpdateFruits", async (string message, IHubContext<ChatHub> context, DatabaseContext dbContext) =>
{
    //message(Param) = { "ID":"2","DisplayName":"Fruit","Leaf":null,"State":"Chopped"}

    try
    {
        using var transaction = dbContext.Database.BeginTransaction();
        var fruitIn = JsonConvert.DeserializeObject<Fruit>(message);

        if (fruitIn == null)
        {
            return Results.NotFound($"Error parsing");
        }

        var dbFruitIn = await dbContext
            .Set<Fruit>()
            .FirstOrDefaultAsync(c => c.ID == fruitIn.ID);

        if (dbContext == null)
        {
            return Results.NotFound($"Fruit id: {dbFruitIn.ID}");
        }

        dbFruitIn.DisplayName = fruitIn.DisplayName;
        dbFruitIn.State = fruitIn.State;

        await dbContext.SaveChangesAsync();

        var jsonString = JsonConvert.SerializeObject(dbFruitIn);

        await context.Clients.All.SendAsync("ReceiveMessage", "FromServer", jsonString); 
        transaction.Commit();
    }
    catch (Exception)
    {
        transaction.Rollback();
    }
    return Results.NoContent();
});

相关C#实体类定义:

public class Fruit
{
    public int ID { get; set; }
    public string DisplayName { get; set; } = String.Empty;
    public List<Leaf>? Leaf{ get; set; }
}

public class Leaf
{
    public int ID { get; set; }
    public string DisplayName { get; set; } = String.Empty;
    public List<Vein>? Vein{ get; set; }
}

public class Vein
{
    public int ID { get; set; }
    public string DisplayName { get; set; } = String.Empty;
}

我有两个问题:

  1. 传递Fruit集合、保存到数据库并同步到所有连接客户端的最佳实践是什么?
  2. 是通过字符串传递JSON并序列化更优,还是可以直接在SignalR中使用Fruit对象?

问题解答

问题2:直接传递Fruit对象 vs JSON字符串

直接使用Fruit对象更优,原因如下:

  • SignalR内置JSON序列化/反序列化机制(默认基于System.Text.Json),无需手动调用JsonConvert,减少代码冗余与出错概率。
  • 强类型传递能在编译阶段发现类型不匹配问题,避免运行时解析错误。
  • 代码可读性更高,无需在字符串与对象间来回转换。

修改示例代码

服务器端接口

将参数从string message改为Fruit fruitIn,ASP.NET Core会自动绑定并反序列化请求体:

app.MapPut("UpdateFruits", async (Fruit fruitIn, IHubContext<ChatHub> context, DatabaseContext dbContext) =>
{
    try
    {
        using var transaction = dbContext.Database.BeginTransaction();

        if (fruitIn == null)
        {
            return Results.BadRequest("无效的Fruit数据");
        }

        var dbFruitIn = await dbContext
            .Set<Fruit>()
            .Include(f => f.Leaf) // 若需关联更新Leaf,需显式Include
            .ThenInclude(l => l.Vein)
            .FirstOrDefaultAsync(c => c.ID == fruitIn.ID);

        if (dbFruitIn == null) // 原代码此处判断错误,修正为检查dbFruitIn
        {
            return Results.NotFound($"未找到ID为{fruitIn.ID}的Fruit");
        }

        // 更新属性,可使用AutoMapper简化赋值逻辑
        dbFruitIn.DisplayName = fruitIn.DisplayName;
        // 注意:原Fruit实体类无State属性,需确认是否要添加该字段
        // dbFruitIn.State = fruitIn.State;

        // 处理关联集合(示例为全量替换Leaf,需根据业务逻辑调整)
        if (fruitIn.Leaf != null)
        {
            dbContext.RemoveRange(dbFruitIn.Leaf);
            dbFruitIn.Leaf = fruitIn.Leaf;
        }

        await dbContext.SaveChangesAsync();

        // 直接发送Fruit对象,SignalR自动完成序列化
        await context.Clients.All.SendAsync("ReceiveFruitUpdate", dbFruitIn); 
        transaction.Commit();
    }
    catch (Exception ex)
    {
        // 建议添加日志记录
        // _logger.LogError(ex, "更新Fruit失败");
        return Results.StatusCode(StatusCodes.Status500InternalServerError);
    }
    return Results.NoContent();
});

WPF客户端接收

直接接收强类型对象并更新UI:

// 假设已初始化HubConnection
_hubConnection.On<Fruit>("ReceiveFruitUpdate", (updatedFruit) =>
{
    // 切换到UI线程更新MVVM绑定数据
    Application.Current.Dispatcher.Invoke(() =>
    {
        var existingFruit = YourObservableCollection.FirstOrDefault(f => f.ID == updatedFruit.ID);
        if (existingFruit != null)
        {
            existingFruit.DisplayName = updatedFruit.DisplayName;
            existingFruit.Leaf = updatedFruit.Leaf;
        }
        else
        {
            YourObservableCollection.Add(updatedFruit);
        }
    });
});

问题1:传递Fruit集合、保存数据库并同步的最佳实践

1. 数据持久化层面

  • 批量操作优化:批量更新时使用dbContext.UpdateRange(fruits)替代单条更新,减少数据库往返次数。
  • 关联数据处理:针对Leaf、Vein等嵌套集合,明确业务逻辑:
    • 全量替换:先删除旧关联数据,再添加新数据;或使用EF Core的TrackGraph处理整个对象图状态。
    • 增量更新:对比本地与数据库集合差异,仅增删改变化项,避免不必要的数据库操作。
  • 事务与异常处理:用事务包裹数据库操作与SignalR推送,确保操作原子性;同时记录异常日志,便于排查问题。

2. SignalR同步层面

  • 精准推送:无需全量推送时,按用户权限或分组发送(如context.Clients.Group("FruitGroup").SendAsync(...)),减少无效网络传输。
  • 增量更新:避免发送完整对象,定义仅包含变更字段的FruitUpdateDto,缩小Payload大小。
  • 并发冲突处理:在Fruit类中添加RowVersion属性,利用EF Core乐观并发控制,防止多用户同时修改导致的数据覆盖。

3. 批量更新示例代码

app.MapPut("UpdateFruitsBatch", async (List<Fruit> fruits, IHubContext<ChatHub> context, DatabaseContext dbContext) =>
{
    try
    {
        using var transaction = dbContext.Database.BeginTransaction();

        if (fruits == null || !fruits.Any())
        {
            return Results.BadRequest("无效的Fruit集合");
        }

        var fruitIds = fruits.Select(f => f.ID).ToList();
        var dbFruits = await dbContext
            .Set<Fruit>()
            .Include(f => f.Leaf)
            .ThenInclude(l => l.Vein)
            .Where(f => fruitIds.Contains(f.ID))
            .ToListAsync();

        foreach (var fruit in fruits)
        {
            var dbFruit = dbFruits.FirstOrDefault(f => f.ID == fruit.ID);
            if (dbFruit != null)
            {
                dbFruit.DisplayName = fruit.DisplayName;
                // 全量替换Leaf集合(根据业务调整逻辑)
                dbContext.RemoveRange(dbFruit.Leaf);
                dbFruit.Leaf = fruit.Leaf;
            }
        }

        await dbContext.SaveChangesAsync();

        // 发送批量更新通知
        await context.Clients.All.SendAsync("ReceiveFruitsBatchUpdate", dbFruits);
        transaction.Commit();
    }
    catch (DbUpdateConcurrencyException)
    {
        return Results.Conflict("数据已被其他用户修改,请刷新后重试");
    }
    catch (Exception ex)
    {
        // 记录日志
        return Results.StatusCode(StatusCodes.Status500InternalServerError);
    }
    return Results.NoContent();
});

4. 额外建议

  • 使用DTO:定义专门的FruitDto、LeafDto,仅包含客户端所需字段,避免暴露敏感数据与不必要关联。
  • 客户端缓存管理:本地维护数据缓存,接收SignalR通知时仅更新变化数据,提升UI响应速度。
  • SignalR连接重连:在WPF客户端实现断开重连逻辑,确保网络波动时自动恢复连接,不影响用户体验。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 04:35:03