惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

M
MIT News - Artificial intelligence
有赞技术团队
有赞技术团队
S
Schneier on Security
aimingoo的专栏
aimingoo的专栏
T
Troy Hunt's Blog
U
Unit 42
Hacker News - Newest:
Hacker News - Newest: "LLM"
V2EX - 技术
V2EX - 技术
T
The Blog of Author Tim Ferriss
V
Visual Studio Blog
H
Heimdal Security Blog
H
Hacker News: Front Page
Blog — PlanetScale
Blog — PlanetScale
博客园 - 司徒正美
Cloudbric
Cloudbric
Google DeepMind News
Google DeepMind News
C
Cisco Blogs
The Cloudflare Blog
C
Cybersecurity and Infrastructure Security Agency CISA
Microsoft Security Blog
Microsoft Security Blog
MyScale Blog
MyScale Blog
F
Fortinet All Blogs
N
News | PayPal Newsroom
Attack and Defense Labs
Attack and Defense Labs
D
DataBreaches.Net
N
News and Events Feed by Topic
Security Archives - TechRepublic
Security Archives - TechRepublic
Forbes - Security
Forbes - Security
Simon Willison's Weblog
Simon Willison's Weblog
F
Full Disclosure
The Register - Security
The Register - Security
L
LINUX DO - 热门话题
Webroot Blog
Webroot Blog
Google Online Security Blog
Google Online Security Blog
AI
AI
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
I
Intezer
S
Security Affairs
阮一峰的网络日志
阮一峰的网络日志
K
Kaspersky official blog
云风的 BLOG
云风的 BLOG
博客园 - 叶小钗
T
Threatpost
Spread Privacy
Spread Privacy
小众软件
小众软件
AWS News Blog
AWS News Blog
S
Secure Thoughts
S
Security @ Cisco Blogs
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
J
Java Code Geeks

博客园 - 东邪独孤

【EF Core】级联删除行为 【EF Core】继承策略——TPC 【EF Core】继承策略——TPT 【EF Core】继承策略——TPH 【EF Core】使用自定义的值比较器 【EF Core】值转换器 【EF Core】直接更新数据 【EF Core】实体追踪——Entry中记录的数据 【EF Core】实体状态与变更追踪 【EF Core】将一个实体映射到多个表的正确方法 【EF Core】“Code First”方案下以编程方式生成迁移 【EF Core】“DB First”方案下用编程方式生成数据库模型代码 【EF Core】三种方法记录生成的 SQL 语句 【EF Core】未定义实体类的数据库模型 【EF Core】“多对多”关系与跳跃导航 【EF Core】FromExpression 方法有什么用? 【EF Core】通过 DbContext 选项扩展框架 【EF Core】框架底层的数据库连接管理 【EF Core】再谈普通实体关系与 Owned 关系的区别 【EF Core】实体类的依赖注入 【EF Core】优化后的模型 【EF Core】使用外部 Model 【EF Core】聊聊“复合”属性 【EF Core】为 DatabaseFacade 扩展“创建”与“删除”数据表功能 【EF Core】带主键实体与无主键实体 【EF Core】框架是如何识别实体类的属性和主键的
【ASP.NET Core】在 Windows Forms 中运行 Web 服务器
东邪独孤 · 2026-07-12 · via 博客园 - 东邪独孤

当大伙伴们看到标题后,是不是在怀疑老周在灌水?非也,老周向来只会造水,从来不灌水。哪怕写的主题和别人一样,但水的内容也肯定与他人不同。这是老周特色,熟悉老周的大伙伴都知道的。

哦,顺便提一下,有人投诉老周写的东西没用,根本不能在项目中使用。项目中用什么,怎么用是你的项目经验和实力问题,老周可不负这个责。老周只传递知识和一些可能对你有用的技巧,如果你想着坐享其成,生吞他人劳动成果的话,请到别处去。

也有人说老周写的代码太“不正经”了,老周问你:什么样的代码才算“正经”?你不用管老周写的什么代码,你看懂了,学到一点点知识就行了,这个才是最重要的。老周瞎编的代码,一方面是防搜索引擎,另一方面是防 AI 侵权。现在想不劳而获的人太多了,不防不行。请记住:任何措施仅限于防小人,而非防君子

好,说回正题,如何把 APS.net core 框在 Windows Forms 或 WPF 中,网上资料很多了,老周你还扯啥。不,老周还真要扯一下的。最近,老周接了一个很“简单”的项目,就是车间大约有 45 台压条机,姑且这么叫吧,也不知道机器的真名叫什么,反正工人是这么叫的。它的作用是把铁皮压直了,一根根像躺尸似的直挺挺的,对,就是把那一条条的不知名金属弄直了。现在老板太想进步了,请人改造了机器,加了一块 ESP32 C3 模组,机器每拉直一条不知名金属,就会通过 Wifi 上报结果(HTTP POST)。他们请另外的团队做了固件,但问题,它能上传了,没人接收。所以,老周的工作就是搞一个 HTTP 服务器公开 Web API 让 Esp32 调用,接收到数据后,把结果保存到数据库。然后车间的大屏幕上要实时展示,就像股票交易所大厅那样。

