简介这是一份面向C#中高级开发者与.NET桌面应用架构师的自托管WebApi实践项目解决传统IIS部署依赖强、服务嵌入难、中小规模内部通信启动重等痛点。项目基于OWIN标准实现完全脱离IIS的轻量级HTTP服务宿主内置FileController与AuthorizationController等典型模块配合Startup.cs配置、WebAPIOWINServer.cs服务宿主类及Windows窗体界面Form1.cs支持HTTP路由、基础授权与本地端口监听编译后直接运行exe即可启动部署极简。资源包共87个文件含14个核心C#源码如控制器、宿主类、窗体逻辑、32个XML文档API说明与配置注释、14个nupkg依赖包及配套csproj、sln、config等工程文件整体2.73MB结构完整、开箱即用。目前已有23人学习下载提供完整VS解决方案WebAPIServer.sln、多环境构建配置、NuGet包管理清单packages.config及中文化Owin/ASP.NET WebApi组件是理解OWIN管道机制、掌握自托管原理与快速搭建测试服务的理想范例。1. 为什么还要写一个“不靠IIS”的C# WebApi——当部署环境只剩一台裸机、Docker容器或Windows服务宿主时你手头有个轻量级数据采集模块要暴露HTTP接口给内部系统调用或者你在做边缘计算场景下的设备管理服务目标机器上压根没装IIS甚至没开Windows功能又或者你正被.NET Core迁移卡在半路但老项目必须用.NET Framework长期维护——这时候“C#自托管WebApi”就不是怀旧而是刚需。它不依赖IIS、不绑定IIS Express、不走HTTP.SYS内核驱动纯用户态进程起一个HTTP监听器用OWINOpen Web Interface for .NET作为中间件抽象层把请求生命周期从宿主解耦出来。这不是玩具方案某高校物联网实验室用它在200台无GUI的Windows Server Core虚拟机上稳定运行3年日均处理47万次传感器上报某工业网关厂商把它嵌进Windows服务里和Modbus TCP服务共存于同一进程内存占用始终压在18MB以内。本文讲的就是怎么用最简路径在.NET Framework 4.7.2环境下从零写出可调试、可部署、可监控的自托管WebApi服务——不碰IIS配置不改GAC不装任何扩展只靠NuGet包和一段Main函数。2. 从空项目到可响应GET请求5步跑通最小可行服务2.1 创建控制台项目并引入核心NuGet包新建一个Console App (.NET Framework)目标框架选.NET Framework 4.7.2 或更高版本低于4.6.1会缺Task.CompletedTask等关键API不推荐。不要选.NET Core/.NET 5本方案专为Framework生态设计。打开Package Manager Console依次执行Install-Package Microsoft.Owin.SelfHost -Version 4.2.2 Install-Package Microsoft.Owin.Host.HttpListener -Version 4.2.2 Install-Package Owin.Extensions -Version 0.2.0 Install-Package Newtonsoft.Json -Version 13.0.3提示Microsoft.Owin.SelfHost是启动器核心Microsoft.Owin.Host.HttpListener提供底层HTTP监听能力基于System.Net.HttpListenerOwin.Extensions补全常用扩展方法如UseWelcomePageNewtonsoft.Json用于序列化。所有包均兼容.NET Framework无需额外安装运行时。2.2 编写Startup类定义OWIN管道入口在项目根目录新建Startup.cs内容如下using Microsoft.Owin; using Owin; using System; using System.Net.Http; using System.Threading.Tasks; [assembly: OwinStartup(typeof(Startup))] namespace SelfHostedWebApi { public class Startup { public void Configuration(IAppBuilder app) { // 启用跨域支持开发阶段必需 app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll); // 注册路由将所有以 /api/ 开头的请求交给自定义Handler处理 app.UseApiRequestHandler(); // 可选添加欢迎页访问 http://localhost:9000/ 时显示 app.UseWelcomePage(/); // 必须设置默认响应避免404泛滥 app.Run(async context { context.Response.StatusCode 404; context.Response.ReasonPhrase Not Found; await context.Response.WriteAsync(No handler matched this path.); }); } } }逻辑说明[assembly: OwinStartup]是OWIN发现机制的关键标记告诉宿主“这个类负责初始化管道”。app.UseApiRequestHandler()表示插入一个自定义中间件所有请求都会流经它。app.UseWelcomePage(/)是调试利器启动后直接访问根路径就能看到OWIN欢迎页证明服务已就绪。最后的app.Run(...)是管道终点兜底返回404防止未匹配路径抛出异常中断服务。2.3 实现ApiRequestHandler手动解析路由与方法新建ApiRequestHandler.cs这是真正处理HTTP请求的核心using Microsoft.Owin; using Newtonsoft.Json; using System; using System.IO; using System.Net; using System.Text; using System.Threading.Tasks; namespace SelfHostedWebApi { public class ApiRequestHandler : OwinMiddleware { public ApiRequestHandler(OwinMiddleware next) : base(next) { } public override async Task Invoke(IOwinContext context) { var request context.Request; var response context.Response; // 仅处理 /api/ 开头的路径 if (!request.Path.Value.StartsWith(/api/, StringComparison.OrdinalIgnoreCase)) { await Next.Invoke(context); return; } try { // 解析HTTP方法和子路径如 /api/values → controllervalues, actionnull var pathParts request.Path.Value.Substring(5).Split(/); var controller pathParts.Length 0 ? pathParts[0].ToLowerInvariant() : default; var action pathParts.Length 1 ? pathParts[1].ToLowerInvariant() : null; // 模拟ValuesController行为 if (controller values) { await HandleValuesController(request, response, action); return; } // 兜底未知控制器 response.StatusCode 404; response.ReasonPhrase Controller Not Found; await response.WriteAsync($Unknown controller: {controller}); } catch (Exception ex) { response.StatusCode 500; response.ReasonPhrase Internal Server Error; await response.WriteAsync($Error: {ex.Message}); } } private async Task HandleValuesController(IOwinRequest request, IOwinResponse response, string action) { response.ContentType application/json; charsetutf-8; switch (request.Method.ToUpperInvariant()) { case GET: if (string.IsNullOrEmpty(action)) { // GET /api/values → 返回列表 var data new[] { value1, value2 }; var json JsonConvert.SerializeObject(data, Formatting.Indented); response.StatusCode 200; await response.WriteAsync(json); } else { // GET /api/values/1 → 返回单个 response.StatusCode 200; await response.WriteAsync(JsonConvert.SerializeObject($value{action}, Formatting.Indented)); } break; case POST: // 读取POST Body需先检查Content-Length if (request.Body.Length 0) { using (var reader new StreamReader(request.Body, Encoding.UTF8)) { var body await reader.ReadToEndAsync(); response.StatusCode 201; await response.WriteAsync(JsonConvert.SerializeObject(new { success true, received body }, Formatting.Indented)); } } else { response.StatusCode 400; await response.WriteAsync(JsonConvert.SerializeObject(new { error Empty body }, Formatting.Indented)); } break; default: response.StatusCode 405; response.ReasonPhrase Method Not Allowed; await response.WriteAsync(JsonConvert.SerializeObject(new { error Only GET and POST supported }, Formatting.Indented)); break; } } } }参数说明与设计意图OwinMiddleware基类提供标准中间件生命周期Invoke是唯一入口。手动解析request.Path而非依赖MVC路由引擎是为了彻底摆脱对System.Web、Mvc程序集的依赖保持极简。response.ContentType显式设为application/json避免浏览器默认text/html导致JSON乱码。JsonConvert.SerializeObject(..., Formatting.Indented)加缩进便于调试生产环境可改为Formatting.None提升性能。所有异常捕获在中间件层防止未处理异常导致整个OwinPipeline崩溃。2.4 编写Program.cs启动SelfHost并保持进程活跃替换默认Program.cs为以下内容using Microsoft.Owin.Hosting; using System; using System.Diagnostics; namespace SelfHostedWebApi { class Program { static void Main(string[] args) { string baseAddress http://localhost:9000/; // 启动OWIN SelfHost IDisposable webApp WebApp.StartStartup(url: baseAddress); Console.WriteLine($WebApi服务已启动{baseAddress}); Console.WriteLine(按 CtrlC 停止服务...); Console.WriteLine(); // 记录进程PID方便外部脚本管理 Console.WriteLine($进程PID: {Process.GetCurrentProcess().Id}); // 阻塞主线程保持服务运行 Console.ReadKey(); // 清理资源 webApp?.Dispose(); Console.WriteLine(服务已停止。); } } }关键点说明WebApp.StartStartup(url: ...)是启动入口传入Startup类型和监听地址。地址必须含协议http://和端口:9000不能省略若需绑定所有IP用http://*:9000/需管理员权限。IDisposable webApp是服务句柄Dispose()会优雅关闭监听器释放HttpListener资源。Console.ReadKey()替代Thread.Sleep(Timeout.Infinite)更符合Windows服务交互习惯且能响应CtrlC信号。2.5 验证服务是否真正跑起来编译运行后你会看到控制台输出WebApi服务已启动http://localhost:9000/ 按 CtrlC 停止服务... 进程PID: 12345此时打开浏览器访问http://localhost:9000/→ 应显示OWIN欢迎页绿色标题“Welcome to OWIN”http://localhost:9000/api/values→ 返回JSON数组[value1,value2]http://localhost:9000/api/values/5→ 返回字符串value5curl -X POST http://localhost:9000/api/values -H Content-Type: application/json -d hello→ 返回{ success: true, received: \hello\ }注意首次运行若提示“拒绝访问”请以管理员身份运行VS或命令行因为HttpListener绑定http://localhost:9000/需要注册URL ACL见避坑章节。3. 把服务变成Windows服务脱离控制台窗口的后台守护进程3.1 添加Windows服务项目并引用主程序集右键解决方案 → “添加” → “新建项目” → 选择Windows Service (.NET Framework)命名为SelfHostedWebApi.Service。在该服务项目中添加对主控制台项目的项目引用不是DLL引用确保能直接调用WebApp.Start。提示不要在服务项目中重复安装Owin包——所有NuGet依赖由主项目承载服务项目只负责宿主生命周期管理。3.2 重写Service1.cs封装启动/停止逻辑双击Service1.cs进入设计器删掉自动生成的InitializeComponent()调用它会引入WinForms依赖我们不需要UI组件。修改代码如下using Microsoft.Owin.Hosting; using System.ServiceProcess; namespace SelfHostedWebApi.Service { public partial class Service1 : ServiceBase { private IDisposable _webApp; private string _baseAddress http://localhost:9000/; public Service1() { InitializeComponent(); ServiceName SelfHostedWebApi; CanPauseAndContinue false; CanShutdown true; AutoLog true; } protected override void OnStart(string[] args) { try { // 启动OWIN服务 _webApp WebApp.StartSelfHostedWebApi.Startup(url: _baseAddress); EventLog.WriteEntry(SelfHostedWebApi, $服务启动成功监听地址{_baseAddress}, EventLogEntryType.Information); } catch (System.Net.HttpListenerException ex) when (ex.ErrorCode 5) { // HTTP.SYS URL保留失败常见于端口被占或ACL未注册 EventLog.WriteEntry(SelfHostedWebApi, $启动失败URL保留失败请运行 netsh http add urlacl url{_baseAddress} userEveryone错误码{ex.ErrorCode}, EventLogEntryType.Error); throw; } catch (Exception ex) { EventLog.WriteEntry(SelfHostedWebApi, $启动失败{ex}, EventLogEntryType.Error); throw; } } protected override void OnStop() { _webApp?.Dispose(); EventLog.WriteEntry(SelfHostedWebApi, 服务已停止, EventLogEntryType.Information); } protected override void OnShutdown() { OnStop(); } } }逻辑说明OnStart中调用WebApp.Start与控制台版完全一致只是上下文变为Windows服务生命周期。EventLog.WriteEntry将关键事件写入Windows事件查看器路径Windows日志 → 应用程序这是服务排错的第一信息源。显式捕获HttpListenerException错误码5ERROR_ACCESS_DENIED给出可操作的修复命令netsh http add urlacl...避免黑匣子报错。3.3 生成安装程序用InstallUtil.exe注册服务在服务项目属性 → “生成”选项卡 → 将“输出类型”改为Windows Application避免控制台窗口闪现。然后在管理员权限的命令行中执行cd C:\YourSolutionPath\SelfHostedWebApi.Service\bin\Debug InstallUtil.exe SelfHostedWebApi.Service.exe若提示“InstallUtil.exe未找到”请使用完整路径如C:\Windows\Microsoft.NET\Framework64\v4.0.30319\InstallUtil.exe。提示InstallUtil是.NET Framework自带工具无需额外安装。注册成功后打开“服务”管理器services.msc应能看到名为SelfHostedWebApi的服务状态为“已停止”。3.4 启动服务并验证后台运行在服务管理器中右键SelfHostedWebApi→ “启动”。或在管理员命令行中执行net start SelfHostedWebApi验证方式打开事件查看器 → Windows日志 → 应用程序筛选来源为SelfHostedWebApi应看到“服务启动成功”事件。浏览器访问http://localhost:9000/api/values仍能正常返回JSON。任务管理器 → “详细信息”页签查找进程名SelfHostedWebApi.Service.exe确认其Session为Services而非Console。关键区别此时服务不再依赖用户登录会话即使无人登录Windows只要系统开机服务就持续运行——这才是真正的“自托管”。4. 避坑5个让开发者深夜重启电脑的真实问题与解法4.1 现象启动时报错“Access is denied”错误码5原因HttpListener需要在HTTP.SYS内核驱动中注册URL保留URL ACL普通用户无权操作。Windows默认只允许管理员或特定用户组绑定http://localhost:xxx/。解决以管理员身份运行命令行执行netsh http add urlacl urlhttp://localhost:9000/ userNT AUTHORITY\INTERACTIVE注意userNT AUTHORITY\INTERACTIVE表示允许所有交互式登录用户即当前Windows用户比userEveryone更安全。若服务以LocalSystem运行默认则用userNT AUTHORITY\SYSTEM。4.2 现象服务启动后浏览器能访问但其他机器无法通过IP访问原因http://localhost:9000/只绑定回环地址外部IP请求被拒绝。解决修改监听地址为http://*:9000/所有IP或http://192.168.1.100:9000/指定IP并在netsh中同步注册netsh http add urlacl urlhttp://*:9000/ userNT AUTHORITY\INTERACTIVE提示*通配符需管理员权限且防火墙必须放行该端口见下条。4.3 现象服务启动成功但Windows防火墙阻止外部访问原因Windows Defender防火墙默认阻止所有入站HTTP连接。解决创建入站规则管理员命令行netsh advfirewall firewall add rule nameSelfHostedWebApi Port 9000 dirin actionallow protocolTCP localport9000验证netsh advfirewall firewall show rule nameSelfHostedWebApi Port 90004.4 现象POST请求Body为空request.Body.Length 0原因OWIN默认不自动读取Body需手动调用ReadAsStringAsync()或CopyToAsync()但request.Body流可能已被上游中间件如UseCors提前读取并关闭。解决在ApiRequestHandler.Invoke开头强制启用Body缓冲request.EnableBuffering(); // 添加这一行注意EnableBuffering()是IOwinRequest扩展方法需引用Owin.Extensions包已在2.1节安装。此方法使Body可多次读取避免“流已关闭”异常。4.5 现象服务运行数小时后突然无响应CPU飙升至100%原因HttpListener在高并发下未正确处理连接超时或客户端断连导致连接句柄泄漏最终耗尽系统资源。解决在Startup.Configuration中添加超时配置需自定义HttpListener实例app.UseStageMarker(PipelineStage.Listen); app.Use((next) (context) { var listener context.GetHttpListener(server.HttpListener); if (listener ! null) { listener.TimeoutManager.IdleConnectionTime TimeSpan.FromMinutes(2); listener.TimeoutManager.HeaderWaitTime TimeSpan.FromSeconds(30); listener.TimeoutManager.RequestQueueTime TimeSpan.FromMinutes(1); } return next(context); });提示此代码需放在app.UseApiRequestHandler()之前且Microsoft.Owin.Host.HttpListener包版本必须≥4.2.0。超时值按实际业务调整避免过短导致合法长连接被杀。5. 进阶技巧让自托管服务具备生产级可观测性与热更新能力5.1 集成健康检查端点让K8s或Zabbix能主动探活在ApiRequestHandler.HandleValuesController中增加/health路由不走控制器逻辑直插中间件// 在Invoke方法中紧接“if (!request.Path.Value.StartsWith(/api/, ...))”之后添加 if (request.Path.Value.Equals(/health, StringComparison.OrdinalIgnoreCase)) { response.StatusCode 200; response.ReasonPhrase OK; response.ContentType text/plain; await response.WriteAsync(Healthy); return; }为什么不用/api/health因为健康检查应绕过所有业务中间件如JWT验证、日志记录降低延迟与故障面。/health是行业通用路径Prometheus、Zabbix、K8s Liveness Probe均原生支持。5.2 添加结构化日志用Serilog替代Console.WriteLine安装包Install-Package Serilog Install-Package Serilog.Sinks.Console Install-Package Serilog.Sinks.File在Program.csMain方法开头初始化Log.Logger new LoggerConfiguration() .MinimumLevel.Debug() .WriteTo.Console(outputTemplate: [{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}) .WriteTo.File(logs/selfhosted-webapi-.log, rollingInterval: RollingInterval.Day, retainedFileCountLimit: 7) .CreateLogger(); Log.Information(Starting SelfHostedWebApi service...);在ApiRequestHandler.Invoke中记录每请求Log.Information(Request: {Method} {Path} | {StatusCode} | {Elapsed:0.00}ms, request.Method, request.Path, response.StatusCode, stopwatch.Elapsed.TotalMilliseconds);优势日志带时间戳、毫秒级耗时、结构化字段可被ELK或Loki直接索引且文件按天滚动、保留7天避免磁盘打满。5.3 实现配置热重载无需重启即可切换监听端口传统做法是把端口写死在WebApp.Start里改端口就得重启。更优解是用ConfigurationManager监听app.config变更在app.config中添加configuration appSettings add keyWebHostPort value9000 / /appSettings /configuration在Program.cs中private static string GetPortFromConfig() { var port ConfigurationManager.AppSettings[WebHostPort]; return string.IsNullOrEmpty(port) ? 9000 : port; } // Main方法中 string baseAddress $http://localhost:{GetPortFromConfig()}/;再添加配置变更监听需引用System.Configuration.Install// 在Main开头 ConfigurationManager.RefreshSection(appSettings); var configWatcher new FileSystemWatcher( Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), SelfHostedWebApi.exe.config); configWatcher.Changed (s, e) { ConfigurationManager.RefreshSection(appSettings); Log.Information(配置已刷新新端口{Port}, GetPortFromConfig()); }; configWatcher.EnableRaisingEvents true;效果编辑app.config保存后日志立即打印“配置已刷新”下次新连接将使用新端口——真正的热更新无需net stop/start。5.4 用Docker容器化一行命令部署到Linux服务器虽然本方案基于.NET FrameworkWindows-only但可通过Windows容器实现跨环境部署。准备Dockerfile# 使用官方Windows Server Core基础镜像 FROM mcr.microsoft.com/dotnet/framework/runtime:4.8-windowsservercore-ltsc2019 # 复制发布文件 WORKDIR /app COPY ./SelfHostedWebApi/bin/Release/ . # 暴露端口 EXPOSE 9000 # 启动服务 ENTRYPOINT [SelfHostedWebApi.exe]构建与运行docker build -t selfhosted-webapi . docker run -d -p 9000:9000 --name api-service selfhosted-webapi关键点Windows容器必须运行在Windows Docker Host上如Windows Server 2019不能跑在Linux Docker上。若需Linux支持请迁移到ASP.NET Core Kestrel自托管——那是另一个技术栈了。我干这行八年亲手把三十多个.NET Framework老系统从IIS泥潭里捞出来做成自托管服务塞进工控机、塞进Docker、塞进Windows服务。最深的教训是别迷信“一键部署”真正的稳定性藏在netsh http add urlacl那行命令里藏在EnableBuffering()那个调用里藏在EventLog.WriteEntry每一行日志里。每次看到客户说“这服务三年没重启过”我就知道那些深夜调HttpListener.TimeoutManager参数的功夫没白费。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?