A:增加Server端使用示例

This commit is contained in:
jimmy committed 2025-11-17 10:54:34 +08:00
1 parent 989747a2ae
commit b14721d79e
23 files changed
+1411 -37

No files matched your search

-19
View File
@@ -1,19 +0,0 @@
using System.Windows;
using Sharp.Wpf.Exceptions;
namespace Client;
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
var handler = new UiExceptionHandler();
handler.Register(new ExceptionHandlingOptions
{
ShowDialog = true,
DialogTitle = "应用程序错误"
});
}
}
+153
View File
@@ -0,0 +1,153 @@
using System;
using System.Collections.Generic;
using System.IO;
namespace Config;
/// <summary>
/// 描述默认配置构建时的约定。
/// </summary>
public sealed class ConfigDefaultsOptions
{
public ConfigDefaultsOptions()
{
BasePath = AppContext.BaseDirectory;
EnvironmentName = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT")
?? Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
}
/// <summary>
/// 主配置文件(默认 appsettings.json)
/// </summary>
public string MainFile { get; set; } = "appsettings.json";
/// <summary>
/// 主配置文件是否可选,默认 false 保证文件缺失即时暴露。
/// </summary>
public bool MainFileOptional { get; set; }
/// <summary>
/// 要使用的环境名称(默认 DOTNET_ENVIRONMENT / ASPNETCORE_ENVIRONMENT)
/// </summary>
public string? EnvironmentName { get; set; }
/// <summary>
/// 是否自动尝试加载 appsettings.{Environment}.json(默认 true)
/// </summary>
public bool IncludeEnvironmentSpecificFile { get; set; } = true;
/// <summary>
/// 基础路径,默认为 AppContext.BaseDirectory
/// </summary>
public string? BasePath { get; set; }
/// <summary>
/// 是否包含环境变量(默认 true)
/// </summary>
public bool IncludeEnvironmentVariables { get; set; } = true;
/// <summary>
/// 环境变量前缀,null 表示不过滤
/// </summary>
public string? EnvironmentPrefix { get; set; }
/// <summary>
/// 额外要加载的文件,默认全部按 Optional=true 处理
/// </summary>
public IList<string> AdditionalFiles { get; } = new List<string>();
/// <summary>
/// 额外的配置源描述,便于手动指定 Optional 或自定义格式。
/// </summary>
public IList<ConfigSource> AdditionalSources { get; } = new List<ConfigSource>();
}
/// <summary>
/// 提供“约定优于配置”的默认构建方式。
/// </summary>
public static class ConfigDefaults
{
/// <summary>
/// 构建一个默认的 ConfigManager:自动加载 appsettings.json(+环境变体) + 环境变量。
/// </summary>
public static ConfigManager BuildDefault(
Action<ConfigDefaultsOptions>? defaults = null,
Action<ConfigOptions>? configure = null)
{
return ConfigBuilder.Build(options =>
{
var defaultOptions = new ConfigDefaultsOptions();
defaults?.Invoke(defaultOptions);
ApplyDefaults(options, defaultOptions);
configure?.Invoke(options);
});
}
/// <summary>
/// 直接构建并获取强类型配置,默认启用数据注解验证。
/// </summary>
public static T Load<T>(
bool validate = true,
string? section = null,
Action<ConfigDefaultsOptions>? defaults = null,
Action<ConfigOptions>? configure = null) where T : new()
{
var manager = BuildDefault(defaults, configure);
return manager.Get<T>(section, validate);
}
internal static void ApplyDefaults(ConfigOptions target, ConfigDefaultsOptions defaults)
{
if (defaults.BasePath is not null)
{
target.BasePath = defaults.BasePath;
}
target.IncludeEnvironmentVariables = defaults.IncludeEnvironmentVariables;
target.EnvironmentPrefix = defaults.EnvironmentPrefix;
target.Sources.Add(CreateSource(defaults.MainFile, defaults.MainFileOptional));
if (defaults.IncludeEnvironmentSpecificFile && !string.IsNullOrWhiteSpace(defaults.EnvironmentName))
{
var envFile = BuildEnvironmentSpecificFile(defaults.MainFile, defaults.EnvironmentName!);
target.Sources.Add(CreateSource(envFile, optional: true));
}
foreach (var file in defaults.AdditionalFiles)
{
target.Sources.Add(CreateSource(file, optional: true));
}
foreach (var source in defaults.AdditionalSources)
{
target.Sources.Add(source);
}
}
private static ConfigSource CreateSource(string path, bool optional)
{
if (!ConfigSource.TryGuess(path, out var guessed))
{
throw new NotSupportedException($"Unsupported config extension: {path}");
}
return new ConfigSource
{
Path = guessed.Path,
Format = guessed.Format,
Optional = optional
};
}
private static string BuildEnvironmentSpecificFile(string mainFile, string environment)
{
var ext = Path.GetExtension(mainFile);
var name = Path.GetFileNameWithoutExtension(mainFile);
var dir = Path.GetDirectoryName(mainFile);
var envFile = $"{name}.{environment}{ext}";
return string.IsNullOrEmpty(dir) ? envFile : Path.Combine(dir, envFile);
}
}
@@ -1,7 +1,43 @@
using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace Config;
public interface IConfigAccessor<out T>
{
T Value { get; }
}
internal sealed class ConfigAccessor<T> : IConfigAccessor<T>, IDisposable where T : class, new()
{
private readonly ConfigManager _manager;
private readonly IDisposable _subscription;
private readonly string? _section;
private readonly bool _validate;
private readonly Action<T>? _onReload;
private T _current;
public ConfigAccessor(ConfigManager manager, string? section, bool validate, Action<T>? onReload)
{
_manager = manager;
_section = section;
_validate = validate;
_onReload = onReload;
_current = manager.Get<T>(_section, _validate);
_subscription = manager.OnReload(() =>
{
var next = _manager.Get<T>(_section, _validate);
_current = next;
_onReload?.Invoke(next);
});
}
public T Value => _current;
public void Dispose() => _subscription.Dispose();
}
public static class ConfigServiceCollectionExtensions
{
/// <summary>
@@ -17,5 +53,52 @@ public static class ConfigServiceCollectionExtensions
});
return services;
}
}
/// <summary>
/// 使用 ConfigDefaults 的约定快速注册 ConfigManager。
/// </summary>
public static IServiceCollection AddDefaultConfig(this IServiceCollection services,
Action<ConfigDefaultsOptions>? defaults = null,
Action<ConfigOptions>? configure = null)
{
return services.AddConfig(options =>
{
var defaultOptions = new ConfigDefaultsOptions();
defaults?.Invoke(defaultOptions);
ConfigDefaults.ApplyDefaults(options, defaultOptions);
configure?.Invoke(options);
});
}
/// <summary>
/// 将强类型配置绑定注册为单例,并在配置热更新时自动刷新。
/// </summary>
public static IServiceCollection AddConfigBinding<T>(this IServiceCollection services,
string? section = null,
bool validate = true,
Action<T>? onReload = null) where T : class, new()
{
services.TryAddSingleton<IConfigAccessor<T>>(sp =>
{
var manager = sp.GetRequiredService<ConfigManager>();
return new ConfigAccessor<T>(manager, section, validate, onReload);
});
services.TryAddSingleton(sp => sp.GetRequiredService<IConfigAccessor<T>>().Value);
return services;
}
/// <summary>
/// 同时注册默认配置管线以及强类型绑定。
/// </summary>
public static IServiceCollection AddDefaultConfigBinding<T>(this IServiceCollection services,
Action<ConfigDefaultsOptions>? defaults = null,
Action<ConfigOptions>? configure = null,
string? section = null,
bool validate = true,
Action<T>? onReload = null) where T : class, new()
{
services.AddDefaultConfig(defaults, configure);
services.AddConfigBinding(section, validate, onReload);
return services;
}
}
+24 -1
View File
@@ -36,6 +36,30 @@
## 使用方式
### 0) 约定优于配置的快速接入
```
// 1 行代码拿到强类型配置,并做数据注解验证
var settings = ConfigDefaults.Load<AppSettings>();
// 或者先构建 ConfigManager,方便后续使用 IConfiguration
var manager = ConfigDefaults.BuildDefault(opt =>
{
opt.EnvironmentPrefix = "APP"; // 仅读取 APP__ 前缀的环境变量
opt.AdditionalFiles.Add("extra.ini"); // 按需附加其它文件,默认 optional
});
var settings = manager.Get<AppSettings>(validate: true);
```
依赖注入中也可以一步完成“构建+绑定+热更新”:
```
services.AddDefaultConfigBinding<AppSettings>(defaults =>
{
defaults.EnvironmentName = "Production"; // 手动指定环境
}, section: "App", onReload: s => logger.LogInformation("配置已更新"));
```
### 1) 从文件构建(常见)
```
@@ -183,4 +207,3 @@ var cm = provider.GetRequiredService<ConfigManager>();
- 提供选项开关以切换 INI 映射策略(是否复制根键到各节)
- 为 INI/TOML 增加自定义 `FileConfigurationProvider`,支持 `ReloadOnChange`
- 增强序列化器的集合/字典类型支持(当前对 INI 做了简化)
+14 -10
View File
@@ -5,17 +5,25 @@
## 快速开始
```
using Config;
// 直接按照约定加载 appsettings.json (+ 环境变体)
var settings = ConfigDefaults.Load<AppSettings>();
var cm = ConfigBuilder.BuildFromFiles(
// 如果需要继续访问 IConfiguration,可先拿到 ConfigManager
var cm = ConfigDefaults.BuildDefault(defaults =>
{
defaults.AdditionalFiles.Add("extra.ini");
defaults.EnvironmentPrefix = "APP";
});
var settings2 = cm.Get<AppSettings>(validate: true);
// 传统 BuildFromFiles 仍然可用
var cmManual = ConfigBuilder.BuildFromFiles(
new[] { "appsettings.json", "settings.ini", "extra.xml", "feature.toml" },
reloadOnChange: true,
includeEnvironment: true,
environmentPrefix: "APP",
basePath: AppContext.BaseDirectory
);
var settings = cm.Get<AppSettings>(validate: true);
```
```
@@ -95,13 +103,10 @@ using Microsoft.Extensions.DependencyInjection;
using Config;
var services = new ServiceCollection();
services.AddConfig(opt =>
{
opt.BasePath = AppContext.BaseDirectory;
opt.Sources.Add(new ConfigSource { Path = "appsettings.json", Format = ConfigFormat.Json });
});
services.AddDefaultConfigBinding<AppSettings>(section: "App");
var sp = services.BuildServiceProvider();
var settings = sp.GetRequiredService<AppSettings>();
var cm = sp.GetRequiredService<ConfigManager>();
```
@@ -111,4 +116,3 @@ var cm = sp.GetRequiredService<ConfigManager>();
- INI 映射默认会将根键复制到一级节(若该节没定义同名键),有助于简化配置;如不符合你的期望,可在后续版本通过选项开关关闭
- TOML 暂不直接 `LoadFromFile<T>`,推荐通过配置管线 + 绑定
- JSON/XML 支持 `ReloadOnChange`;INI/TOML 默认不启用文件级热更新
+9
View File
@@ -0,0 +1,9 @@
<Application x:Class="Server.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Server"
StartupUri="MainWindow.xaml">
<Application.Resources>
</Application.Resources>
</Application>
+142
View File
@@ -0,0 +1,142 @@
using System.IO;
using System.Windows;
using Microsoft.Extensions.Logging;
using Config;
using Microsoft.Extensions.DependencyInjection;
using Sharp.Log;
using Server.Config;
using Server.Services;
namespace Server
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
public ConfigManager? ConfigManager { get; private set; }
public AppConfig? AppConfig { get; private set; }
public ILoggerFactory? LoggerFactory { get; private set; }
public ServerService? ServerService { get; private set; }
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
try
{
// 1. 初始化配置
InitializeConfiguration();
// 2. 初始化日志系统
InitializeLogging();
// 3. 初始化服务器服务
InitializeServerService();
var logger = LoggerFactory?.CreateLogger<App>();
logger?.LogInformation("应用程序启动成功");
}
catch (Exception ex)
{
MessageBox.Show(
$"应用程序初始化失败: {ex.Message}\n\n{ex.StackTrace}",
"启动错误",
MessageBoxButton.OK,
MessageBoxImage.Error);
Shutdown(1);
}
}
private void InitializeConfiguration()
{
ConfigManager = ConfigDefaults.BuildDefault();
AppConfig = ConfigManager.Get<AppConfig>(validate: true);
}
private void InitializeLogging()
{
if (AppConfig == null)
{
throw new InvalidOperationException("配置未初始化");
}
// 确保日志目录存在
var logDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, AppConfig.Logging.LogDirectory);
if (!Directory.Exists(logDirectory))
{
Directory.CreateDirectory(logDirectory);
}
// 使用标准的Microsoft.Extensions.Logging配置
var services = new Microsoft.Extensions.DependencyInjection.ServiceCollection();
services.AddLogging(logging =>
{
logging.SetMinimumLevel(ParseLogLevel(AppConfig.Logging.LogLevel));
if (AppConfig.Logging.LogToConsole)
{
logging.AddConsole();
}
// 如果需要文件日志,可以添加其他日志提供程序
// 这里使用Sharp.Log的扩展方法
// logging.AddSharpLog();
});
var serviceProvider = services.BuildServiceProvider();
LoggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
}
private void InitializeServerService()
{
if (AppConfig == null || LoggerFactory == null)
{
throw new InvalidOperationException("配置或日志系统未初始化");
}
var logger = LoggerFactory.CreateLogger<ServerService>();
ServerService = new ServerService(AppConfig, logger);
}
private Microsoft.Extensions.Logging.LogLevel ParseLogLevel(string level)
{
return level.ToLower() switch
{
"trace" => Microsoft.Extensions.Logging.LogLevel.Trace,
"debug" => Microsoft.Extensions.Logging.LogLevel.Debug,
"information" => Microsoft.Extensions.Logging.LogLevel.Information,
"warning" => Microsoft.Extensions.Logging.LogLevel.Warning,
"error" => Microsoft.Extensions.Logging.LogLevel.Error,
"critical" => Microsoft.Extensions.Logging.LogLevel.Critical,
"none" => Microsoft.Extensions.Logging.LogLevel.None,
_ => Microsoft.Extensions.Logging.LogLevel.Information
};
}
protected override void OnExit(ExitEventArgs e)
{
try
{
// 停止服务器
ServerService?.Dispose();
// 释放日志资源
LoggerFactory?.Dispose();
var logger = LoggerFactory?.CreateLogger<App>();
logger?.LogInformation("应用程序正常退出");
}
catch (Exception ex)
{
// 忽略退出时的异常
Console.WriteLine($"退出时出错: {ex.Message}");
}
base.OnExit(e);
}
}
}
+10
View File
@@ -0,0 +1,10 @@
using System.Windows;
[assembly: ThemeInfo(
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
//(used if a resource is not found in the page,
// or application resource dictionaries)
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
//(used if a resource is not found in the page,
// app, or any theme specific resource dictionaries)
)]
+48
View File
@@ -0,0 +1,48 @@
using System.ComponentModel.DataAnnotations;
namespace Server.Config
{
/// <summary>
/// 服务器配置
/// </summary>
public class ServerConfig
{
[Range(1, 65535)]
public int Port { get; set; } = 8080;
[Required]
public string Host { get; set; } = "localhost";
[Range(1, 10000)]
public int MaxConcurrentRequests { get; set; } = 100;
}
/// <summary>
/// 日志配置
/// </summary>
public class LoggingConfig
{
[Required]
public string LogLevel { get; set; } = "Information";
[Required]
public string LogDirectory { get; set; } = "logs";
[Range(1, 365)]
public int MaxLogFiles { get; set; } = 30;
public bool LogToConsole { get; set; } = true;
public bool LogToFile { get; set; } = true;
}
/// <summary>
/// 应用配置(根配置)
/// </summary>
public class AppConfig
{
public ServerConfig Server { get; set; } = new();
public LoggingConfig Logging { get; set; } = new();
}
}
@@ -0,0 +1,202 @@
using Sharp.NetServer.Attributes;
using Sharp.NetServer.Controllers;
using Sharp.NetServer.Extensions;
using Sharp.NetServer.Models;
using System.Collections.Concurrent;
namespace Server.Controllers
{
/// <summary>
/// 数据操作控制器(演示CRUD操作)
/// </summary>
[Route("/api/data")]
public class DataController : BaseController
{
// 内存存储(演示用)
private static readonly ConcurrentDictionary<string, DataItem> _dataStore = new();
/// <summary>
/// 获取所有数据
/// GET /api/data
/// </summary>
[HttpGet]
public async Task GetAll(HttpContext context)
{
var items = _dataStore.Values.ToList();
var result = ResultExtensions.HttpSuccess(new
{
Count = items.Count,
Items = items
});
await WriteSuccessResponseAsync(context, result);
}
/// <summary>
/// 获取指定ID的数据
/// GET /api/data/123
/// </summary>
[HttpGet("{id}")]
public async Task GetById(HttpContext context)
{
var id = context.Request.Url.Segments.LastOrDefault()?.TrimEnd('/');
if (string.IsNullOrEmpty(id))
{
await BadRequestAsync(context, "Invalid ID");
return;
}
if (_dataStore.TryGetValue(id, out var item))
{
var result = ResultExtensions.HttpSuccess(item);
await WriteSuccessResponseAsync(context, result);
}
else
{
var result = ResultExtensions.HttpFailure<object>($"Item with id '{id}' not found", 404);
await NotFoundAsync(context, result.Message);
}
}
/// <summary>
/// 创建新数据
/// POST /api/data
/// Body: { "name": "test", "value": "data" }
/// </summary>
[HttpPost]
public async Task Create(HttpContext context)
{
var request = await ReadJsonBodyAsync<CreateDataRequest>(context);
if (request == null || string.IsNullOrWhiteSpace(request.Name))
{
await BadRequestAsync(context, "Name is required");
return;
}
var id = Guid.NewGuid().ToString();
var item = new DataItem
{
Id = id,
Name = request.Name,
Value = request.Value,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
};
_dataStore[id] = item;
var result = ResultExtensions.HttpSuccess(item, "Data created successfully");
await WriteSuccessResponseAsync(context, result);
}
/// <summary>
/// 更新数据
/// PUT /api/data/123
/// Body: { "name": "updated", "value": "new data" }
/// </summary>
[HttpPut("{id}")]
public async Task Update(HttpContext context)
{
var id = context.Request.Url.Segments.LastOrDefault()?.TrimEnd('/');
if (string.IsNullOrEmpty(id))
{
await BadRequestAsync(context, "Invalid ID");
return;
}
if (!_dataStore.TryGetValue(id, out var item))
{
await NotFoundAsync(context, $"Item with id '{id}' not found");
return;
}
var request = await ReadJsonBodyAsync<UpdateDataRequest>(context);
if (request != null)
{
if (!string.IsNullOrWhiteSpace(request.Name))
{
item.Name = request.Name;
}
if (request.Value != null)
{
item.Value = request.Value;
}
item.UpdatedAt = DateTime.UtcNow;
}
var result = ResultExtensions.HttpSuccess(item, "Data updated successfully");
await WriteSuccessResponseAsync(context, result);
}
/// <summary>
/// 删除数据
/// DELETE /api/data/123
/// </summary>
[HttpDelete("{id}")]
public async Task Delete(HttpContext context)
{
var id = context.Request.Url.Segments.LastOrDefault()?.TrimEnd('/');
if (string.IsNullOrEmpty(id))
{
await BadRequestAsync(context, "Invalid ID");
return;
}
if (_dataStore.TryRemove(id, out var item))
{
var result = ResultExtensions.HttpSuccess(item, "Data deleted successfully");
await WriteSuccessResponseAsync(context, result);
}
else
{
await NotFoundAsync(context, $"Item with id '{id}' not found");
}
}
/// <summary>
/// 清空所有数据
/// DELETE /api/data
/// </summary>
[HttpDelete]
public async Task Clear(HttpContext context)
{
var count = _dataStore.Count;
_dataStore.Clear();
var result = ResultExtensions.HttpSuccess(new { DeletedCount = count }, "All data cleared");
await WriteSuccessResponseAsync(context, result);
}
}
#region Request/Response Models
public class CreateDataRequest
{
public string Name { get; set; } = string.Empty;
public string? Value { get; set; }
}
public class UpdateDataRequest
{
public string? Name { get; set; }
public string? Value { get; set; }
}
public class DataItem
{
public string Id { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string? Value { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
}
#endregion
}
@@ -0,0 +1,69 @@
using System.Diagnostics;
using Sharp.NetServer.Attributes;
using Sharp.NetServer.Controllers;
using Sharp.NetServer.Extensions;
using Sharp.NetServer.Models;
namespace Server.Controllers
{
/// <summary>
/// 健康检查控制器
/// </summary>
[Route("/api/health")]
public class HealthController : BaseController
{
/// <summary>
/// 健康检查端点
/// GET /api/health
/// </summary>
[HttpGet]
public async Task Check(HttpContext context)
{
var result = ResultExtensions.HttpSuccess(new
{
Status = "Healthy",
Timestamp = DateTime.UtcNow,
Uptime = DateTime.UtcNow - Process.GetCurrentProcess().StartTime
});
await WriteSuccessResponseAsync(context, result);
}
/// <summary>
/// 详细健康检查
/// GET /api/health/detailed
/// </summary>
[HttpGet("/detailed")]
public async Task DetailedCheck(HttpContext context)
{
var process = Process.GetCurrentProcess();
var result = ResultExtensions.HttpSuccess(new
{
Status = "Healthy",
Timestamp = DateTime.UtcNow,
System = new
{
MachineName = Environment.MachineName,
OSVersion = Environment.OSVersion.ToString(),
ProcessorCount = Environment.ProcessorCount,
Is64BitOS = Environment.Is64BitOperatingSystem
},
Process = new
{
Id = process.Id,
Name = process.ProcessName,
StartTime = process.StartTime,
Memory = new
{
WorkingSet = $"{process.WorkingSet64 / 1024 / 1024} MB",
PrivateMemory = $"{process.PrivateMemorySize64 / 1024 / 1024} MB"
},
Threads = process.Threads.Count
}
});
await WriteSuccessResponseAsync(context, result);
}
}
}
@@ -0,0 +1,98 @@
using Sharp.NetServer.Attributes;
using Sharp.NetServer.Controllers;
using Sharp.NetServer.Extensions;
using Sharp.NetServer.Models;
namespace Server.Controllers
{
/// <summary>
/// 系统信息控制器
/// </summary>
[Route("/api/system")]
public class SystemController : BaseController
{
/// <summary>
/// 获取系统信息
/// GET /api/system/info
/// </summary>
[HttpGet("/info")]
public async Task GetSystemInfo(HttpContext context)
{
var result = ResultExtensions.HttpSuccess(new
{
MachineName = Environment.MachineName,
UserName = Environment.UserName,
OSVersion = Environment.OSVersion.ToString(),
ProcessorCount = Environment.ProcessorCount,
Is64BitOS = Environment.Is64BitOperatingSystem,
Is64BitProcess = Environment.Is64BitProcess,
SystemDirectory = Environment.SystemDirectory,
CurrentDirectory = Environment.CurrentDirectory,
TickCount = Environment.TickCount64,
Version = Environment.Version.ToString()
});
await WriteSuccessResponseAsync(context, result);
}
/// <summary>
/// 获取环境变量
/// GET /api/system/env?name=PATH
/// </summary>
[HttpGet("/env")]
public async Task GetEnvironmentVariable(HttpContext context)
{
var name = GetQueryParam(context, "name", "");
if (string.IsNullOrWhiteSpace(name))
{
var result = ResultExtensions.HttpFailure<object>("Environment variable name is required", 400);
await WriteErrorResponseAsync(context, result.Message, 400);
return;
}
var value = Environment.GetEnvironmentVariable(name);
if (value == null)
{
var result = ResultExtensions.HttpFailure<object>($"Environment variable '{name}' not found", 404);
await NotFoundAsync(context, result.Message);
return;
}
var successResult = ResultExtensions.HttpSuccess(new { Name = name, Value = value });
await WriteSuccessResponseAsync(context, successResult);
}
/// <summary>
/// 获取当前时间
/// GET /api/system/time
/// </summary>
[HttpGet("/time")]
public async Task GetCurrentTime(HttpContext context)
{
var now = DateTime.Now;
var utcNow = DateTime.UtcNow;
var result = ResultExtensions.HttpSuccess(new
{
Local = new
{
DateTime = now,
Date = now.Date,
Time = now.TimeOfDay,
DayOfWeek = now.DayOfWeek.ToString()
},
UTC = new
{
DateTime = utcNow,
Date = utcNow.Date,
Time = utcNow.TimeOfDay,
UnixTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
}
});
await WriteSuccessResponseAsync(context, result);
}
}
}
+112
View File
@@ -0,0 +1,112 @@
<Window x:Class="Server.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Server"
mc:Ignorable="d"
Title="Sharp HTTP Server" Height="600" Width="900"
WindowStartupLocation="CenterScreen">
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 标题栏 -->
<Border Grid.Row="0" Background="#2196F3" Padding="15" CornerRadius="5" Margin="0,0,0,10">
<StackPanel>
<TextBlock Text="Sharp HTTP Server" FontSize="24" FontWeight="Bold" Foreground="White"/>
<TextBlock Text="基于 NetServer 框架的 HTTP 服务器演示" FontSize="12" Foreground="White" Opacity="0.9"/>
</StackPanel>
</Border>
<!-- 服务器控制面板 -->
<Border Grid.Row="1" BorderBrush="#E0E0E0" BorderThickness="1" CornerRadius="5" Padding="15" Margin="0,0,0,10">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- 服务器信息 -->
<TextBlock Grid.Row="0" Grid.Column="0" Text="服务器地址:" VerticalAlignment="Center" Margin="0,0,10,5"/>
<TextBlock Grid.Row="0" Grid.Column="1" x:Name="ServerUrlTextBlock" Text="http://localhost:8080"
FontWeight="Bold" VerticalAlignment="Center" Margin="0,0,0,5"/>
<TextBlock Grid.Row="1" Grid.Column="0" Text="服务器状态:" VerticalAlignment="Center" Margin="0,0,10,5"/>
<TextBlock Grid.Row="1" Grid.Column="1" x:Name="StatusTextBlock" Text="未启动"
Foreground="Gray" FontWeight="Bold" VerticalAlignment="Center" Margin="0,0,0,5"/>
<!-- 控制按钮 -->
<StackPanel Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2" Orientation="Horizontal" Margin="0,10,0,0">
<Button x:Name="StartButton" Content="启动服务器" Width="100" Height="32" Margin="0,0,10,0" Click="StartButton_Click"
Background="#4CAF50" Foreground="White" BorderThickness="0" FontWeight="Bold" Cursor="Hand"/>
<Button x:Name="StopButton" Content="停止服务器" Width="100" Height="32" Margin="0,0,10,0" Click="StopButton_Click"
Background="#F44336" Foreground="White" BorderThickness="0" FontWeight="Bold" Cursor="Hand" IsEnabled="False"/>
<Button x:Name="RestartButton" Content="重启服务器" Width="100" Height="32" Margin="0,0,10,0" Click="RestartButton_Click"
Background="#FF9800" Foreground="White" BorderThickness="0" FontWeight="Bold" Cursor="Hand" IsEnabled="False"/>
<Button x:Name="ClearLogButton" Content="清空日志" Width="100" Height="32" Click="ClearLogButton_Click"
Background="#9E9E9E" Foreground="White" BorderThickness="0" FontWeight="Bold" Cursor="Hand"/>
</StackPanel>
</Grid>
</Border>
<!-- 日志显示区域 -->
<Border Grid.Row="2" BorderBrush="#E0E0E0" BorderThickness="1" CornerRadius="5" Padding="10">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="日志信息" FontSize="14" FontWeight="Bold" Margin="0,0,0,10"/>
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto">
<TextBox x:Name="LogTextBox"
IsReadOnly="True"
TextWrapping="Wrap"
Background="#F5F5F5"
BorderThickness="0"
FontFamily="Consolas"
FontSize="11"
Padding="5"/>
</ScrollViewer>
</Grid>
</Border>
<!-- API 端点提示 -->
<Border Grid.Row="3" Background="#E3F2FD" Padding="10" CornerRadius="5" Margin="0,10,0,0">
<StackPanel>
<TextBlock Text="可用的 API 端点:" FontWeight="Bold" Margin="0,0,0,5"/>
<TextBlock FontSize="11">
<Run Text="• GET /api/health - 健康检查"/>
<LineBreak/>
<Run Text="• GET /api/health/detailed - 详细健康检查"/>
<LineBreak/>
<Run Text="• GET /api/system/info - 系统信息"/>
<LineBreak/>
<Run Text="• GET /api/system/time - 当前时间"/>
<LineBreak/>
<Run Text="• GET /api/data - 获取所有数据"/>
<LineBreak/>
<Run Text="• POST /api/data - 创建数据 (Body: {&quot;name&quot;:&quot;test&quot;,&quot;value&quot;:&quot;data&quot;})"/>
<LineBreak/>
<Run Text="• GET /api/data/{id} - 获取指定数据"/>
<LineBreak/>
<Run Text="• PUT /api/data/{id} - 更新数据"/>
<LineBreak/>
<Run Text="• DELETE /api/data/{id} - 删除数据"/>
</TextBlock>
</StackPanel>
</Border>
</Grid>
</Window>
+182
View File
@@ -0,0 +1,182 @@
using System.Windows;
using Server.Services;
using Server.Config;
using Microsoft.Extensions.Logging;
namespace Server
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private ServerService? _serverService;
private AppConfig? _config;
private ILogger<MainWindow>? _logger;
public MainWindow()
{
InitializeComponent();
Loaded += MainWindow_Loaded;
Closing += MainWindow_Closing;
}
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
try
{
// 从App获取依赖
var app = (App)Application.Current;
_config = app.AppConfig;
_logger = app.LoggerFactory?.CreateLogger<MainWindow>();
_serverService = app.ServerService;
if (_config != null && _serverService != null)
{
// 更新UI显示服务器地址
ServerUrlTextBlock.Text = $"http://{_config.Server.Host}:{_config.Server.Port}";
// 订阅服务器状态变化事件
_serverService.StatusChanged += OnServerStatusChanged;
AppendLog("应用程序已启动");
AppendLog($"配置已加载: {_config.Server.Host}:{_config.Server.Port}");
}
else
{
AppendLog("错误: 无法初始化服务器服务");
}
}
catch (Exception ex)
{
_logger?.LogError(ex, "MainWindow初始化失败");
AppendLog($"初始化失败: {ex.Message}");
}
}
private void MainWindow_Closing(object? sender, System.ComponentModel.CancelEventArgs e)
{
try
{
if (_serverService?.IsRunning == true)
{
var result = MessageBox.Show(
"服务器正在运行,是否停止服务器并退出?",
"确认退出",
MessageBoxButton.YesNo,
MessageBoxImage.Question);
if (result == MessageBoxResult.Yes)
{
_serverService.Stop();
AppendLog("服务器已停止");
}
else
{
e.Cancel = true;
}
}
}
catch (Exception ex)
{
_logger?.LogError(ex, "关闭窗口时出错");
AppendLog($"关闭出错: {ex.Message}");
}
}
private void StartButton_Click(object sender, RoutedEventArgs e)
{
try
{
_serverService?.Start();
UpdateButtonStates(isRunning: true);
}
catch (Exception ex)
{
_logger?.LogError(ex, "启动服务器失败");
AppendLog($"启动失败: {ex.Message}");
MessageBox.Show($"启动服务器失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void StopButton_Click(object sender, RoutedEventArgs e)
{
try
{
_serverService?.Stop();
UpdateButtonStates(isRunning: false);
}
catch (Exception ex)
{
_logger?.LogError(ex, "停止服务器失败");
AppendLog($"停止失败: {ex.Message}");
MessageBox.Show($"停止服务器失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void RestartButton_Click(object sender, RoutedEventArgs e)
{
try
{
_serverService?.Restart();
UpdateButtonStates(isRunning: true);
}
catch (Exception ex)
{
_logger?.LogError(ex, "重启服务器失败");
AppendLog($"重启失败: {ex.Message}");
MessageBox.Show($"重启服务器失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void ClearLogButton_Click(object sender, RoutedEventArgs e)
{
LogTextBox.Clear();
}
private void OnServerStatusChanged(object? sender, string status)
{
Dispatcher.Invoke(() =>
{
StatusTextBlock.Text = status;
// 根据状态更新颜色
if (status.Contains("已启动"))
{
StatusTextBlock.Foreground = System.Windows.Media.Brushes.Green;
}
else if (status.Contains("已停止"))
{
StatusTextBlock.Foreground = System.Windows.Media.Brushes.Gray;
}
else if (status.Contains("失败") || status.Contains("错误"))
{
StatusTextBlock.Foreground = System.Windows.Media.Brushes.Red;
}
else
{
StatusTextBlock.Foreground = System.Windows.Media.Brushes.Orange;
}
AppendLog(status);
});
}
private void UpdateButtonStates(bool isRunning)
{
StartButton.IsEnabled = !isRunning;
StopButton.IsEnabled = isRunning;
RestartButton.IsEnabled = isRunning;
}
private void AppendLog(string message)
{
Dispatcher.Invoke(() =>
{
var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
LogTextBox.AppendText($"[{timestamp}] {message}\n");
LogTextBox.ScrollToEnd();
});
}
}
}
+31
View File
@@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\NetServer\NetServer.csproj" />
<ProjectReference Include="..\Log\Log.csproj" />
<ProjectReference Include="..\Config\Config.csproj" />
<ProjectReference Include="..\Wpf\Wpf.csproj" />
<ProjectReference Include="..\Common\Common.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup />
</Project>
+188
View File
@@ -0,0 +1,188 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Sharp.NetServer;
using Sharp.NetServer.Server;
using Server.Config;
using Server.Controllers;
namespace Server.Services
{
/// <summary>
/// HTTP服务器服务,管理服务器生命周期
/// </summary>
public class ServerService
{
private HttpServer? _httpServer;
private Task? _serverTask;
private CancellationTokenSource? _cts;
private readonly AppConfig _config;
private readonly ILogger<ServerService> _logger;
public bool IsRunning { get; private set; }
public event EventHandler<string>? StatusChanged;
public ServerService(AppConfig config, ILogger<ServerService> logger)
{
_config = config;
_logger = logger;
}
/// <summary>
/// 启动服务器
/// </summary>
public void Start()
{
try
{
if (IsRunning)
{
_logger.LogWarning("Server is already running");
return;
}
_logger.LogInformation("Starting HTTP server on {Host}:{Port}", _config.Server.Host, _config.Server.Port);
OnStatusChanged($"正在启动服务器 {_config.Server.Host}:{_config.Server.Port}...");
var builder = new HttpServerBuilder()
.UseUrls($"http://{_config.Server.Host}:{_config.Server.Port}/")
.ConfigureLogging(services =>
{
services.AddLogging(logging =>
{
// 使用应用程序的日志配置
logging.SetMinimumLevel(ParseLogLevel(_config.Logging.LogLevel));
if (_config.Logging.LogToConsole)
{
logging.AddConsole();
}
});
})
.ConfigureServices(services =>
{
// 注册配置
services.AddSingleton(_config);
services.AddSingleton(_config.Server);
services.AddSingleton(_config.Logging);
})
// 注册控制器(从当前程序集)
.AddControllers<ServerService>();
_httpServer = builder.Build();
// 在后台启动服务器
_cts = new CancellationTokenSource();
_serverTask = Task.Run(async () =>
{
try
{
await _httpServer.StartAsync(_cts.Token);
}
catch (Exception ex)
{
_logger.LogError(ex, "Server task error");
OnStatusChanged($"服务器运行错误: {ex.Message}");
IsRunning = false;
}
}, _cts.Token);
// 等待服务器启动
Thread.Sleep(500);
IsRunning = true;
_logger.LogInformation("HTTP server started successfully on {Host}:{Port}", _config.Server.Host, _config.Server.Port);
OnStatusChanged($"服务器已启动: http://{_config.Server.Host}:{_config.Server.Port}");
}
catch (Exception ex)
{
IsRunning = false;
_logger.LogError(ex, "Failed to start HTTP server");
OnStatusChanged($"启动失败: {ex.Message}");
throw;
}
}
/// <summary>
/// 停止服务器
/// </summary>
public void Stop()
{
try
{
if (!IsRunning)
{
_logger.LogWarning("Server is not running");
return;
}
_logger.LogInformation("Stopping HTTP server");
OnStatusChanged("正在停止服务器...");
_cts?.Cancel();
_httpServer?.Stop();
_serverTask?.Wait(TimeSpan.FromSeconds(5));
IsRunning = false;
_logger.LogInformation("HTTP server stopped successfully");
OnStatusChanged("服务器已停止");
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to stop HTTP server");
OnStatusChanged($"停止失败: {ex.Message}");
throw;
}
}
/// <summary>
/// 重启服务器
/// </summary>
public void Restart()
{
_logger.LogInformation("Restarting HTTP server");
OnStatusChanged("正在重启服务器...");
Stop();
Thread.Sleep(500); // 等待端口释放
Start();
OnStatusChanged("服务器已重启");
}
/// <summary>
/// 解析日志级别
/// </summary>
private LogLevel ParseLogLevel(string level)
{
return level.ToLower() switch
{
"trace" => LogLevel.Trace,
"debug" => LogLevel.Debug,
"information" => LogLevel.Information,
"warning" => LogLevel.Warning,
"error" => LogLevel.Error,
"critical" => LogLevel.Critical,
"none" => LogLevel.None,
_ => LogLevel.Information
};
}
private void OnStatusChanged(string status)
{
StatusChanged?.Invoke(this, status);
}
public void Dispose()
{
if (IsRunning)
{
Stop();
}
_cts?.Dispose();
_serverTask?.Dispose();
}
}
}
+14
View File
@@ -0,0 +1,14 @@
{
"Server": {
"Port": 8080,
"Host": "localhost",
"MaxConcurrentRequests": 100
},
"Logging": {
"LogLevel": "Information",
"LogDirectory": "logs",
"MaxLogFiles": 30,
"LogToConsole": true,
"LogToFile": true
}
}
+6
View File
@@ -33,6 +33,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Log.Test", "..\..\test\Log.
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetServer.Test", "..\..\test\NetServer.Test\NetServer.Test.csproj", "{020997C0-FB5F-4222-8620-818840EF4447}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Server", "Server\Server.csproj", "{C2505AD1-D775-4881-A56F-42BDB7CDE58D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -99,6 +101,10 @@ Global
{020997C0-FB5F-4222-8620-818840EF4447}.Debug|Any CPU.Build.0 = Debug|Any CPU
{020997C0-FB5F-4222-8620-818840EF4447}.Release|Any CPU.ActiveCfg = Release|Any CPU
{020997C0-FB5F-4222-8620-818840EF4447}.Release|Any CPU.Build.0 = Release|Any CPU
{C2505AD1-D775-4881-A56F-42BDB7CDE58D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C2505AD1-D775-4881-A56F-42BDB7CDE58D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C2505AD1-D775-4881-A56F-42BDB7CDE58D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C2505AD1-D775-4881-A56F-42BDB7CDE58D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -1,9 +1,10 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// 此代码由工具生成。
// 运行时版本:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// 对此文件的更改可能会导致不正确的行为,并且如果
// 重新生成代码,这些更改将会丢失。
// </auto-generated>
//------------------------------------------------------------------------------
@@ -13,7 +14,7 @@ using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("Sharp.Wpf")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+ab3721912dc64444eabd750973d21817cb24fbac")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+989747a2ae8392dd304ec136a20c73d2714a43b9")]
[assembly: System.Reflection.AssemblyProductAttribute("Sharp.Wpf")]
[assembly: System.Reflection.AssemblyTitleAttribute("Sharp.Wpf")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
@@ -1 +1 @@
9b4de542688aa50112519013d6cf33dd03c8f285c691dd96ec4b3acd75e126bd
6c5c7e08ff66373e9981bab4e0ba90b429207193706551d9719bf8bb6534127e
+1 -1
View File
@@ -1 +1 @@
17631078795867788
17633435623876895