由于工厂里面的人可能不会搞那么复杂的配置,最好就是搞个 exe,他们双击运行就完事了。于是自然要把 ASP.NET Core 弄到 WinForms 程序里。为了省事,基础代码打算让打工皇帝 AI 去干,结果它给的代码是寒武纪时期的。然后网上搜搜,也确实很多代码都是寒武纪时期的。现代版的只有微软自己文档有提到。

今天老周干脆分享一下,让新手伙伴们不迷路。接下来老周用最简单的方式讲讲理论部分。

第一,先认准哪些 API 是你要的。ASP.NET Core 其实是用 .NET 的 Host 来启动的,所以,先记住 Microsoft.Extensions.Hosting 这个包。

第二,在同名的命名空间下,你会看到这样的让人迷惑的场景。

image

那么多 Builder,我用哪个,一句话斯基:凡看到 HostBuilder 就忽略,那是寒武纪时代的遗物,现代版要认准 HostApplicationBuilder 类(基础接口 IHostApplicationBuilder)。

先别急,接下来老周给看看这些玩意儿是怎么玩的。现在,各位不想 ASP.NET Core 的事,暂时把它忘了。

刚刚我们认识了一个类叫 HostApplicationBuilder,它的构造函数:

// 无参版本
public HostApplicationBuilder();

// 需要命令行参数版本
public HostApplicationBuilder(string[]? args);

// 源头版本
public HostApplicationBuilder(HostApplicationBuilderSettings? settings);

说到底,就是需要一个 HostApplicationBuilderSettings 对象,它的作用是配置应用程序的基本信息。其中,ApplicationName  和 Args 属性用得最多。HostApplicationBuilder(string[] args) 构造函数就是设置了 Args 属性。

public HostApplicationBuilder(string[]? args)
    : this(new HostApplicationBuilderSettings { Args = args })
{
}

于是,动手能力强的你,就会……

// 记得添加 Microsoft.Extensions.Hosting 包的引用

static void Main(string[] args)
{
    HostApplicationBuilder appBuilder = new(args);

    var myHost = appBuilder.Build();
    myHost.Run();
}

这个逻辑对吗?那就得运行下,如果能看到如下输出,说明是对的。

image

这个能干啥?答:啥也不能干。有.NET 基础的大伙伴一定想到,得加几个东西到服务容器里才行。

public class MyService
{
    public void Boom()
    {
        Console.WriteLine("*** 自爆程序已启动 ***");
    }
}

static void Main(string[] args)
{
    HostApplicationBuilder appBuilder = new(args);
    appBuilder.Services.AddTransient<MyService>();

    var myHost = appBuilder.Build();
    myHost.Run();
}

运行之后,咦,没反应。那当然了,服务你是加了,可,谁调用?不管你添加的服务有多少个,它们之间什么依赖关系。但万事总有个头啊,起码有一个要先被调用吧。那么……

 static void Main(string[] args)
 {
     HostApplicationBuilder appBuilder = new(args);
     appBuilder.Services.AddTransient<MyService>();

     var myHost = appBuilder.Build();
     IHostApplicationLifetime lifeTime = myHost.Services.GetRequiredService<IHostApplicationLifetime>();

     lifeTime.ApplicationStarted.Register(() =>
     {
         Console.WriteLine("===== 应用程序启动 ====");
         MyService svc = myHost.Services.GetRequiredService<MyService>();
         svc.Boom();
     });

     lifeTime.ApplicationStopping.Register(() =>
     {
         Console.WriteLine("==== 应用程序即将停止 ====");
     });

     myHost.Run();
 }

是个大聪明,获取 IHostApplicationLifetime 服务,毕竟这个服务是框架自动添加的,当 Host 启动后,会触发 ApplicationStarted,而 ApplicationStopping 是在 Host 即将完蛋时触发,而且,它会一直 block,直到注册的委托执行完成。所以,ApplicationStopping 注册的委托中,你的代码不要执行太久,否则会耽误应用程序去投胎的时间。

ApplicationStarted、ApplicationStopping、ApplicationStopped 虽然定义的是属性成员,其实和事件一样。虽然它是 CancellationToken 类型,但这个结构体可以用 Register 方法注册一个委托,当发生取消时调用这个委托。你看看,这不跟事件一个德性吗。所以,咱们上面的代码就是在应用启动后调用 MyService 服务,那不就可以运行了吗。

image

