添加一个 HttpServer 到桌面程序中

之前在一个 .NET3.5 的桌面程序中,内嵌的 HttpServer 是通过 HttpListener 来实现的, 但是这个东西对 Https 证书 支持的不是很好,找了很多文档,都说是把证书导入到计算机。但是作为一个要分发的应用程序,不可能这样做。为此, 我还用 学了几天的 GO 肝了一个升级版,但毕竟GO不是咱的专长。

在 .NET8 中,ASP.NET Core 提供了一个 SlimBuilder, 这个东西支持 AOT。但是要把这个 SlimBuilder 嵌入到桌面程序中, 还是有个波折的。

WebApplication.CreateSlimBuilder()

RequestDelegateGenerator

这个东西要运行在 AOT 下面, 需要借助 RequestDelegateGenerator, 没错, 又是一个 IIncrementalGenerator, 你可以在新建的 .NET8 ASP.NETCore 项目的分析器里找到它。
但是这个东西只支持 Web项目, 而且当前没有 NUGET 包。

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

要在非Web 项目里使用 WebApplicaton 需要引用框架,但是, 这个 SourceGenerator 会被自动屏蔽掉。

    <ItemGroup>
        <!--WebApplication.CreateSlimBuilder 是框架的功能, 没有 NUGET包, 只能引用框架-->
        <FrameworkReference Include="Microsoft.AspNetCore.App" />
    </ItemGroup>

为了能使用上这个 SourceGenerator 只能祭上反编译工具, 导出代码,在放到自己的项目中去。

<ProjectReference Include="..\Microsoft.AspNetCore.Http.RequestDelegateGenerator\RequestDelegateGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />

注意, 分析器项目必须指定:
OutputItemType="Analyzer" ReferenceOutputAssembly="false"

ConfigureHttpJsonOptions

即使在这个 HttpServer 中, 输出的都是文本, 也需要加上Json 的 SerializeOption, 不然AOT报错。

var builder = WebApplication.CreateSlimBuilder();

builder.Services.ConfigureHttpJsonOptions(options =>
{
    //特意加的, 不然 AOT 报错
    options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default);
。。。
});
。。。
internal record Todo(int Id, string? Title, DateOnly? DueBy = null, bool IsComplete = false);

[JsonSerializable(typeof(Todo[]))]
internal partial class AppJsonSerializerContext : JsonSerializerContext
{
}

不必在意这个 Todo ,它只是为了调用 System.Text.Json 的 SourceGenerator.

日志

由于这个 WebApplicaton 是嵌入到桌面程序中的, 所以不能直观的看到它抛出的异常, 不方便定位问题, 所以需要一个可以输出到界面上的 LogProvider.
注意:Log4Net 目前不支持 AOT。

    internal sealed class ObservableCollectionLoggerProvider : ILoggerProvider
    {

        private readonly ObservableCollection<string> collection;

        public ObservableCollectionLoggerProvider(ObservableCollection<string> collection)
        {
            this.collection = collection;
        }

        public ILogger CreateLogger(string categoryName)
        {
            return new ObservableCollectionLogger(this.collection, categoryName);
        }


        private class ObservableCollectionLogger : ILogger
        {
            private readonly ObservableCollection<string> collection;

            private readonly string categoryName;

            public ObservableCollectionLogger(ObservableCollection<string> collection, string categoryName)
            {
                this.collection = collection;
                this.categoryName = categoryName;
            }

            public IDisposable? BeginScope<TState>(TState state) where TState : notnull
            {
                return new NoopDisposable();
            }

            public bool IsEnabled(LogLevel logLevel)
            {
                return true;
            }

            public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
            {
                string message = "";
                if (formatter != null)
                {
                    message += formatter(state, exception);
                    //formatter 没有打印 exception 的具体信息
                    //目前不了解如何自定义 formatter, 用如下简单方法处理
                    if (exception != null)
                    {
                        message = $"{message}\r\n{exception.Message}\r\n{exception.StackTrace}";
                    }
                }

                this.collection.Add($"{DateTime.Now:HH:mm:ss}\t{logLevel}\t{this.categoryName}\r\n{message}\r\n");
            }
        }

        private sealed class NoopDisposable : IDisposable
        {
            public void Dispose()
            {
            }
        }

        #region dispose
        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }

        ~ObservableCollectionLoggerProvider()
        {
            this.Dispose(false);
        }


        private bool isDisposed = false;
        private void Dispose(bool flag)
        {
            if (!isDisposed)
            {
                if (flag)
                {
                }
                isDisposed = true;
            }
        }
        #endregion        
    }

这里使用了 ObservableCollection<T> ,用它可以监控日志变化,实时的更新到界面上去。

