WPF 中调用 AOT 发布的 ASP.NET Core 类库(DLL)

296 阅读5分钟

前言

随着 .NET 8 的发布,ASP.NET Core 已原生支持 AOT(Ahead-of-Time)编译。虽然大多数教程仍聚焦于将 ASP.NET Core 发布为独立可执行文件(.exe),但其实它也支持以类库形式发布为 .dll 文件,并通过 UnmanagedCallersOnly 导出函数,供其他应用程序调用。

本文将带你探索一种新颖的架构方式:在一个 WPF 进程中,集成 AOT 编译的 ASP.NET Core 类库,实现本地 HTTP 服务。

这种设计让:

  • WPF 负责 UI 层逻辑与用户交互

  • ASP.NET Core 提供轻量级 HTTP 接口服务

两者各司其职,充分发挥各自优势。

为什么选择 AOT 类库方式?

相比传统的项目引用或 NuGet 包集成,AOT 发布为 DLL 有以下优势:

优势说明
无依赖污染输出仅一个 DLL,不带 ASP.NET Core 的大量依赖 DLL
模块解耦HTTP 服务作为边缘功能,可独立编译、部署
版本隔离WPF 和 ASP.NET Core 可使用不同 .NET 版本运行(通过原生互操作)
轻量化部署AOT 编译后体积小,启动快,适合嵌入式场景

第一步:创建支持 AOT 的 ASP.NET Core 类库

1、创建项目

dotnet new webapiaot -o Lib1
cd Lib1

该模板默认启用 AOT 支持,生成的是顶层语句风格的 Program.cs

2、改造 Program.cs

删除 Main 方法(类库不能有入口点),将逻辑封装到静态方法中,并通过 UnmanagedCallersOnly 导出函数。

完整代码如下:

using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace Lib1;

public static class Program
{
    private static string _greetText = "Hello from Lib1!";

    [UnmanagedCallersOnly(EntryPoint = "Start", CallConvs = [typeof(CallConvCdecl)])]
    public static int Start()
    {
        Console.WriteLine("Start run");

        // ⚠️ 注意:此处使用 Task.Run 会占用线程池资源,仅用于演示
        Task.Run(StartInner);

        return 2; // 返回值示例
    }

    [UnmanagedCallersOnly(EntryPoint = "SetGreetText", CallConvs = [typeof(CallConvCdecl)])]
    public static void SetGreetText(IntPtr greetText, int charCount)
    {
        _greetText = Marshal.PtrToStringUni(greetText, charCount);
    }

    private static void StartInner()
    {
        var builder = WebApplication.CreateSlimBuilder([]);

        var app = builder.Build();
        app.MapGet("/", () => _greetText);

        app.Run(); // 启动 Web 服务
    }
}

📌 关键说明

  • UnmanagedCallersOnly 允许非托管代码调用 .NET 方法。
  • 参数必须是基础类型或指针(如 IntPtr),不支持 .NET 对象跨边界传递
  • 使用 Marshal.PtrToStringUni 将指针转换为字符串。

第二步:解决 AOT 类库已知问题

由于 .NET 运行时的一个 已知 Bug,当 AOT 类库被 .NET 应用加载时,控制台输出可能会卡住。

解决方案:禁用 EventSource 支持

Lib1.csproj 中添加:

<PropertyGroup>
  <EventSourceSupport>false</EventSourceSupport>
</PropertyGroup>

第三步:发布 AOT 类库

执行发布命令:

dotnet publish -r win-x64 --self-contained

发布成功后,可在 bin/Debug/net8.0/win-x64/publish/ 找到 Lib1.dll

💡 文件大小:约 7MB(Slim + AOT 编译优化后)

✅ 唯一必需文件:Lib1.dll
❌ 其他 DLL(如 System.*)可删除

第四步:在 WPF 项目中调用 AOT 类库

1、创建 WPF 项目

dotnet new wpf -o WPFDemo

2、添加 Lib1.dll 到输出目录

Lib1.dll 拷贝到 WPFDemo/bin/Debug/net8.0/ 目录下。