现在应用程序阻止了主线程,直到程序结束。怎么结束?日志上不是写了吗,按 Ctrl + C。

image

这样做虽然能调用服务,但一来不好控制,二来万一你那个服务不行,一执行就挂了,或者在那里死循环,就会让后面的程序无法执行了。所以,这个方案还是不佳。

于是,有一个神秘的服务浮出水面—— IHostedService。这个服务接口只有两个方法。

A、StartAsync;

B、StopAsync。

看名字就知道,开始和结束呗。咱们实现一下。

public class MyHostedService : IHostedService
{
    MyService src;

    public MyHostedService(MyService s) => src = s;

    public Task StartAsync(CancellationToken cancellationToken)
    {
        Console.WriteLine("==== 服务已启动 ====");
        src.Boom();
        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        Console.WriteLine("==== 服务停止 ====");
        return Task.CompletedTask;
    }
}

MyService 服务只要通过构造函数参数就能获得,这就是依赖注入。

现在,我们不需要 IHostApplicationLifetime 服务了,直接把 MyHostedService 放进服务容器就行了。

static void Main(string[] args)
{
    HostApplicationBuilder appBuilder = new(args);
    appBuilder.Services.AddTransient<MyService>();
    appBuilder.Services.AddSingleton<IHostedService, MyHostedService>();

    var myHost = appBuilder.Build();
    myHost.Run();
}

hosted 的服务最好生命周期为单实例,毕竟它不需要多次实例化,很多时候它的生命周期贯穿整应用程序。

代码运行结果如下:

image

实现了 IHostedService 接口的服务到底神在哪?神就神在框架会自动调用它。

因为这种服务实现的接口必然是 IHostedService,为了方便大家调用,.NET 还提供了 AddHostedService 方法。这个方法添加以 IHostedService 为基础接口的服务类型,且注册为单实例。和我们上面的写法功能一样,只是方便。

static void Main(string[] args)
{
    HostApplicationBuilder appBuilder = new(args);
    appBuilder.Services.AddTransient<MyService>();
    appBuilder.Services.AddHostedService<MyHostedService>();

    var myHost = appBuilder.Build();
    myHost.Run();
}

现在你应该知道,ASP.NET Core 的内置服务器能自动运行的原因了吧,就是实现了 IHostedService 接口。

如果,咱们继续深挖,会发生什么呢?

在 VS 中打开项目属性窗口,按下面所示修改属性。

image

image

项目文件会变成这样。

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net10.0-windows</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <UseWindowsForms>True</UseWindowsForms>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.9" />
  </ItemGroup>

</Project>

是不是很熟悉?这不就是 WinForms 项目吗。

