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

Blazor Server应用中基于Entity Framework实现多用户共享读写数据的方案咨询

Blazor Server 多用户共享内存数据 + EF 持久化方案实现

你的方案完全可行,尤其适合你提到的少量并发冲突可接受的场景,核心思路「内存缓存共享数据+定期/按需持久化」刚好匹配Blazor Server的会话特性和EF的持久化能力。下面是结合Blazor和EF的简便实现方法:

1. 内存共享数据管理:单例服务

创建一个单例的共享数据管理服务,用来按分组(如部门)缓存数据并管理生命周期:

  • 定义一个CachedData<T>类,封装实体数据及生命周期信息:
    public class CachedData<T>
    {
        public T Data { get; set; }
        public DateTime LastAccessed { get; set; }
        public int UserCount { get; set; }
    }
    
  • 实现SharedDataManager<T>单例服务,用ConcurrentDictionary保证线程安全:
    public class SharedDataManager<T> where T : class
    {
        private readonly ConcurrentDictionary<string, CachedData<T>> _cache = new();
        private readonly IDbContextFactory<YourDbContext> _dbContextFactory;
        private readonly Timer _persistTimer;
    
        public SharedDataManager(IDbContextFactory<YourDbContext> dbContextFactory)
        {
            _dbContextFactory = dbContextFactory;
            // 启动定时持久化任务,示例为每5分钟执行一次
            _persistTimer = new Timer(async _ => await PersistIdleDataAsync(), null, TimeSpan.Zero, TimeSpan.FromMinutes(5));
        }
    
        // 获取或加载分组数据
        public async Task<T> GetOrLoadDataAsync(string groupKey, Func<YourDbContext, Task<T>> loadFunc)
        {
            if (_cache.TryGetValue(groupKey, out var cachedItem))
            {
                cachedItem.LastAccessed = DateTime.Now;
                cachedItem.UserCount++;
                return cachedItem.Data;
            }
    
            // 从数据库加载数据(使用新的DbContext实例)
            using var dbContext = _dbContextFactory.CreateDbContext();
            var data = await loadFunc(dbContext);
            var newCachedItem = new CachedData<T> { Data = data, LastAccessed = DateTime.Now, UserCount = 1 };
            _cache.TryAdd(groupKey, newCachedItem);
            return data;
        }
    
        // 减少用户计数(组件销毁时调用)
        public void DecrementUserCount(string groupKey)
        {
            if (_cache.TryGetValue(groupKey, out var cachedItem))
            {
                cachedItem.UserCount--;
                cachedItem.LastAccessed = DateTime.Now;
            }
        }
    
        // 手动保存数据到数据库
        public async Task SaveDataAsync(string groupKey)
        {
            if (_cache.TryGetValue(groupKey, out var cachedItem))
            {
                using var dbContext = _dbContextFactory.CreateDbContext();
                dbContext.Set<T>().Update(cachedItem.Data);
                await dbContext.SaveChangesAsync();
                cachedItem.LastAccessed = DateTime.Now;
            }
        }
    
        // 持久化闲置数据(超时或无用户访问)
        private async Task PersistIdleDataAsync()
        {
            var idleKeys = _cache.Where(kv => 
                    kv.Value.UserCount == 0 || 
                    DateTime.Now - kv.Value.LastAccessed > TimeSpan.FromMinutes(30))
                .Select(kv => kv.Key)
                .ToList();
    
            foreach (var key in idleKeys)
            {
                if (_cache.TryRemove(key, out var cachedItem))
                {
                    using var dbContext = _dbContextFactory.CreateDbContext();
                    dbContext.Set<T>().Update(cachedItem.Data);
                    await dbContext.SaveChangesAsync();
                }
            }
        }
    }
    
  • 在Program.cs中注册为单例:
    builder.Services.AddSingleton(typeof(SharedDataManager<>));
    

2. 实时UI更新:状态同步

利用Blazor的组件生命周期和事件订阅实现UI自动刷新:

  • 让共享的实体类实现INotifyPropertyChanged接口,触发属性变更通知:
    public class DepartmentData : INotifyPropertyChanged
    {
        private string _name;
        public string Name
        {
            get => _name;
            set
            {
                if (_name != value)
                {
                    _name = value;
                    OnPropertyChanged(nameof(Name));
                }
            }
        }
    
        public event PropertyChangedEventHandler PropertyChanged;
        protected void OnPropertyChanged(string propertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
    
  • 在Blazor组件中注入服务并订阅变更:
    @inject SharedDataManager<DepartmentData> DataManager
    @implements IDisposable
    
    <input @bind="DepartmentData.Name" @bind:event="oninput" placeholder="部门名称" />
    <button @onclick="SaveChanges" class="ml-2">手动保存</button>
    
    @code {
        private DepartmentData DepartmentData { get; set; }
        private string _groupKey; // 从用户部门属性动态获取,示例固定为"Dept001"
    
        protected override async Task OnInitializedAsync()
        {
            _groupKey = "Dept001"; 
            DepartmentData = await DataManager.GetOrLoadDataAsync(_groupKey, db => 
                db.DepartmentData.AsNoTracking().FirstAsync(d => d.Id == _groupKey));
            DepartmentData.PropertyChanged += OnDataChanged;
        }
    
        private void OnDataChanged(object sender, PropertyChangedEventArgs e)
        {
            StateHasChanged();
        }
    
        private async Task SaveChanges()
        {
            await DataManager.SaveDataAsync(_groupKey);
        }
    
        public void Dispose()
        {
            DepartmentData.PropertyChanged -= OnDataChanged;
            DataManager.DecrementUserCount(_groupKey);
        }
    }
    

3. EF 使用关键注意事项

  • 绝对不要在单例服务中持有DbContext实例,必须通过IDbContextFactory每次创建新的上下文,避免线程安全问题和跟踪冲突。
  • 加载数据时建议使用AsNoTracking(),因为内存中的数据是脱离EF跟踪的,后续更新时直接调用Update()即可。

额外提示

  • 如果想进一步减少冲突,可以给实体加Version字段,启用EF的乐观锁:
    [Timestamp]
    public byte[] Version { get; set; }
    
  • 可根据业务需求调整缓存超时时间和定时持久化间隔。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.28 17:07:54