添加一個 HttpServer 到桌面程序中

之前在一個 .NET3.5 的桌面程序中鹰霍,內(nèi)嵌的 HttpServer 是通過 HttpListener 來實現(xiàn)的碗短, 但是這個東西對 Https 證書 支持的不是很好唱凯,找了很多文檔喜庞,都說是把證書導(dǎo)入到計算機。但是作為一個要分發(fā)的應(yīng)用程序终娃,不可能這樣做味廊。為此, 我還用 學(xué)了幾天的 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 只能祭上反編譯工具今瀑, 導(dǎo)出代碼,在放到自己的項目中去点把。

<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 探赫,它只是為了調(diào)用 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> 魂拦,用它可以監(jiān)控日志變化毛仪,實時的更新到界面上去。

完整示例

    [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
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末芯勘,一起剝皮案震驚了整個濱河市箱靴,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌荷愕,老刑警劉巖衡怀,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異安疗,居然都是意外死亡抛杨,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進店門荐类,熙熙樓的掌柜王于貴愁眉苦臉地迎上來怖现,“玉大人,你說我怎么就攤上這事掉冶≌媸” “怎么了?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵厌小,是天一觀的道長。 經(jīng)常有香客問我战秋,道長璧亚,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任脂信,我火速辦了婚禮癣蟋,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘狰闪。我一直安慰自己疯搅,他們只是感情好,可當我...
    茶點故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布埋泵。 她就那樣靜靜地躺著幔欧,像睡著了一般罪治。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上礁蔗,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天觉义,我揣著相機與錄音,去河邊找鬼浴井。 笑死晒骇,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的磺浙。 我是一名探鬼主播洪囤,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼撕氧!你這毒婦竟也來了箍鼓?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤呵曹,失蹤者是張志新(化名)和其女友劉穎款咖,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體奄喂,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡铐殃,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了跨新。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片富腊。...
    茶點故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖域帐,靈堂內(nèi)的尸體忽然破棺而出赘被,到底是詐尸還是另有隱情,我是刑警寧澤肖揣,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布民假,位于F島的核電站,受9級特大地震影響龙优,放射性物質(zhì)發(fā)生泄漏羊异。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一彤断、第九天 我趴在偏房一處隱蔽的房頂上張望野舶。 院中可真熱鬧,春花似錦宰衙、人聲如沸平道。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽一屋。三九已至窘疮,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間陆淀,已是汗流浹背考余。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留轧苫,地道東北人楚堤。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像含懊,于是被迫代替她去往敵國和親身冬。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,976評論 2 355

推薦閱讀更多精彩內(nèi)容