⚠️ 注意:不能直接添加项目引用(因为是 AOT 原生 DLL),只能通过 DllImport 调用。

3、编辑 MainWindow.xaml

添加简单的 UI 界面用于设置欢迎词并触发更新:

<Grid>
    <StackPanel VerticalAlignment="Center">
        <Grid MinWidth="300" HorizontalAlignment="Center">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition/>
            </Grid.ColumnDefinitions>
            <Grid.Resources>
                <Style TargetType="TextBlock">
                    <Setter Property="Margin" Value="0 5 5 5"/>
                </Style>
                <Style TargetType="TextBox">
                    <Setter Property="Margin" Value="0 5 0 5"/>
                </Style>
            </Grid.Resources>

            <TextBlock Text="欢迎词:"/>
            <TextBox x:Name="GreetTextBox" Grid.Column="1" Text="Hello"/>

            <Button x:Name="UpdateButton" Grid.Row="1" Grid.ColumnSpan="2"
                    Width="100" Height="30" Margin="10"
                    Click="UpdateButton_OnClick">更新</Button>
        </Grid>
    </StackPanel>
</Grid>

4、添加 P/Invoke 调用定义

MainWindow.xaml.cs 中声明外部函数:

[DllImport("Lib1.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern int Start();

[DllImport("Lib1.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern void SetGreetText(IntPtr greetText, int charCount);

✅ 调用方式与 Win32 API 完全一致。

5、启动时调用 Start()

在窗口加载时启动 ASP.NET Core 服务:

public MainWindow()
{
    InitializeComponent();
    Loaded += MainWindow_Loaded;
}

private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
    Start(); // 启动 HTTP 服务
}

6、实现按钮点击更新文本

private unsafe void UpdateButton_OnClick(object sender, RoutedEventArgs e)
{
    var greetText = GreetTextBox.Text;
    fixed (char* c = greetText)
    {
        SetGreetText(new IntPtr(c), greetText.Length);
    }
}

🔐 使用 fixed 固定字符串内存,获取指针后传递给原生函数。

第五步:测试验证

1、运行 WPF 应用

2、使用 .http 文件或浏览器访问 http://localhost:5000/

初始返回:

Hello from Lib1!

3、修改文本框内容,点击【更新】按钮

4、再次请求接口,返回内容已更新!

成功实现 WPF 与 AOT 版 ASP.NET Core 的双向通信!

总结

本文实现:

  • 使用 AOT 将 ASP.NET Core 编译为独立 DLL

  • 通过 UnmanagedCallersOnly 导出函数

  • 在 WPF 中通过 DllImport 调用并传参

  • 实现 UI 与 HTTP 服务分离协作

适用场景

  • 桌面应用需提供本地 API 接口(如调试、插件、配置)
  • 嵌入式设备中的小型 Web 控制台
  • 需要最小化部署体积的服务模块

获取完整源码

由于仓库较大,建议使用部分拉取方式:

# 创建空文件夹并进入
mkdir wpf-aspnetcore-aot
cd wpf-aspnetcore-aot

# 初始化并拉取指定提交
git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin 3acde0686b53ca3122cb0fe28838624e1101500c

若无法访问 Gitee,请切换至 GitHub:

git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin 3acde0686b53ca3122cb0fe28838624e1101500c

进入目录:WPFDemo/BarnemwheanejayHelbellacall 即可查看完整代码。

后续优化建议

1、避免 Task.Run:应使用专用线程或异步调度器运行 app.Run(),避免占用线程池。

2、增加 Stop 接口:导出 Stop 函数,优雅关闭 Web 服务。

3、错误处理:增加返回码、异常日志等机制。

4、跨平台支持:发布 linux-x64osx-x64 版本供其他平台调用。

最后

如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。

也可以加入微信公众号 [DotNet技术匠] 社区,与其他热爱技术的同行一起交流心得,共同成长!

优秀是一种习惯,欢迎大家留言学习!

作者:lindexi

出处:.cnblogs.com/lindexi/p/19049877

声明:网络内容,仅供学习,尊重版权,侵权速删,歉意致谢!