public class MyMainForm : Form
{
    private Label someText;
    public MyMainForm()
    {
        this.SuspendLayout();       // 控件多的时候,可以减少闪烁
        Text = "主窗口";
        Size = new Size(600, 480);
        someText = new Label()
        {
            Text = "这是一个简单得要死的程序",
            Font = new Font("微软雅黑", 12.0f),
AutoSize = true }; someText.Location
= new Point(35, 40); this.Controls.Add(someText); ResumeLayout(); // 控件初始化完成,统一刷新 } } [STAThread] static async Task Main(string[] args) { HostApplicationBuilder appBuilder = new(args); var myHost = appBuilder.Build(); await myHost.StartAsync(); Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); // 系统 DPI 缩放优先 Application.SetHighDpiMode(HighDpiMode.SystemAware); Application.Run(new MyMainForm()); // 主窗口关闭后终止 Host await myHost.StopAsync(); }

1、Main 方法上记得加上 [STAThread];

2、由于 Application.Run 会在主窗口上进行消息循环处理,它会阻塞当前线程。所以,要先启动 Application Host,再运行 Run 方法。等 Run 方法退出后,再 Stop Host。

3、其他窗口可以放服务容器中,让它们也可以依赖注入。不过,主窗口如果需要处理消息循环,还是不要放容器中。

好了,折腾到这里,相信换成 ASP.NET Core,你也知道怎么弄了。

我们新建一个 ASP.NET Core 项目(什么类型都可以,MVC、Blazor 或 空白 都行)。

然后,打开项目属性窗口。这回知道怎么配置了吧,和前文说的一样。

image

image

是否把 WinForms 和 WPF 都开启那看实际情况了,如果你两个都用上就一起开,如果你只用一个就开一个。

项目文件中的 SDK 名称不要改,保留 Microsoft.NET.Sdk.Web。网上有些资料是把这个改为 .NET.Sdk 了,这样就没有了 ASP.NET Core 的 API 子集了,然后又要手动引用 Nuget 包。那是错误的!开启 Windows 桌面支持与 SDK 子集是无关的,因为这个功能只针对 Windows,不跨平台。它只作为 API 的扩展集处理。所以,.NET.Sdk.Web 对使用桌面 API 是没有影响的。

然后 Main 方法改成以下代码就好了。


  [STAThread]

public static async Task Main(string[] args)
{
    var builder = WebApplication.CreateBuilder(args);
    var app = builder.Build();

    app.MapGet("/", () => "Hello World!");

    //app.Run();
    await app.StartAsync();

    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);
    Application.SetHighDpiMode(HighDpiMode.SystemAware);
    Application.Run(new MainForm());

    await app.StopAsync();
}

由于现在是 WinForms 来控制应用生命周期,不再调用 app.Run 方法。Run 方法和 WinForms 里面的 Application.Run 方法都会导致当前线程被阻塞,因此,必须让其中一个不阻塞。WinForms Application 类是不可能的了,Run 必须在所有窗口都结束后才能返回(或消息队列结束),那只能让 WebApplication 不阻塞了,StartAsync 和 StopAsync 都是可以异步等待的。StartAsync 方法调用后,遇到 Application.Run 方法,由于它一直阻塞,使得 StopAsync 方法不会马上调用,所以 Web 服务器也不会启动后就立马挂掉。等到所有窗口都被关闭了,Run 方法返回,StopAsync 方法就会调用,Web 服务器也停止,然后程序退出。

当你按下【F5】运行后,你会看到,控制台窗口还跑出来。这个不用担心,这是调用试器搞的鬼。你直接运行程序 exe 是不会出现控制台窗口的。如果没有去配置,那么脱离调试运行,默认的端口是 5000。

上面的做法是 Web 服务器随着应用程序启动的,但有些时候,客户会觉得要手动启动。这时候可以将 Web 的初始化搬到窗口代码里。Main 方法只保留 WinForms 或 WPF 的初始化代码。

[STAThread]
public static async Task Main(string[] args)
{
    ……
    Application.Run(new MainForm());
}

我们在主窗口类中搞个 InitWebApp 方法。

private void InitWebApp()
{
    WebApplicationBuilder builder = WebApplication.CreateBuilder();
    // 添加所需服务
    builder.Services.AddRouting();

    // 生成Web应用程序
    webApp = builder.Build();

    // 配置终结点
    webApp.MapGet("/", () => "你好,宇宙!");
}

webApp 是在窗口类中声明的私有字段。

WebApplicationBuilder 的作用只是构建内部服务,Build 了之后就没用了,所以它不用声明到类的成员级别,声明为局部变量就行了,用完就扔,只留着 WebApplication 实例在内存中就好。

然后,窗口上你可以放两个按钮。一个用来启动,一个停止。

image

分别处理两个按钮的 Click 事件。

private async void btnStart_Click(object sender, EventArgs e)
{
    (sender as Button)?.Enabled = false;
    InitWebApp();
    if (webApp != null)
    {
        await webApp.StartAsync();
        lbMsg.Text = "Web 服务器正在服徭役";
        lbMsg.Text += "\n地址:" + webApp?.Urls.First();
    }
    (sender as Button)?.Enabled = true;
}

private async void btnStop_Click(object sender, EventArgs e)
{
    if(webApp != null)
    {
        await webApp.StopAsync();
        await webApp.DisposeAsync();
        webApp = null;
        lbMsg.Text = "Web 服务器已投胎";
    }
}

有大伙伴们会说,老周你真笨,为什么每次启动都要初始化 Web 服务器,直接保留实例,调用 StartAsync、StopAsync 方法不就好了吗。其实是不行的,服务器 Stop 后再调用就有问题,问题就出在 CancellationTokenSource 上。

public async Task StartAsync(CancellationToken cancellationToken = default)
{
    _logger.Starting();

    using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _applicationLifetime.ApplicationStopping))
    {
        if (_options.StartupTimeout != Timeout.InfiniteTimeSpan)
            cts.CancelAfter(_options.StartupTimeout);

        cancellationToken = cts.Token;
      ……

}

哪怕你自己定义了 CancellationTokenSource 对象传进去也不行,因为 CreateLinkedTokenSource 和 ApplicationStopping 生命周期事件也关联了,只要服务被 stop ,相关的 cancel token 全都取消了。

更重要的是,前面咱们说过,所有 IHostedService 服务都会自动启动,而在 stop 时,也会将所有 IHostedService 全部 stop 掉。你不能保证,这些服务可能都清理内存了。所以说,维护这么难搞的状态还不如重新实例化更爽。

这么一来,ASP.NET Core 就可以作为一个普通的 exe 程序发布,客户在他们的电脑上双击运行就完事了。老周特意用了 SQLite 数据库,基本上做到双击运行,啥都不用他们去配置,也不用部署数据库服务器什么的。

好了,就分享到这里了。这个方案只在特殊需求下用,毕竟这么一搞,项目就不能跨平台了。