ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Blazor组件式开发实战:基于SqlServer与EF Core的数据管理

Blazor组件式开发实战:基于SqlServer与EF Core的数据管理 简介基于C#与ASP.NET的Blazor组件式开发案例基于Core 6.0框架使用Visual Studio 2022开发适合正在学习Blazor组件复用、数据库操作与前后端分离的.NET开发者。压缩包共169个文件主要类型包括dll程序集、C#逻辑源码、CSS样式、JSON配置文件以及Razor组件文件并附带SQL脚本、说明文档和运行效果截图整体体积约5.1MB目录规范便于按需查阅。目前已有517人学习下载案例围绕数据的新增、删除、修改、查询和明细显示展开所有数据操作均调用同一个公共组件可显著减少组件数量与重复代码直观演示组件参数传递、事件回调以及EF操作SQL Server数据库的核心流程。项目使用SQL Server 2012及以上版本数据库访问采用EF方式适合有一定.NET基础、希望深入理解组件式结构的读者。解压后“说明”文件夹内含运行效果截图和代码说明增删改功能均已测试通过可直接打开项目对照学习也可作为课程设计或Blazor实际项目的参考模板帮助快速掌握组件式开发与数据库交互的落地方法。1. 为什么我要把一个 SqlServer 后台项目做成 Blazor 组件式先交底再动手如果你和我一样常年和 C#、Asp.net、SqlServer 这三个词打交道大概率会遇到一个尴尬场景公司要做一个带数据库增删改查的内部管理系统工期紧团队没人愿意碰前端。我最初对 Blazor 是有偏见的觉得它就是个“套了 C# 皮的网页组件框架”直到我在 .NET 6.0 下把一个完整的 SqlServer 案例从零搭起来才意识到组件式开发真正的价值在于ViewModel 和 UI 状态都在 C# 里后端逻辑和前端渲染逻辑用同一套语言几乎没有“前后端接口对不上”的吵架空间。这个项目全称是“基于 C# Asp.net Blazor 组件式开发的 Blazor 案例”核心就是围绕 Blazor 组件、SqlServer 数据库和 EF Core 数据访问展开适合想从 WebForms 或 Asp.net MVC 迁移过来的 .NET 开发者也适合被前端工程化折腾到崩溃的 C# 上位机开发者。2. 从空项目到数据库连通项目骨架与 EF Core 接入细节2.1 为什么选 Blazor Server 而不是 Blazor WebAssembly这个案例选择的是 Blazor Server 模式。理由非常直接项目里要连 SqlServer数据查询和写入都在服务端完成Blazor Server 通过 SignalR 长连接把 UI 事件传到服务端再由服务端渲染回传整个数据链路都在同一台服务器上不需要像 WebAssembly 那样把数据库连接串暴露到浏览器端。数据安全性和开发成本上Server 模式都更适合企业内部案例。我一般会建议新手优先看 Server 模式原因有三点调试体验接近传统 Asp.net断点能直接打在 .cs 文件里不用像前端那样开 DevTools 疯狂找请求。数据库操作和 UI 事件处理可以写在同一个类里代码量明显比 MVC jQuery 少。部署不需要额外配 Nginx 托管静态文件一个 Asp.net 站点就能跑。2.2 创建项目与依赖注入的三处关键配置先把项目骨架建出来。用 .NET 6.0 的 CLI 创建 Blazor Server 项目命令如下dotnet new blazorserver -n BlazorCase -f net6.0 cd BlazorCase dotnet add package Microsoft.EntityFrameworkCore.SqlServer dotnet add package Microsoft.EntityFrameworkCore.Tools创建完项目后打开 Program.cs你需要把 DbContext 注册到依赖注入容器里同时配置连接串读取逻辑常见做法是using BlazorCase.Data; using Microsoft.EntityFrameworkCore; var builder WebApplication.CreateBuilder(args); // 从 appsettings.json 读取连接串 var connectionString builder.Configuration.GetConnectionString(DefaultConnection); // 注册 DbContext指定 SqlServer 作为数据库提供程序 builder.Services.AddDbContextAppDbContext(options options.UseSqlServer(connectionString)); // Blazor Server 必需注册服务端组件服务 builder.Services.AddRazorPages(); builder.Services.AddServerSideBlazor(); var app builder.Build(); app.UseStaticFiles(); app.UseRouting(); app.MapRazorPages(); app.MapBlazorHub(); app.MapFallbackToPage(/_Host); app.Run();这里有三处必须注意的参数UseSqlServer指定了数据库提供程序如果漏掉它会默认走内存数据库数据一切换就全部丢光AddDbContext的生命周期默认是 Scoped在 Blazor Server 里这正好匹配组件实例的生命周期MapBlazorHub是 SignalR 通道的入口它没有注册的话页面会一直转圈不渲染。2.3 从 SqlServer 生成实体类逆向工程的两个常用参数如果是拿现成数据库做例子不需要手写实体类。我用的是Scaffold-DbContext命令逆向生成dotnet tool install --global dotnet-ef dotnet ef dbcontext scaffold Serverlocalhost;DatabaseBlazorCaseDb;User Idsa;Passwordyour_password;TrustServerCertificateTrue Microsoft.EntityFrameworkCore.SqlServer -o Models敲完这条命令后Models 目录下会自动生成与数据库表对应的实体类。需要注意的是连接串里TrustServerCertificateTrue必须带上否则在 .NET 6.0 且没有正式证书的环境下SqlClient 会因为证书校验失败直接抛异常。如果表比较多想只生成指定表可以追加--table User --table Order这类参数来过滤。提示逆向生成完强烈建议在AppDbContext里检查一下OnModelCreating里的表名映射SqlServer 里的表如果带 Schema 前缀EF Core 可能默认映射到dbo和查询 SQL 不一致时会出现运行时警告。3. 组件式开发的核心机制参数传递、事件回调和组件生命周期3.1 组件拆分的边界什么时候该把页面拆成 Razor 组件很多第一次接触 Blazor 的人会问组件式开发听起来很玄学到底哪些代码应该拆成组件我的判断标准很简单页面上重复出现两次以上的独立 UI 区域就值得拆成组件。比如设备管理页面里的设备状态卡片、工单列表里的状态标签、仪表盘里的数据统计卡片这些都在不同页面复用。最常见的拆法是把一个业务实体的“表单 列表”拆成两个组件一个负责输入一个负责展示。以项目里的“设备台账查询”为例我拆出了三个组件DeviceSearch.razor搜索条件区、DeviceTable.razor结果列表区、DeviceStatusTag.razor状态标签。这样做的价值在于设备状态标签这个组件只接收一个枚举值内部负责把状态值映射为颜色和文字其他页面要显示同样的标签时直接引用即可不用重写样式逻辑。组件的数量不是越多越好。组件拆得太碎参数传递会变得非常啰嗦父组件里一层层往子组件传对象维护成本反而上升。我一般遵守一个原则子组件只关心自己的局部状态父组件负责所有跨组件共享的数据。3.2 组件参数与事件回调的标准写法组件之间通信的两种基本方式是参数传递和事件回调。先看参数传递的写法code { // 组件参数父组件通过这个属性传值进来 [Parameter] public string Title { get; set; } // 级联参数从父组件往下传不用每个子组件手动指定 [CascadingParameter] public DeviceContext Context { get; set; } [Parameter] public EventCallbackstring OnSearchCompleted { get; set; } }[Parameter]属性必须声明成 public 属性Blazor 框架在渲染时会把父组件里写的Titlexxx映射到这个属性上这个过程是自动完成的不需要额外的绑定代码。[CascadingParameter]则用于那些每个子组件都要用的共享对象比如登录用户信息、全局配置对象这类参数如果每个页面都手工传一遍代码会很冗余用级联参数后只需要在顶层组件包一层CascadingValue Valuecontext。事件回调的写法相对特别一些EventCallback本质上是一个封装好的委托子组件触发它时父组件里注册的方法会被框架自动调用。注意EventCallback是泛型类型里面的类型参数表示回调方法接收的参数类型如果你不想传递任何数据可以声明为EventCallback不带泛型参数。3.3 生命周期钩子什么时候查数据什么时候释放资源组件式开发最容易翻车的环节是生命周期。Blazor Server 组件的生命周期和 WebForms 有点像但又不完全一样主要有四个钩子生命周期方法执行时机常见用途OnInitializedAsync组件第一次初始化时查询初始数据OnParametersSet父组件传参更新后根据参数值重新加载数据OnAfterRenderAsync每次渲染完成后调用 JavaScript 操作 DOMIDisposable.Dispose组件销毁时释放订阅、定时器、数据库连接我踩过的一个具体坑是在OnInitializedAsync里查数据库时没有判断组件是不是已经被销毁导致用户快速切换页面时查询线程还在跑返回后去更新一个已经被释放的组件状态直接抛出ObjectDisposedException。解决方式是定义一个CancellationTokenSource在Dispose里取消查询implements IDisposable code { private CancellationTokenSource _cts new CancellationTokenSource(); protected override async Task OnInitializedAsync() { // 把 CancellationToken 传给 EF Core 的 ToListAsync _items await _context.Devices .Where(x x.IsActive) .ToListAsync(_cts.Token); } public void Dispose() { // 组件销毁时主动取消还在跑的数据库查询 _cts.Cancel(); _cts.Dispose(); } }这里把CancellationToken传给ToListAsync是很多人会忽略的细节。EF Core 的异步方法都支持取消令牌一旦组件销毁令牌被取消后查询就会及时终止不会白白占用数据库连接池里的连接这在大并发场景下能明显减少超时报警。4. SqlServer 数据交互实战一个完整模块从表单到数据库落库4.1 表单绑定与验证指定Value和ValueChanged的来龙去脉组件式开发里表单绑定最常用的语法是bind-Value它等价于同时指定Value参数和ValueChanged回调。以项目里的设备信息编辑表单为例EditForm Modeldevice OnValidSubmitHandleValidSubmit DataAnnotationsValidator / ValidationSummary / div classform-group label设备名称/label InputText bind-Valuedevice.DeviceName classform-control / /div div classform-group label所属车间/label InputSelect bind-Valuedevice.Workshop option value请选择车间/option option valueA车间A车间/option option valueB车间B车间/option /InputSelect /div button typesubmit classbtn btn-primary保存/button /EditFormbind-Value在编译后会展开成Valuedevice.DeviceName和ValueChanged(value) device.DeviceName value。理解这一点很重要当你想在绑定同时做额外逻辑时就直接手动声明这两个参数而不是硬套bind-Value。比如设备名称输入后需要立即去掉前后空格可以写成InputText Valuedevice.DeviceName ValueChanged((string val) OnNameChanged(val)) /private void OnNameChanged(string val) { driver.DeviceName val?.Trim() ?? string.Empty; }表单验证用的是DataAnnotationsValidator组件它会在提交时自动校验 Model 上的数据注解。整个机制是异步的校验失败时OnValidSubmit不会触发而OnInvalidSubmit会执行错误信息会展示在ValidationSummary里。4.2 数据列表加载与分页不要一次性查全表项目里查询设备列表时我第一次犯的错误是直接用ToListAsync()把整张表的数据拉到内存导致页面渲染几千行数据浏览器直接卡死。后来改成了服务端分页关键代码长这样public async Task(ListDevice Data, int Total) GetDevicesAsync(int pageIndex, int pageSize, string keyword) { var query _context.Devices.AsNoTracking(); if (!string.IsNullOrWhiteSpace(keyword)) { query query.Where(x x.DeviceName.Contains(keyword) || x.ModelNumber.Contains(keyword)); } var total await query.CountAsync(); var data await query .OrderByDescending(x x.CreatedAt) .Skip((pageIndex - 1) * pageSize) .Take(pageSize) .ToListAsync(); return (data, total); }分页查询有两个细节容易忽略。第一是AsNoTracking()如果查询出来的实体不需要跟踪修改加上这个标记会明显提升查询性能EF Core 不会为返回的实体创建变更跟踪快照第二是CountAsync和ToListAsync要分开执行如果先ToQueryString()再直接ToList会导致每一次翻页都要把全表数据计算完再 SkipSqlServer 的 I/O 压力会成倍增加。EF Core 的Skip和Take最终会生成OFFSET分页 SQL这是 SqlServer 2012 及以上版本的标准实现。组件里调用这个方法时分页参数由 UI 上的页码控件提供private async Task LoadDataAsync() { _isLoading true; var result await _service.GetDevicesAsync(_currentPage, _pageSize, _keyword); _devices result.Data; _totalCount result.Total; _totalPages (int)Math.Ceiling(_totalCount / (double)_pageSize); _isLoading false; }4.3 批量操作与事务多表写入时保证数据一致性在设备管理模块里除了单条增删改还有一种“批量报废”场景选择多条设备统一将它们标记为报废状态同时写入一条操作日志到另一个表。这个场景必须用事务来保证一致性我再三强调这一点是因为很多人只做单表操作没有意识到 EF Core 的SaveChangesAsync默认是隐式事务一次性提交多个脏实体时才自动包裹。涉及多个 DbContext 的写操作时必须手动控制事务public async Task BatchScrapAsync(Listint deviceIds, string operatorName) { await using var transaction await _context.Database.BeginTransactionAsync(); try { var devices await _context.Devices .Where(x deviceIds.Contains(x.Id)) .ToListAsync(); foreach (var device in devices) { device.Status 报废; device.ScrapTime DateTime.Now; } var log new OperationLog { DeviceIds string.Join(,, deviceIds), OperationType 批量报废, Operator operatorName, OperatedAt DateTime.Now }; _context.OperationLogs.Add(log); await _context.SaveChangesAsync(); await transaction.CommitAsync(); } catch (Exception) { // 出任何异常都回滚保证不会出现“设备已报废但日志没写入” await transaction.RollbackAsync(); throw; } }这里的事务写法是从 .NET 6.0 开始推荐的方式BeginTransactionAsync获得的事务对象可以配合await using进行异步释放。需要注意SaveChangesAsync只需要调用一次因为 EF Core 的上下文会跟踪devices的修改和新增的log实体最终生成一个包含 UPDATE 和 INSERT 语句的批量 SQL 提交而不是多次往返数据库。5. Blazor SqlServer 避坑实录五条血泪经验帮你少走一天弯路5.1 连接串里缺TrustServerCertificateTrue部署后连不上数据库现象是本地跑得好好的发布到服务器上后页面一打开就报错错误信息大致是“证书链是由不受信任的颁发机构颁发的”。原因很简单目标服务器上的 SqlServer 使用自签名证书而 .NET 6.0 的 SqlClient 默认要求校验证书。解决方法是把TrustServerCertificateTrue显式写进连接串这一点我在前面已经提到过但值得单独拿出来再说一次如果不想在连接串里明文写密码可以改用ManagedIdentity认证不过在小团队内网环境里直接信任证书更省事。5.2 Blazor Server 页面长时间不操作后重连失败现象是浏览器挂着页面过了一晚第二天点按钮没反应F12 能看到 SignalR 连接状态是Disconnected。原因是 Blazor Server 默认的DisconnectTimeout是 30 秒如果在这段时间内没有重新连上服务器就会释放该电路并销毁组件状态。解决思路有两个方向一是前端定期发送心跳请求保活二是在_Host.cshtml里配置重新连接机制。实际项目中我更倾向把长时间无操作后自动释放这件事保留下来但在Program.cs里把DetailedErrors打开方便排查掉线后恢复失败的具体异常。注意如果页面里有正在编辑的大段表单数据Blazor Server 的掉线会直接导致未保存的数据丢失这是它和 WebAssembly 相比最大的痛点选型时务必考虑到。5.3 组件参数变更后 UI 不刷新数据还是旧值现象是在父组件里更新了传给子组件的对象属性子组件显示的内容没有变化。原因很隐蔽Blazor 的渲染对比是基于组件参数引用的父组件只是修改了传入对象的某个属性对象的引用没有变化框架也就不会通知子组件重新渲染。解决方式有两种一是修改后手动调用StateHasChanged()二是把这部分数据重新赋值成新对象强制引用变更。我第一次遇到这个问题时排查了半天找不到原因后来意识到是引用传递而非值传递的问题。现在写组件参数前都会先问一句这里到底传的是对象引用还是基础类型的值。5.4 EF Core 追踪多个实体时 FK 冲突现象是保存订单时给Order实体赋值了已存在的CustomerId但是在插入 Order 时 EF Core 同时把赋值给它的Customer对象也当作新增实体处理导致主键冲突。原因是在同一个 DbContext 生命周期里EF Core 的变更追踪器把所有到达Added状态的实体都视为新增你手动指定的CustomerId和导航属性引用的Customer产生了冲突。解决方法是手动将导航属性置空只保留外键属性var newOrder new Order { CustomerId existingCustomer.Id, // 不要给 Customer 导航属性赋值 CreatedAt DateTime.Now }; _context.Orders.Add(newOrder); await _context.SaveChangesAsync();5.5 布到 IIS 后 HttpContext 为 null现象是代码里用IHttpContextAccessor获取用户 IP本地运行时正常部署到 IIS 后访问属性直接空引用。原因是 Blazor Server 组件里的作用域是 SignalR 而非 HttpContext多数情况下组件运行在异步回调上下文中没有直接关联的 HttpContext。解决方式是在组件初始化时将需要的 HttpContext 信息如 IP、Claim提取出来存入局部变量protected override void OnInitialized() { var httpContext _httpContextAccessor.HttpContext; if (httpContext ! null) { _currentUser httpContext.User.Identity.Name; _userIp httpContext.Connection.RemoteIpAddress?.ToString(); } }6. 把组件复用做到极致从列表页到编辑页的无缝衔接技巧项目做到后期设备列表和设备编辑是两个独立页面用户每点一次“编辑”就要从列表页跳转到编辑页保存后再跳转回来体验和开发成本都不理想。我在第二个版本里把编辑功能改成了弹窗式组件同时利用EventCallback把保存成功后的列表刷新做成了完全无感知的联动。做法是先在父组件也就是列表页里定义一个布尔状态和一个当前选中实体private bool _showEditDialog; private Device _selectedDevice; private void OpenEditDialog(Device device) { // 传给弹窗组件一个全新的副本避免弹窗内修改直接污染列表数据 _selectedDevice new Device { Id device.Id, DeviceName device.DeviceName, Workshop device.Workshop, ModelNumber device.ModelNumber, Status device.Status }; _showEditDialog true; }弹窗组件内部收到Device类型的参数后编辑表单的绑定目标就是这个对象而父组件已经拿到一个深拷贝副本即使编辑过程中取消了操作父组件列表中的原始数据也不会受影响。保存按钮触发EventCallback时我可以把操作结果告诉父组件button classbtn btn-primary onclickHandleSave保存/buttonprivate async Task HandleSave() { await _service.UpdateDeviceAsync(_device); // 通知父组件刷新列表并关闭弹窗 await OnSaved.InvokeAsync(true); }之后列表页只需要在收到回调后重新查一次数据将_showEditDialog置回false即可。整套流程没有使用任何 JavaScript纯 C# 把状态同步做完这在过去的 Asp.net WebForms 里需要写一堆__doPostBack逻辑在 MVC 里则需要更繁琐的 Ajax 数据拼接。组件式开发这种“后端驱动状态变更”的思维方式是 Blazor 对比传统 Web 开发最大的体验提升点。这个组件复用经验是从一次实际项目教训里提炼出来的当初第一个版本直接在编辑页修改原设备对象结果用户点了取消后列表页显示的名字都已经变了当时我还纳闷是不是缓存问题后来才意识到是组件参数引用导致了数据污染。从那以后我每次编写涉及父组件和子组件共享数据对象的逻辑都会强制走一遍“深拷贝副本 EventCallback 回调”的流程宁可多写两个类也不让 ViewModel 被莫名其妙地改掉。希望这一篇能帮你把 Blazor 组件式开发的脉络盘清楚至少在部署和参数传递这些关键节点上不再走我走过的弯路。本文还有配套的精品资源点击获取
返回列表