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