简介:面向.NET开发者的ASP.NET Core 6 Web API示例工程,演示如何使用Entity Framework Core构建符合RESTful风格的接口服务。对于需要快速搭建API脚手架或理解对象关系映射的读者,项目从入口配置、依赖注入、数据库上下文、实体模型、数据迁移到控制器路由,均提供了清晰完整的示例代码,覆盖了常见的增删改查操作,可直接作为基础模板复用。资源压缩包仅有13KB,包含16个文件,核心为9个C#源文件,配合3个JSON配置文件和项目工程文件,结构简洁、层次分明,方便逐文件研读。目前已有388人学习下载,适合.NET初学者入门,也适合有经验者快速对照检查自己的项目布局。通过该示例,读者可以重点掌握Entity Framework Core的数据模型映射方式、RESTful控制器编写方法、依赖注入与配置管理技巧,同时还能结合作者的配套博文,理解从项目初始化到数据库操作落地的完整思路,从而在实际开发中减少重复踩坑,快速搭建出高质量的后端服务。
1. 项目背景与整体思路
年前帮一个做园区安防的朋友搭了一套视频集成平台,核心诉求很简单:把海康综合安防管理平台(iSecure Center)里的监控画面,以 HLS 流的形式嵌入到他们自己的 Web 管理系统里。后端选型商量了一圈,最后定了 ASP.NET Core 6 Web API。这篇文章就把整套实现过程整理出来,包含基础框架搭建、接口设计,以及最关键的海康平台 HLS 流获取对接方案,给需要在 .NET 生态里做安防集成的同学提供一个可以直接参考的样例。
先说结论:ASP.NET Core 6 做这种集成类 API 非常合适。理由有三个:一是它本身跨平台,Linux 上部署省授权费;二是内置的 HttpClientFactory 对付海康这种需要频繁带 token 调用的 API 很方便;三是性能上限高,一个几百路摄像头的园区,用 .NET 6 扛并发完全不是问题。
整个项目的设计思路,大致分三块:
- 基础层:JWT 身份认证 + 统一响应格式 + 全局异常处理;
- 业务层:摄像头信息管理、HLS 流地址获取、播放地址转换;
- 对接层:海康 OpenAPI 网关封装、token 管理、HLS 流请求与缓存。
这三层拆开之后,不管后面是要换设备厂商,还是加新的业务模块,都只需要在对应层做改动,不会牵一发动全身。
2. 环境准备与项目初始化
2.1 开发环境与依赖包
我用的是 Visual Studio 2022,装了 .NET 6 SDK。如果你用 Rider 或者 VS Code,操作也差不多,命令行能跑dotnet --version看到 6.x 就行。
除了框架自带的包,还需要引入以下几个 NuGet 包:
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer --version 6.0.16 dotnet add package Swashbuckle.AspNetCore --version 6.5.0 dotnet add package Newtonsoft.Json --version 13.0.3简单说明下每个包的作用:
- JwtBearer:实现基于 JWT 的认证,客户端拿到 token 后访问受保护的接口;
- Swashbuckle:自动生成 Swagger 文档,对接调试时直接浏览器里试接口,不用额外装 POSTMAN;
- Newtonsoft.Json:海康的 OpenAPI 返回体里有些字段命名比较随意,用它做反序列化时控制更灵活。
2.2 创建项目与目录结构
用命令行创建项目,干净利落:
dotnet new webapi -n HikVision.WebApi -f net6.0 cd HikVision.WebApi然后按下面的结构整理目录:
HikVision.WebApi/ ├── Controllers/ │ ├── AuthController.cs │ ├── CameraController.cs │ └── StreamController.cs ├── Models/ │ ├── ApiResponse.cs │ ├── CameraInfo.cs │ └── HikVisionDtos.cs ├── Services/ │ ├── HikVisionService.cs │ └── TokenService.cs ├── Middlewares/ │ └── ExceptionHandlingMiddleware.cs └── appsettings.json提示:Controller 只做参数接收和结果返回,业务逻辑尽量放到 Services 里,这样后面对接别的平台或者写单元测试都会轻松很多。
3. 基础框架搭建:认证、统一响应、异常处理
不管业务是什么,一个 Web API 首先得把"门面"做好。我这里说的门面不是 UI,而是三个基础能力:接口要鉴权、返回格式要统一、报错信息要可控。
3.1 JWT 身份认证配置
在appsettings.json里加上 JWT 相关配置:
{ "JwtSettings": { "Issuer": "HikVision.WebApi", "Audience": "HikVision.Client", "SecretKey": "your-256-bit-secret-key-please-change", "ExpireMinutes": 120 } }然后在Program.cs里注册认证服务:
var jwtSettings = builder.Configuration.GetSection("JwtSettings"); var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings["SecretKey"])); builder.Services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = jwtSettings["Issuer"], ValidAudience = jwtSettings["Audience"], IssuerSigningKey = key }; });这里有个小细节:SecretKey必须超过 256 位(32 字节),否则 HS256 算法跑不起来。我一开始用了个短 key,运行时报错,折腾了好一会儿才意识到是这个原因。
3.2 统一响应格式
前端对接最烦的就是"十个接口十个返回格式"。所以我这边统一用一个ApiResponse<T>包装:
public class ApiResponse<T> { public int Code { get; set; } public string Message { get; set; } public T Data { get; set; } public static ApiResponse<T> Success(T data, string message = "ok") => new ApiResponse<T> { Code = 0, Message = message, Data = data }; public static ApiResponse<T> Fail(string message, int code = -1) => new ApiResponse<T> { Code = code, Message = message, Data = default }; }所有 Controller 的动作方法都返回这个类型,前端拿到code == 0就说明业务成功,否则看message做提示。这个习惯后来在对接 HLS 流地址的时候帮了大忙,因为海康接口偶尔会返回"设备不在线""能力集不支持"这类业务错误,统一包装后,错误信息能直接透传给前端展示,不用再二次翻译。
3.3 全局异常处理中间件
光有统一返回还不够,代码里总有预料之外的异常。全局异常中间件的思路是:捕获所有未处理异常,记录日志,然后返回一个标准化的 500 响应。实现如下:
public class ExceptionHandlingMiddleware { private readonly RequestDelegate _next; private readonly ILogger<ExceptionHandlingMiddleware> _logger; public ExceptionHandlingMiddleware(RequestDelegate next, ILogger<ExceptionHandlingMiddleware> logger) { _next = next; _logger = logger; } public async Task InvokeAsync(HttpContext context) { try { await _next(context); } catch (Exception ex) { _logger.LogError(ex, "Unhandled exception"); context.Response.StatusCode = 500; context.Response.ContentType = "application/json"; var response = ApiResponse<object>.Fail("服务器内部错误"); await context.Response.WriteAsJsonAsync(response); } } }注册到管道里:
app.UseMiddleware<ExceptionHandlingMiddleware>();注意:生产环境不要直接把异常堆栈抛给客户端,既暴露内部结构,也不安全。真正排查问题靠服务端日志就够了。
4. 海康综合安防管理平台 HLS 流对接
这部分是项目的重头戏,也是网上问的人最多的地方。海康的 iSecure Center(综合安防管理平台)提供了完整的 OpenAPI 网关,我们可以用它来获取监控点的 HLS 播放地址。
4.1 海康 OpenAPI 对接前置条件
在做任何代码之前,你需要先向海康的实施工程师要到三样东西:
- 平台地址(形如
http://192.168.1.100:8443,注意有端口) - AppKey
- AppSecret
这三样是调用 OpenAPI 的凭证。AppKey 和 AppSecret 通常在平台的"系统管理 -> 合作方管理"里创建,你可以理解为海康给你发的专属钥匙,后续每次请求都要用它来签名。
4.2 签名算法实现
海康 OpenAPI 的鉴权方式是 HMAC-SHA256 签名。需要拼接以下内容:
X-Ca-Key:AppKeyX-Ca-Timestamp:毫秒级时间戳- 请求方法(GET/POST)+ 请求路径 + 请求体
签名串格式如下:
stringToSign = method + "\n" + accept + "\n" + contentType + "\n" + path + "\n" + body其中accept一般是application/json,contentType也是application/json。用 AppSecret 作为密钥做 HMAC-SHA256 计算,结果 Base64 编码后放到X-Ca-Signature头里。
我封装了一个签名帮助类:
public static class HikVisionSigner { public static string Sign(string appSecret, string method, string path, string body) { var contentType = "application/json"; var accept = "application/json"; var stringToSign = $"{method}\n{accept}\n{contentType}\n{path}\n{body}"; using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(appSecret)); var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(stringToSign)); return Convert.ToBase64String(hash); } }实操心得:这里最容易踩的坑是
path必须只包含路由部分,不能带域名和 query string。比如完整地址是http://192.168.1.100:8443/artemis/api/video/v2/cameras/previewURLs,签名时path只写/artemis/api/video/v2/cameras/previewURLs。我刚开始把整个 URL 放进去,签名一直校验失败,排查了大半天。
4.3 获取 HLS 流地址的完整流程
海康 OpenAPI 获取 HLS 流的接口(以新版 artemis 网关为例)路径是/artemis/api/video/v2/cameras/previewURLs。
请求参数:
{ "cameraCode": "摄像头唯一编码", "streamType": 1, "protocol": "hls", "transMode": 1 }参数含义:
cameraCode:监控点编码,在平台资源树里能看到;streamType:0 主码流,1 子码流。需要高清看主码流,多路同时预览时建议用子码流;protocol:固定hls;transMode:传输模式,1 表示 TCP。
完整的调用代码:
public async Task<string> GetHlsPreviewUrl(string cameraCode, int streamType = 1) { var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString(); var path = "/artemis/api/video/v2/cameras/previewURLs"; var bodyObj = new { cameraCode, streamType, protocol = "hls", transMode = 1 }; var body = JsonConvert.SerializeObject(bodyObj); var sign = HikVisionSigner.Sign(_appSecret, "POST", path, body); var request = new HttpRequestMessage(HttpMethod.Post, _baseUrl + path) { Content = new StringContent(body, Encoding.UTF8, "application/json") }; request.Headers.Add("X-Ca-Key", _appKey); request.Headers.Add("X-Ca-Timestamp", timestamp); request.Headers.Add("X-Ca-Signature", sign); var response = await _httpClient.SendAsync(request); var content = await response.Content.ReadAsStringAsync(); var result = JsonConvert.DeserializeObject<HikVisionResponse>(content); if (result.Code != "0") { throw new Exception($"海康接口返回错误: {result.Msg}"); } return result.Data.Url; }返回结果里data.url就是 HLS 的播放地址,形如http://192.168.1.100:8443/artemis/live?token=xxx...,前端拿这个地址直接丢给video.js或者hls.js就能播。
4.4 HLS 地址过期与缓存策略
海康这个预览 URL 有时效性(一般是几分钟到十几分钟不等),这意味着你不能每次前端要地址都实时调海康,也不能缓存太久。我这边采用的策略是:
- 用 ConcurrentDictionary 做内存缓存,key 是
cameraCode + "_" + streamType,value 是完整的 URL 和过期时间; - 过期时间设置为 10 分钟,因为我们实测海康返回的 URL 大约 30 分钟有效,留足余量;
- 前端播放失败时,调一个刷新接口强制清除缓存再拿新地址。
缓存代码:
private static readonly ConcurrentDictionary<string, (string Url, DateTime ExpireAt)> _urlCache = new(); public async Task<string> GetHlsPreviewUrlWithCache(string cameraCode, int streamType = 1) { var key = $"{cameraCode}_{streamType}"; if (_urlCache.TryGetValue(key, out var cached) && cached.ExpireAt > DateTime.Now) { return cached.Url; } var url = await GetHlsPreviewUrl(cameraCode, streamType); _urlCache[key] = (url, DateTime.Now.AddMinutes(10)); return url; }4.5 大华等其他平台对接的兼容思考
文章标题虽然是海康,但很多项目里是大华、宇视混着用的。好在这类平台的 HLS 取流逻辑高度相似,都是"AppKey/AppSecret 签名 -> 获取 token -> 调用 previewURL 接口拿地址",只是签名规则和接口路径不同。
所以代码里我把IHikVisionService抽成接口,后续要是接大华,新写一个DaHuaService注入进去就行,Controller 层完全不用动。
5. 摄像头管理与业务接口实现
拿到 HLS 流地址只是第一步,一个管理平台上总不能把摄像头编号写死在前端吧。所以还需要一组摄像头信息的 CRUD 接口。为了简化,我用内存数据库,你可以根据自己的项目换 EF Core 或 Dapper 接 MySQL。
5.1 摄像头信息模型
public class CameraInfo { public string CameraCode { get; set; } public string CameraName { get; set; } public string GroupName { get; set; } public int Channel { get; set; } public bool Enabled { get; set; } public DateTime CreatedAt { get; set; } }这个模型刻意做得比较轻,实际项目里可以根据资源树、区域、权限等因素扩展字段。我一般会加一个DevicePlatform字段,用来区分是海康还是大华,这样在StreamController里就能根据这个字段做路由选择。
5.2 摄像头信息管理接口
CameraController里提供常规的增删改查,这里只展示查询接口和启用/停用接口:
[HttpGet("list")] public async Task<ApiResponse<List<CameraInfo>>> GetCameras([FromQuery] string groupName = "") { var query = _cameras.AsQueryable(); if (!string.IsNullOrEmpty(groupName)) { query = query.Where(x => x.GroupName.Contains(groupName)); } return ApiResponse<List<CameraInfo>>.Success(query.ToList()); } [HttpPost("{cameraCode}/status")] public async Task<ApiResponse<bool>> SetCameraStatus(string cameraCode, [FromBody] bool enabled) { var camera = _cameras.FirstOrDefault(x => x.CameraCode == cameraCode); if (camera == null) return ApiResponse<bool>.Fail("摄像头不存在"); camera.Enabled = enabled; return ApiResponse<bool>.Success(true, enabled ? "已启用" : "已停用"); }5.3 获取播放流的组合接口
业务系统前端通常关心的是"给我这个摄像头的播放地址",它不关心你是海康还是大华,也不关心你是 HLS 还是 RTMP。所以我在StreamController里做了一个聚合接口:
[HttpGet("preview/{cameraCode}")] public async Task<ApiResponse<object>> GetPreview(string cameraCode, int streamType = 1) { var camera = _cameraService.GetByCode(cameraCode); if (camera == null) return ApiResponse<object>.Fail("摄像头不存在"); if (!camera.Enabled) return ApiResponse<object>.Fail("摄像头已停用"); var url = await _hikVisionService.GetHlsPreviewUrlWithCache(cameraCode, streamType); return ApiResponse<object>.Success(new { cameraCode = camera.CameraCode, cameraName = camera.CameraName, streamType, hlsUrl = url, expireAt = DateTime.Now.AddMinutes(10) }); }这里返回给前端的信息里我刻意包含了expireAt。前端可以根据这个时间提前 1 分钟去请求新的播放地址,避免播放画面突然中断。
6. 常见问题与现场排查实录
对接过程中遇到的问题,十个里有七个出在海康的鉴权或者网络环境上。我把几个典型的坑整理出来,做成速查表,方便大家对照排查。
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 签名校验失败 | 时间戳与服务器时间偏差过大 | 比对海康服务器时间与当前时间 | 同步服务器时间,或用海康返回的时间生成签名 |
| 返回 code=20001(参数错误) | body 内容与签名不一致 | 确认签名使用的 body 和实际发送的 body 完全相同 | 统一用序列化后的字符串签名和发送 |
| 请求超时 | 网络策略拦截 | telnet 平台 IP 端口是否通 | 在防火墙中放行对应端口 |
| 拿到 URL 打不开 | URL 有效期过期 | 检查生成时间和当前时间差 | 实现缓存刷新机制 |
| 偶发 401 | 网关并发限流 | 查看平台日志是否有限流记录 | 客户端做好流量控制,增加重试机制 |
6.1 签名失败的典型场景
我遇到最隐蔽的一个坑是:用 Postman 调海康接口时,Body 里写了格式化后的 JSON(有换行有空格),签名也用的这个格式化字符串,能通。但是到了 C# 代码里,用 Newtonsoft 序列化出来的 JSON 和 Postman 里那个不完全一样(比如空格处理),导致签名串不一致。
解决办法:把要签名的 body 字符串固定住,签名和发送请求严格使用同一个字符串变量,不要"这边序列化一次,那边又重新组装一次"。
6.2 时间戳偏差问题
海康的网关要求请求时间与服务器时间偏差在 5 分钟以内,否则拒绝。有次客户现场的海康服务器时间慢了两分钟,我这边代码用DateTimeOffset.UtcNow生成的毫秒时间戳是标准的 UTC 时间,两边一对比就差了 8 小时(时区问题)——不,准确说是差了时区偏移加上服务器慢的时间,直接签名失败。
最稳妥的做法:先从海康的一个公开接口(比如/artemis/api/basic/v1/auth/systems)拿到它的服务器时间,然后计算本地与它的偏移量,后续所有请求都加上这个偏移量。
6.3 HLS 播放地址无法播放
这类问题通常不是接口本身的问题,而是播放链路的问题。我总结了一个快速定位链:
- 先用 VLC 播放器直接打开拿到的 HLS 地址,确认能播;
- VLC 能播但浏览器不行,基本就是跨域或者 HTTPS 混合内容问题。海康返回的 HLS 地址是
http://,如果你的 Web 系统是 HTTPS,浏览器会直接拦截,这时候要么让海康网关也走 HTTPS,要么在页面里做个代理转发; - 多路同时播放卡顿,大概率是子码流没用上,检查
streamType参数。
我在实际项目中,前端用的是hls.js,直接支持 HLS 协议。在 HTTPS 页面里放了 HTTP 的流地址,被浏览器拦截过一次,后来用 Nginx 做了个/live/反向代理,把流地址的协议和域名统一到 HTTPS 下,问题才解决。
7. 部署与性能优化经验
7.1 Linux 部署注意点
.NET 6 API 部署到 Linux(CentOS 7 或 Ubuntu 20.04 都行)很简单,发布命令:
dotnet publish -c Release -r linux-x64 --self-contained true--self-contained true的意思是发布产物里携带 .NET 运行时,服务器不需要另外安装 .NET 环境。缺点是包体积大一些,但对于客户现场的服务器,能少装一个依赖就少一点麻烦。
部署后我用systemd注册成服务,开机自启:
[Unit] Description=HikVision WebApi After=network.target [Service] WorkingDirectory=/opt/hikapi ExecStart=/opt/hikapi/HikVision.WebApi Restart=always RestartSec=10 [Install] WantedBy=multi-user.target7.2 并发与性能参数调优
海康综合安防平台有个特点:预览 URL 接口的并发能力有限(有网关流控),所以我们的 API 不能无脑把请求透传过去。除了前面说的缓存机制外,我还在HttpClient里做了配置:
services.AddHttpClient<IHikVisionService, HikVisionService>(client => { client.Timeout = TimeSpan.FromSeconds(10); }) .AddPolicyHandler(GetRetryPolicy()); static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy() { return HttpPolicyExtensions .HandleTransientHttpError() .OrResult(r => !r.IsSuccessStatusCode) .WaitAndRetryAsync(2, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))); }这里用了一个小技巧:指数退避重试,最多重试 2 次。第一次失败等 2 秒,第二次失败等 4 秒。注意这个重试只适用于非幂等性不敏感的 GET 类请求,POST 类型要慎重。
7.3 日志与监控
日志框架用的Serilog,配了文件输出和控制台输出。每次调用海康接口时,我会把请求参数、耗时、返回码记录在案:
_logger.LogInformation("请求海康HLS地址: cameraCode={CameraCode}, streamType={StreamType}, 耗时={Elapsed}ms", cameraCode, streamType, sw.ElapsedMilliseconds);这个日志在对接和排查问题时非常有用。有次客户反馈"明天早上 8 点画面总是不出来",最后看日志发现每天早上 7:50 左右有一条超时记录,再逆推发现是海康平台在 7:30 执行了定时任务占用了大量资源,导致预览接口响应变慢。如果没有日志,这种问题很难定位。
8. 写在最后:项目实施中的几点体会
做这个项目的最大感受是:技术本身的难度其实不大,真正的难点在于对接方的"黑盒"程度。海康的 OpenAPI 文档算是业界比较完善的了,签名原理清晰,接口定义明确,但真到现场还是会遇到文档没写到的情况,比如某些老版本平台的 HLS 地址不支持直接外网访问、某些型号的摄像头必须要先调一次"启动预览"才能拿到流地址等。
另外,在代码结构上,我把所有和海康相关的签名、加密、请求细节都封装在 Service 层里,Controller 层只认业务对象。当时看着只是多花了半天时间做抽象,后来客户说要接大华平台,改造工作量从预计的一周直接降到了两天。
再分享一个设计上的小建议:对接类接口的参数,从数据库或者配置中心读取,而不是硬编码在代码里。AppKey、AppSecret、平台地址这些东西,一旦换了服务器或者项目迁移,直接改配置文件就能生效。我习惯把它们放在appsettings.json的自定义节点里,配合环境变量做覆盖,这样开发和生产的配置互不影响。
这个项目后续如果要扩展,方向很明确:一是把摄像头在线状态监测加上(海康有对应的状态查询接口);二是做一个流地址统一网关,把 HLS、RTMP、WebRTC 几种协议统一转换成适合浏览器播放的格式;三是加上录像回放 URL 的获取接口。核心架构不变,往这个框架里添新接口就行了。
本文还有配套的精品资源,点击获取