之前在一個 .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