完整示例

    [Regist<ISlimHttpServer>(RegistMode.Singleton)]
    public class SlimHttpServer : ISlimHttpServer, IDisposable
    {
        private readonly ILogger<SlimHttpServer> logger;
        private readonly IEnumerable<ISilmHttpServerModule> modules;
        private CancellationTokenSource? cts;
        private readonly ObservableCollectionLoggerProvider logProvider;
        public ObservableCollection<string> Logs { get; } = new();

        public bool Started { get; private set; }

        public SlimHttpServer(ILogger<SlimHttpServer> logger, IEnumerable<ISilmHttpServerModule> modules)
        {
            this.logger = logger;
            this.modules = modules;

            //文本框显示日志
            this.logProvider = new ObservableCollectionLoggerProvider(this.Logs);
        }

        public bool Start()
        {
            lock (this)
            {
                if (Started)
                    return true;

                this.cts?.Dispose();
                this.cts = new CancellationTokenSource();
                try
                {
                    Task.Factory.StartNew(async () =>
                    {
                        var builder = WebApplication.CreateSlimBuilder();

                        builder.Services.ConfigureHttpJsonOptions(options =>
                        {
                            //特意加的, 不然 AOT 报错
                            options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default);

                            foreach (var m in this.modules)
                            {
                                if (m is ISilmHttpServerModule.IJson json)
                                    options.SerializerOptions.TypeInfoResolverChain.Add(json.JsonTypeInfoResolver);
                            }
                        });

                        //文件日志
                        builder.Logging.AddSimpleFile();
                        
                        builder.Logging.AddProvider(this.logProvider);

                        var certificate = new X509Certificate2(Resources.localhost, "CNBOOKING");

                        builder.WebHost.UseKestrel(o =>
                        {
                            o.ListenLocalhost(9998, oo => oo.UseHttps(certificate));
                            o.ListenLocalhost(9999);
                        });

                        //开启CORS
                        builder.Services.AddCors(c => c.AddDefaultPolicy(cp =>
                            cp.AllowAnyOrigin()
                            .AllowAnyHeader()
                            .AllowAnyMethod()
                        ));

                        var app = builder.Build();

                        app.UseCors();

                        foreach (var m in modules)
                        {
                            var methods = new List<string>() { m.HttpMethod.Method };
                            if (m is ISilmHttpServerModule.IContent content)
                            {
                                app.MapMethods(m.Pattern, methods, async () =>
                                {
                                    var rst = await content.Delegate();
                                    return Results.Content(rst, content.ContentType, content.Encoding);
                                });
                            }
                            else if (m is ISilmHttpServerModule.IHttpContext http)
                            {
                                app.MapMethods(m.Pattern, methods, (h) => http.RequestDelegate.Invoke(h));
                            }
                            else if (m is ISilmHttpServerModule.IJson json)
                            {
                                app.MapMethods(m.Pattern, methods, () => json.Delegate.DynamicInvoke());
                            }
                        }

                        cts.Token.Register(async () =>
                        {
                            certificate?.Dispose();
                            await app.DisposeAsync();
                        });

                        await app.RunAsync();

                    }, this.cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Current);

                    this.Started = true;
                    return true;
                }
                catch (Exception ex)
                {
                    this.Started = false;
                    this.logger.LogError(ex, ex.Message);
                    return false;
                }
            }
        }

        public bool Stop()
        {
            this.cts?.Cancel();
            this.Started = false;
            return true;
        }

        #region dispose
        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }

        ~SlimHttpServer()
        {
            this.Dispose(false);
        }

        private bool isDisposed = false;
        private void Dispose(bool flag)
        {
            if (!isDisposed)
            {
                if (flag)
                {
                    this.cts?.Dispose();
                    this.logProvider.Dispose();
                }
                isDisposed = true;
            }
        }
        #endregion
    }

接口定义:

    public interface ISilmHttpServerModule
    {
        string Pattern { get; }
        HttpMethod HttpMethod { get; }

        public interface IJson : ISilmHttpServerModule
        {
            IJsonTypeInfoResolver JsonTypeInfoResolver { get; }
            Delegate Delegate { get; }
        }

        public interface IContent : ISilmHttpServerModule
        {
            string? ContentType { get; }
            Encoding? Encoding { get; }
            Func<Task<string>> Delegate { get; }
        }

        public interface IHttpContext : ISilmHttpServerModule
        {
            Func<HttpContext, Task> RequestDelegate { get; }
        }
    }

Json Module 示例

   [Regist<ISilmHttpServerModule>(RegistMode.Singleton)]
   public class Json : ISilmHttpServerModule.IJson
   {
       public IJsonTypeInfoResolver JsonTypeInfoResolver => AppJsonSerializerContext.Default;
       public Delegate Delegate => () => new List<Item>() {
           new Item() { ID = 1, Name = "Xling"}
       };
       public string Pattern => "/test/3";
       public HttpMethod HttpMethod => HttpMethod.Get;
   }


   public class Item
   {
       public int ID { get; set; }
       public string? Name { get; set; }
   }

   [JsonSerializable(typeof(IEnumerable<Item>))]
   internal partial class AppJsonSerializerContext : JsonSerializerContext
   {
   }

HttpCotnext Module 示例

    [Regist<ISilmHttpServerModule>(RegistMode.Singleton)]
    public class WithHttpContext : ISilmHttpServerModule.IHttpContext
    {
        public string Pattern => "/test/2";
        public HttpMethod HttpMethod => HttpMethod.Get;
        public Func<HttpContext, Task> RequestDelegate => async (h) => await h.Response.WriteAsync(DateTime.Now.ToString());
    }
~~~
a
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,122评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,070评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,491评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,636评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,676评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,541评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,292评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,211评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,655评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,846评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,965评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,684评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,295评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,894评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,012评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,126评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,914评论 2 355

推荐阅读更多精彩内容