Compare commits
8
Commits
989747a2ae
...
my-main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1d059ed29 | ||
|
|
2739a71134 | ||
|
|
dd993e5b45 | ||
|
|
e334e65c2d | ||
|
|
1d412bba33 | ||
|
|
f7660c49e8 | ||
|
|
53d1dd2dbf | ||
|
|
b14721d79e |
No files matched your search
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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 = "应用程序错误"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,9 +1,16 @@
|
|||||||
<Application x:Class="Client.App"
|
<Application x:Class="Client.App"
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
xmlns:local="clr-namespace:Client"
|
|
||||||
StartupUri="MainWindow.xaml">
|
|
||||||
<Application.Resources>
|
<Application.Resources>
|
||||||
|
<ResourceDictionary>
|
||||||
|
<ResourceDictionary.MergedDictionaries>
|
||||||
|
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/SkinDefault.xaml" />
|
||||||
|
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/Theme.xaml" />
|
||||||
|
</ResourceDictionary.MergedDictionaries>
|
||||||
|
</ResourceDictionary>
|
||||||
</Application.Resources>
|
</Application.Resources>
|
||||||
</Application>
|
</Application>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,12 +1,19 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
using Client.Services;
|
||||||
|
using Client.ViewModels;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Common;
|
using Common;
|
||||||
using Config;
|
using Config;
|
||||||
using NetClient.DependencyInjection;
|
using NetClient.DependencyInjection;
|
||||||
|
using Sharp.Infrastructure.Configuration;
|
||||||
using Sharp.Log.Extensions;
|
using Sharp.Log.Extensions;
|
||||||
using Sharp.NetServer.DependencyInjection;
|
using Sharp.NetServer.DependencyInjection;
|
||||||
|
using Sharp.Wpf.Exceptions;
|
||||||
|
using Sharp.Wpf.Services;
|
||||||
|
|
||||||
namespace Client
|
namespace Client
|
||||||
{
|
{
|
||||||
@@ -20,7 +27,7 @@ namespace Client
|
|||||||
protected override void OnStartup(StartupEventArgs e)
|
protected override void OnStartup(StartupEventArgs e)
|
||||||
{
|
{
|
||||||
base.OnStartup(e);
|
base.OnStartup(e);
|
||||||
|
|
||||||
// 1) Build configuration manager from appsettings.json (+ reload on change + env vars)
|
// 1) Build configuration manager from appsettings.json (+ reload on change + env vars)
|
||||||
var cm = ConfigBuilder.BuildFromFiles(new[] { "appsettings.json" }, reloadOnChange: true, includeEnvironment: true, basePath: AppContext.BaseDirectory);
|
var cm = ConfigBuilder.BuildFromFiles(new[] { "appsettings.json" }, reloadOnChange: true, includeEnvironment: true, basePath: AppContext.BaseDirectory);
|
||||||
|
|
||||||
@@ -29,6 +36,7 @@ namespace Client
|
|||||||
|
|
||||||
// ConfigManager and typed SystemConfig accessor
|
// ConfigManager and typed SystemConfig accessor
|
||||||
services.AddSingleton(cm);
|
services.AddSingleton(cm);
|
||||||
|
services.TryAddSingleton<IConfiguration>(cm.Configuration); // Register IConfiguration for BindConfiguration
|
||||||
services.AddSystemConfig(cm);
|
services.AddSystemConfig(cm);
|
||||||
|
|
||||||
// Logging (binds from section "SharpLog")
|
// Logging (binds from section "SharpLog")
|
||||||
@@ -38,20 +46,49 @@ namespace Client
|
|||||||
services.AddNetClient(cm);
|
services.AddNetClient(cm);
|
||||||
services.AddNetServer(cm);
|
services.AddNetServer(cm);
|
||||||
|
|
||||||
|
// WPF exception handler
|
||||||
|
services.AddSingleton<IUiExceptionHandler, UiExceptionHandler>();
|
||||||
|
|
||||||
// WPF app services
|
// WPF app services
|
||||||
|
services.AddSingleton<IDialogService, DialogService>();
|
||||||
|
services.AddSingleton<IAuthenticationService, DemoAuthenticationService>();
|
||||||
|
services.AddSingleton<IUserSession, UserSession>();
|
||||||
|
|
||||||
|
services.AddTransient<LoginViewModel>();
|
||||||
|
services.AddTransient<MainViewModel>();
|
||||||
|
services.AddTransient<LoginWindow>();
|
||||||
|
services.AddTransient<MainWindow>();
|
||||||
|
|
||||||
_serviceProvider = services.BuildServiceProvider();
|
_serviceProvider = services.BuildServiceProvider();
|
||||||
|
|
||||||
// 3) Register global facades for convenience
|
// 3) Activate global WPF exception handler
|
||||||
|
var exceptionHandler = _serviceProvider.GetRequiredService<IUiExceptionHandler>();
|
||||||
|
exceptionHandler.Register();
|
||||||
|
|
||||||
|
// 4) Register global facades for convenience
|
||||||
Global.RegisterSingleton(_serviceProvider);
|
Global.RegisterSingleton(_serviceProvider);
|
||||||
Global.RegisterSingleton(cm);
|
Global.RegisterSingleton(cm);
|
||||||
Global.RegisterSingleton(_serviceProvider.GetRequiredService<ISystemConfigAccessor>());
|
Global.RegisterSingleton(_serviceProvider.GetRequiredService<ISystemConfigAccessor>());
|
||||||
|
|
||||||
// 4) Show main window
|
// 5) Show login window first, then main shell if authenticated
|
||||||
var main = new MainWindow
|
var logger = _serviceProvider.GetRequiredService<ILogger<App>>();
|
||||||
|
logger.LogInformation("Showing login window");
|
||||||
|
var loginWindow = _serviceProvider.GetRequiredService<LoginWindow>();
|
||||||
|
var loginResult = loginWindow.ShowDialog();
|
||||||
|
|
||||||
|
logger.LogInformation("Login window closed with result: {LoginResult}", loginResult);
|
||||||
|
|
||||||
|
if (loginResult == true)
|
||||||
{
|
{
|
||||||
DataContext = new ViewModels.MainViewModel()
|
logger.LogInformation("Login successful, showing main window");
|
||||||
};
|
var main = _serviceProvider.GetRequiredService<MainWindow>();
|
||||||
main.Show();
|
main.Show();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
logger.LogInformation("Login cancelled or failed, shutting down application");
|
||||||
|
Shutdown();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnExit(ExitEventArgs e)
|
protected override void OnExit(ExitEventArgs e)
|
||||||
@@ -60,4 +97,4 @@ namespace Client
|
|||||||
base.OnExit(e);
|
base.OnExit(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.1" />
|
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.1" />
|
||||||
|
<PackageReference Include="HandyControl" Version="3.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -19,6 +20,7 @@
|
|||||||
<ProjectReference Include="..\NetClient\NetClient.csproj" />
|
<ProjectReference Include="..\NetClient\NetClient.csproj" />
|
||||||
<ProjectReference Include="..\NetServer\NetServer.csproj" />
|
<ProjectReference Include="..\NetServer\NetServer.csproj" />
|
||||||
<ProjectReference Include="..\Log\Log.csproj" />
|
<ProjectReference Include="..\Log\Log.csproj" />
|
||||||
|
<ProjectReference Include="..\Sharp.Infrastructure\Sharp.Infrastructure.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
|
||||||
|
namespace Client.Helpers;
|
||||||
|
|
||||||
|
public static class PasswordBoxHelper
|
||||||
|
{
|
||||||
|
public static readonly DependencyProperty BindablePasswordProperty =
|
||||||
|
DependencyProperty.RegisterAttached(
|
||||||
|
"BindablePassword",
|
||||||
|
typeof(string),
|
||||||
|
typeof(PasswordBoxHelper),
|
||||||
|
new PropertyMetadata(null, OnBindablePasswordChanged));
|
||||||
|
|
||||||
|
private static readonly DependencyProperty IsUpdatingProperty =
|
||||||
|
DependencyProperty.RegisterAttached(
|
||||||
|
"IsUpdating",
|
||||||
|
typeof(bool),
|
||||||
|
typeof(PasswordBoxHelper),
|
||||||
|
new PropertyMetadata(false));
|
||||||
|
|
||||||
|
public static string GetBindablePassword(DependencyObject obj) => (string)obj.GetValue(BindablePasswordProperty);
|
||||||
|
public static void SetBindablePassword(DependencyObject obj, string value) => obj.SetValue(BindablePasswordProperty, value);
|
||||||
|
|
||||||
|
private static bool GetIsUpdating(DependencyObject obj) => (bool)obj.GetValue(IsUpdatingProperty);
|
||||||
|
private static void SetIsUpdating(DependencyObject obj, bool value) => obj.SetValue(IsUpdatingProperty, value);
|
||||||
|
|
||||||
|
private static void OnBindablePasswordChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (d is not PasswordBox passwordBox)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
passwordBox.PasswordChanged -= HandlePasswordChanged;
|
||||||
|
|
||||||
|
if (!GetIsUpdating(passwordBox))
|
||||||
|
{
|
||||||
|
passwordBox.Password = e.NewValue?.ToString() ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
passwordBox.PasswordChanged += HandlePasswordChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void HandlePasswordChanged(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is not PasswordBox passwordBox)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
passwordBox.PasswordChanged -= HandlePasswordChanged;
|
||||||
|
|
||||||
|
SetIsUpdating(passwordBox, true);
|
||||||
|
SetBindablePassword(passwordBox, passwordBox.Password);
|
||||||
|
SetIsUpdating(passwordBox, false);
|
||||||
|
|
||||||
|
passwordBox.PasswordChanged += HandlePasswordChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<hc:GlowWindow x:Class="Client.LoginWindow"
|
||||||
|
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:hc="https://handyorg.github.io/handycontrol"
|
||||||
|
xmlns:conv="clr-namespace:Sharp.Wpf.Converters;assembly=Sharp.Wpf"
|
||||||
|
xmlns:h="clr-namespace:Client.Helpers"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
Title="Sharp 客户端登录"
|
||||||
|
WindowStartupLocation="CenterScreen"
|
||||||
|
Width="420"
|
||||||
|
Height="420"
|
||||||
|
ResizeMode="NoResize">
|
||||||
|
<hc:GlowWindow.Resources>
|
||||||
|
<conv:BoolToVisibilityConverter x:Key="BoolToVisibility" />
|
||||||
|
</hc:GlowWindow.Resources>
|
||||||
|
<Grid Background="{DynamicResource RegionBrush}">
|
||||||
|
<Border CornerRadius="16" Padding="32" Background="{DynamicResource PopupBrush}">
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="欢迎回来"
|
||||||
|
FontSize="24"
|
||||||
|
FontWeight="Bold"
|
||||||
|
Margin="0,0,0,4" />
|
||||||
|
<TextBlock Text="请输入管理员账号信息"
|
||||||
|
Foreground="{DynamicResource SecondaryTextBrush}" />
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Row="1" Margin="0,24,0,0">
|
||||||
|
<TextBlock Text="账号" FontWeight="SemiBold" />
|
||||||
|
<TextBox Margin="0,8,0,0"
|
||||||
|
Text="{Binding UserName, UpdateSourceTrigger=PropertyChanged}"
|
||||||
|
Height="36" />
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Row="2" Margin="0,12,0,0">
|
||||||
|
<TextBlock Text="密码" FontWeight="SemiBold" />
|
||||||
|
<PasswordBox Margin="0,8,0,0"
|
||||||
|
h:PasswordBoxHelper.BindablePassword="{Binding Password, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||||
|
Height="36" />
|
||||||
|
</StackPanel>
|
||||||
|
<TextBlock Grid.Row="3"
|
||||||
|
Margin="0,12,0,0"
|
||||||
|
Text="{Binding ErrorMessage}"
|
||||||
|
Foreground="Tomato"
|
||||||
|
Visibility="{Binding HasError, Converter={StaticResource BoolToVisibility}}" />
|
||||||
|
<Button Grid.Row="4"
|
||||||
|
Margin="0,24,0,0"
|
||||||
|
Height="40"
|
||||||
|
Content="登 录"
|
||||||
|
Command="{Binding LoginCommand}" />
|
||||||
|
<ProgressBar Grid.Row="5"
|
||||||
|
Height="4"
|
||||||
|
IsIndeterminate="True"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Top"
|
||||||
|
Margin="0,24,0,0"
|
||||||
|
Visibility="{Binding IsBusy, Converter={StaticResource BoolToVisibility}}" />
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</hc:GlowWindow>
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using System;
|
||||||
|
using HandyControl.Controls;
|
||||||
|
using Client.ViewModels;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Client;
|
||||||
|
|
||||||
|
public partial class LoginWindow : GlowWindow
|
||||||
|
{
|
||||||
|
private readonly LoginViewModel _viewModel;
|
||||||
|
private readonly ILogger<LoginWindow>? _logger;
|
||||||
|
|
||||||
|
public LoginWindow(LoginViewModel viewModel, ILogger<LoginWindow>? logger = null)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
_viewModel = viewModel;
|
||||||
|
_logger = logger;
|
||||||
|
DataContext = viewModel;
|
||||||
|
_viewModel.LoginSucceeded += HandleLoginSucceeded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleLoginSucceeded(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
_logger?.LogInformation("HandleLoginSucceeded called, setting DialogResult = true");
|
||||||
|
DialogResult = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnClosed(EventArgs e)
|
||||||
|
{
|
||||||
|
base.OnClosed(e);
|
||||||
|
_viewModel.LoginSucceeded -= HandleLoginSucceeded;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,39 +1,110 @@
|
|||||||
<Window x:Class="Client.MainWindow"
|
<hc:GlowWindow x:Class="Client.MainWindow"
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:local="clr-namespace:Client"
|
xmlns:hc="https://handyorg.github.io/handycontrol"
|
||||||
xmlns:b="clr-namespace:Sharp.Wpf.Behaviors;assembly=Sharp.Wpf"
|
|
||||||
xmlns:w="clr-namespace:Sharp.Wpf.Windowing;assembly=Sharp.Wpf"
|
xmlns:w="clr-namespace:Sharp.Wpf.Windowing;assembly=Sharp.Wpf"
|
||||||
xmlns:conv="clr-namespace:Sharp.Wpf.Converters;assembly=Sharp.Wpf"
|
xmlns:conv="clr-namespace:Sharp.Wpf.Converters;assembly=Sharp.Wpf"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
Title="MainWindow" Height="450" Width="800"
|
Title="Sharp 控制台"
|
||||||
|
Height="560"
|
||||||
|
Width="960"
|
||||||
|
WindowStartupLocation="CenterScreen"
|
||||||
w:WindowPlacement.RememberPlacement="True"
|
w:WindowPlacement.RememberPlacement="True"
|
||||||
w:WindowPlacement.PlacementId="MainWindow">
|
w:WindowPlacement.PlacementId="MainWindow">
|
||||||
<Window.Resources>
|
<hc:GlowWindow.Resources>
|
||||||
<conv:BoolToVisibilityConverter x:Key="Bool2Vis" />
|
<conv:BoolToVisibilityConverter x:Key="Bool2Vis" />
|
||||||
</Window.Resources>
|
</hc:GlowWindow.Resources>
|
||||||
<Grid Margin="16">
|
<Grid Background="{DynamicResource RegionBrush}">
|
||||||
<Grid.RowDefinitions>
|
<Grid Margin="24">
|
||||||
<RowDefinition Height="Auto"/>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="Auto"/>
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="Auto"/>
|
<RowDefinition Height="*" />
|
||||||
<RowDefinition Height="*"/>
|
</Grid.RowDefinitions>
|
||||||
</Grid.RowDefinitions>
|
|
||||||
<TextBlock Text="Sharp.Wpf 集成示例" FontSize="18" FontWeight="Bold"/>
|
|
||||||
<StackPanel Orientation="Horizontal" Grid.Row="1" Margin="0,8,0,0">
|
|
||||||
<Button Content="点击弹窗"
|
|
||||||
Margin="0,0,8,0"
|
|
||||||
b:EventToCommand.EventName="Click"
|
|
||||||
b:EventToCommand.Command="{Binding ShowDialogCommand}" />
|
|
||||||
<Button Content="触发异常"
|
|
||||||
b:EventToCommand.EventName="Click"
|
|
||||||
b:EventToCommand.Command="{Binding ThrowCommand}" />
|
|
||||||
</StackPanel>
|
|
||||||
<TextBlock Grid.Row="2" Text="正在处理..." Foreground="DarkOrange"
|
|
||||||
Visibility="{Binding IsBusy, Converter={StaticResource Bool2Vis}}"/>
|
|
||||||
<TextBlock Grid.Row="3" Text="{Binding Greeting}" VerticalAlignment="Top"/>
|
|
||||||
</Grid>
|
|
||||||
</Window>
|
|
||||||
|
|
||||||
|
<Border Background="{DynamicResource PopupBrush}" Padding="24" CornerRadius="16">
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="{Binding Greeting}" FontSize="26" FontWeight="Bold" />
|
||||||
|
<TextBlock Text="{Binding StatusMessage}" Margin="0,6,0,0" Foreground="{DynamicResource SecondaryTextBrush}" />
|
||||||
|
<TextBlock Text="最后同步: " Margin="0,12,0,0" Foreground="{DynamicResource SecondaryTextBrush}">
|
||||||
|
<Run Text="{Binding LastSyncTime, StringFormat=yyyy-MM-dd HH:mm:ss}" />
|
||||||
|
</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center">
|
||||||
|
<Button Content="刷新仪表盘"
|
||||||
|
Padding="18,6"
|
||||||
|
Margin="0,0,12,0"
|
||||||
|
Command="{Binding RefreshDashboardCommand}" />
|
||||||
|
<Button Content="演示弹窗"
|
||||||
|
Padding="18,6"
|
||||||
|
Margin="0,0,12,0"
|
||||||
|
Command="{Binding ShowDialogCommand}" />
|
||||||
|
<Button Content="触发异常"
|
||||||
|
Padding="18,6"
|
||||||
|
Command="{Binding ThrowCommand}" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Grid Grid.Row="1" Margin="0,24,0,0">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="2*" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<UniformGrid Columns="2" Margin="0,0,16,0">
|
||||||
|
<Border Background="{DynamicResource PopupBrush}" Margin="0,0,16,16" Padding="20" CornerRadius="12">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="登录用户" FontWeight="SemiBold" />
|
||||||
|
<TextBlock Text="{Binding UserDisplayName}" FontSize="20" Margin="0,8,0,0" />
|
||||||
|
<TextBlock Text="当前会话已建立"
|
||||||
|
Foreground="{DynamicResource SecondaryTextBrush}" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
<Border Background="{DynamicResource PopupBrush}" Margin="0,0,16,16" Padding="20" CornerRadius="12">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="系统健康度" FontWeight="SemiBold" />
|
||||||
|
<TextBlock Text="{Binding StatusMessage}" Margin="0,8,0,0" />
|
||||||
|
<ProgressBar Height="6" Margin="0,12,0,0"
|
||||||
|
IsIndeterminate="True"
|
||||||
|
Visibility="{Binding IsBusy, Converter={StaticResource Bool2Vis}}" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
<Border Background="{DynamicResource PopupBrush}" Margin="0,0,16,16" Padding="20" CornerRadius="12">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="上次操作" FontWeight="SemiBold" />
|
||||||
|
<TextBlock Text="{Binding Greeting}" Margin="0,8,0,0" TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
<Border Background="{DynamicResource PopupBrush}" Margin="0,0,16,16" Padding="20" CornerRadius="12">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="提示" FontWeight="SemiBold" />
|
||||||
|
<TextBlock Text="使用 HandyControl 让界面更柔和" Margin="0,8,0,0" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</UniformGrid>
|
||||||
|
|
||||||
|
<Border Grid.Column="1" Background="{DynamicResource PopupBrush}" Padding="24" CornerRadius="12">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="操作日志" FontSize="18" FontWeight="SemiBold" />
|
||||||
|
<TextBlock Text="演示命令" Foreground="{DynamicResource SecondaryTextBrush}" Margin="0,8,0,16" />
|
||||||
|
<ItemsControl>
|
||||||
|
<ItemsControl.Items>
|
||||||
|
<TextBlock Text="• 刷新仪表盘会模拟异步调用" Margin="0,4" />
|
||||||
|
<TextBlock Text="• 演示弹窗来自 Sharp.Wpf DialogService" Margin="0,4" />
|
||||||
|
<TextBlock Text="• 触发异常会被全局处理" Margin="0,4" />
|
||||||
|
<TextBlock Text="• 登录信息通过依赖注入管理" Margin="0,4" />
|
||||||
|
</ItemsControl.Items>
|
||||||
|
</ItemsControl>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</hc:GlowWindow>
|
||||||
@@ -1,18 +1,13 @@
|
|||||||
using System.Windows;
|
using HandyControl.Controls;
|
||||||
using Client.ViewModels;
|
using Client.ViewModels;
|
||||||
|
|
||||||
namespace Client
|
namespace Client;
|
||||||
|
|
||||||
|
public partial class MainWindow : GlowWindow
|
||||||
{
|
{
|
||||||
/// <summary>
|
public MainWindow(MainViewModel viewModel)
|
||||||
/// Interaction logic for MainWindow.xaml
|
|
||||||
/// </summary>
|
|
||||||
public partial class MainWindow : Window
|
|
||||||
{
|
{
|
||||||
public MainWindow()
|
InitializeComponent();
|
||||||
{
|
DataContext = viewModel;
|
||||||
InitializeComponent();
|
|
||||||
DataContext = new MainViewModel();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Client.Services;
|
||||||
|
|
||||||
|
public interface IAuthenticationService
|
||||||
|
{
|
||||||
|
Task<AuthenticationResult> LoginAsync(string userName, string password, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record AuthenticationResult(bool Succeeded, string? Error, string? DisplayName);
|
||||||
|
|
||||||
|
public sealed class DemoAuthenticationService : IAuthenticationService
|
||||||
|
{
|
||||||
|
public async Task<AuthenticationResult> LoginAsync(string userName, string password, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
await Task.Delay(600, cancellationToken);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(userName) || string.IsNullOrWhiteSpace(password))
|
||||||
|
{
|
||||||
|
return new AuthenticationResult(false, "请输入账号和密码", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.Equals(userName, "admin", StringComparison.OrdinalIgnoreCase) || password != "123456")
|
||||||
|
{
|
||||||
|
return new AuthenticationResult(false, "账号或密码不正确 (admin / 123456)", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new AuthenticationResult(true, null, "Sharp 管理员");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
namespace Client.Services;
|
||||||
|
|
||||||
|
public interface IUserSession
|
||||||
|
{
|
||||||
|
bool IsAuthenticated { get; }
|
||||||
|
string? UserName { get; }
|
||||||
|
string? DisplayName { get; }
|
||||||
|
void SignIn(string userName, string? displayName);
|
||||||
|
void SignOut();
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class UserSession : IUserSession
|
||||||
|
{
|
||||||
|
public bool IsAuthenticated { get; private set; }
|
||||||
|
public string? UserName { get; private set; }
|
||||||
|
public string? DisplayName { get; private set; }
|
||||||
|
|
||||||
|
public void SignIn(string userName, string? displayName)
|
||||||
|
{
|
||||||
|
IsAuthenticated = true;
|
||||||
|
UserName = userName;
|
||||||
|
DisplayName = string.IsNullOrWhiteSpace(displayName) ? userName : displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SignOut()
|
||||||
|
{
|
||||||
|
IsAuthenticated = false;
|
||||||
|
UserName = null;
|
||||||
|
DisplayName = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Client.Services;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Sharp.Wpf.Commands;
|
||||||
|
using Sharp.Wpf.Mvvm;
|
||||||
|
|
||||||
|
namespace Client.ViewModels;
|
||||||
|
|
||||||
|
public sealed class LoginViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
private readonly IAuthenticationService _authenticationService;
|
||||||
|
private readonly IUserSession _userSession;
|
||||||
|
private readonly ILogger<LoginViewModel> _logger;
|
||||||
|
private readonly IAsyncCommand _loginCommand;
|
||||||
|
|
||||||
|
private string _userName = "admin";
|
||||||
|
private string _password = string.Empty;
|
||||||
|
private string? _errorMessage;
|
||||||
|
|
||||||
|
public event EventHandler? LoginSucceeded;
|
||||||
|
|
||||||
|
public LoginViewModel(IAuthenticationService authenticationService, IUserSession userSession, ILogger<LoginViewModel> logger)
|
||||||
|
{
|
||||||
|
_authenticationService = authenticationService;
|
||||||
|
_userSession = userSession;
|
||||||
|
_logger = logger;
|
||||||
|
_loginCommand = new AsyncCommand(LoginAsync);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string UserName
|
||||||
|
{
|
||||||
|
get => _userName;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (SetProperty(ref _userName, value))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Password
|
||||||
|
{
|
||||||
|
get => _password;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (SetProperty(ref _password, value))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? ErrorMessage
|
||||||
|
{
|
||||||
|
get => _errorMessage;
|
||||||
|
private set
|
||||||
|
{
|
||||||
|
if (SetProperty(ref _errorMessage, value))
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(HasError));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasError => !string.IsNullOrWhiteSpace(ErrorMessage);
|
||||||
|
|
||||||
|
public IAsyncCommand LoginCommand => _loginCommand;
|
||||||
|
|
||||||
|
private async Task LoginAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Login attempt for user: {UserName}", UserName);
|
||||||
|
IsBusy = true;
|
||||||
|
ErrorMessage = null;
|
||||||
|
|
||||||
|
var result = await _authenticationService.LoginAsync(UserName, Password);
|
||||||
|
|
||||||
|
if (!result.Succeeded)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Login failed for user: {UserName}, Error: {Error}", UserName, result.Error);
|
||||||
|
ErrorMessage = result.Error;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Login succeeded for user: {UserName}", UserName);
|
||||||
|
_userSession.SignIn(UserName, result.DisplayName);
|
||||||
|
_logger.LogInformation("Invoking LoginSucceeded event");
|
||||||
|
LoginSucceeded?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Login failed for user: {UserName}", UserName);
|
||||||
|
ErrorMessage = "An unexpected error occurred.";
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
IsBusy = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
|
using Client.Services;
|
||||||
using Sharp.Wpf.Commands;
|
using Sharp.Wpf.Commands;
|
||||||
using Sharp.Wpf.Mvvm;
|
using Sharp.Wpf.Mvvm;
|
||||||
using Sharp.Wpf.Services;
|
using Sharp.Wpf.Services;
|
||||||
@@ -9,35 +10,77 @@ namespace Client.ViewModels;
|
|||||||
|
|
||||||
public sealed class MainViewModel : ViewModelBase
|
public sealed class MainViewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
private readonly IDialogService _dialog = new DialogService();
|
private readonly IDialogService _dialogService;
|
||||||
|
private readonly IUserSession _userSession;
|
||||||
|
|
||||||
|
private string _greeting = "欢迎使用 Sharp";
|
||||||
|
private string _statusMessage = "系统就绪";
|
||||||
|
private DateTime _lastSyncTime = DateTime.Now;
|
||||||
|
|
||||||
|
public MainViewModel(IDialogService dialogService, IUserSession userSession)
|
||||||
|
{
|
||||||
|
_dialogService = dialogService;
|
||||||
|
_userSession = userSession;
|
||||||
|
_greeting = $"欢迎回来,{UserDisplayName}";
|
||||||
|
_statusMessage = "准备就绪";
|
||||||
|
|
||||||
|
ShowDialogCommand = new AsyncCommand(ShowDialogAsync);
|
||||||
|
RefreshDashboardCommand = new AsyncCommand(RefreshDashboardAsync);
|
||||||
|
ThrowCommand = new RelayCommand(() => throw new InvalidOperationException("演示:未处理异常将被全局处理"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public string UserDisplayName => _userSession.DisplayName ?? _userSession.UserName ?? "访客";
|
||||||
|
|
||||||
private string _greeting = "欢迎使用 Sharp.Wpf";
|
|
||||||
public string Greeting
|
public string Greeting
|
||||||
{
|
{
|
||||||
get => _greeting;
|
get => _greeting;
|
||||||
set => SetProperty(ref _greeting, value);
|
set => SetProperty(ref _greeting, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string StatusMessage
|
||||||
|
{
|
||||||
|
get => _statusMessage;
|
||||||
|
set => SetProperty(ref _statusMessage, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DateTime LastSyncTime
|
||||||
|
{
|
||||||
|
get => _lastSyncTime;
|
||||||
|
set => SetProperty(ref _lastSyncTime, value);
|
||||||
|
}
|
||||||
|
|
||||||
public IAsyncCommand ShowDialogCommand { get; }
|
public IAsyncCommand ShowDialogCommand { get; }
|
||||||
|
public IAsyncCommand RefreshDashboardCommand { get; }
|
||||||
public ICommand ThrowCommand { get; }
|
public ICommand ThrowCommand { get; }
|
||||||
|
|
||||||
public MainViewModel()
|
private async Task ShowDialogAsync()
|
||||||
{
|
{
|
||||||
ShowDialogCommand = new AsyncCommand(async () =>
|
try
|
||||||
{
|
{
|
||||||
try
|
IsBusy = true;
|
||||||
{
|
await Task.Delay(300).ConfigureAwait(false);
|
||||||
IsBusy = true;
|
await _dialogService.ShowInfoAsync($"Hi {UserDisplayName}, 这是来自 Sharp.Wpf 的提醒!").ConfigureAwait(false);
|
||||||
await Task.Delay(300);
|
Greeting = $"问候更新时间: {DateTime.Now:HH:mm:ss}";
|
||||||
await _dialog.ShowInfoAsync("Hello from Sharp.Wpf DialogService!");
|
}
|
||||||
Greeting = $"时间: {DateTime.Now:HH:mm:ss}";
|
finally
|
||||||
}
|
{
|
||||||
finally
|
IsBusy = false;
|
||||||
{
|
}
|
||||||
IsBusy = false;
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ThrowCommand = new RelayCommand(() => throw new InvalidOperationException("演示:未处理异常将被全局处理"));
|
private async Task RefreshDashboardAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
IsBusy = true;
|
||||||
|
StatusMessage = "正在同步仪表盘数据...";
|
||||||
|
await Task.Delay(600).ConfigureAwait(false);
|
||||||
|
LastSyncTime = DateTime.Now;
|
||||||
|
StatusMessage = "同步完成";
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
IsBusy = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
#pragma checksum "..\..\..\App.xaml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "878B875A6A09A9568F92EC16A87C1A2A9A208AA0"
|
#pragma checksum "..\..\..\App.xaml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "A01D87CBC408FE1E9934F8938867D92F17E3A2CF"
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// <auto-generated>
|
// <auto-generated>
|
||||||
// This code was generated by a tool.
|
// This code was generated by a tool.
|
||||||
@@ -8,7 +8,6 @@
|
|||||||
// </auto-generated>
|
// </auto-generated>
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
using Client;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
@@ -41,15 +40,22 @@ namespace Client {
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class App : System.Windows.Application {
|
public partial class App : System.Windows.Application {
|
||||||
|
|
||||||
|
private bool _contentLoaded;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// InitializeComponent
|
/// InitializeComponent
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||||
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "9.0.4.0")]
|
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "9.0.4.0")]
|
||||||
public void InitializeComponent() {
|
public void InitializeComponent() {
|
||||||
|
if (_contentLoaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_contentLoaded = true;
|
||||||
|
System.Uri resourceLocater = new System.Uri("/Client;component/app.xaml", System.UriKind.Relative);
|
||||||
|
|
||||||
#line 5 "..\..\..\App.xaml"
|
#line 1 "..\..\..\App.xaml"
|
||||||
this.StartupUri = new System.Uri("MainWindow.xaml", System.UriKind.Relative);
|
System.Windows.Application.LoadComponent(this, resourceLocater);
|
||||||
|
|
||||||
#line default
|
#line default
|
||||||
#line hidden
|
#line hidden
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// <auto-generated>
|
||||||
|
// 此代码由工具生成。
|
||||||
|
// 运行时版本:4.0.30319.42000
|
||||||
|
//
|
||||||
|
// 对此文件的更改可能会导致不正确的行为,并且如果
|
||||||
|
// 重新生成代码,这些更改将会丢失。
|
||||||
|
// </auto-generated>
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
[assembly: System.Reflection.AssemblyCompanyAttribute("Client")]
|
||||||
|
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||||
|
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||||
|
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+f7660c49e81d5fe2747f6514545ceb961c2066d3")]
|
||||||
|
[assembly: System.Reflection.AssemblyProductAttribute("Client")]
|
||||||
|
[assembly: System.Reflection.AssemblyTitleAttribute("Client")]
|
||||||
|
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||||
|
[assembly: System.Runtime.Versioning.TargetPlatformAttribute("Windows7.0")]
|
||||||
|
[assembly: System.Runtime.Versioning.SupportedOSPlatformAttribute("Windows7.0")]
|
||||||
|
|
||||||
|
// 由 MSBuild WriteCodeFragment 类生成。
|
||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
eeea53cf7db257ee716efdd2fdcdb5f1a810b49927811d410a05e1ff19899369
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
is_global = true
|
||||||
|
build_property.TargetFramework = net8.0-windows
|
||||||
|
build_property.TargetPlatformMinVersion = 7.0
|
||||||
|
build_property.UsingMicrosoftNETSdkWeb =
|
||||||
|
build_property.ProjectTypeGuids =
|
||||||
|
build_property.InvariantGlobalization =
|
||||||
|
build_property.PlatformNeutralAssembly =
|
||||||
|
build_property.EnforceExtendedAnalyzerRules =
|
||||||
|
build_property._SupportedPlatformList = Linux,macOS,Windows
|
||||||
|
build_property.RootNamespace = Client
|
||||||
|
build_property.ProjectDir = D:\yuanll\sharp-basic\src\Sharp\Client\
|
||||||
|
build_property.EnableComHosting =
|
||||||
|
build_property.EnableGeneratedComInterfaceComImportInterop =
|
||||||
|
build_property.CsWinRTUseWindowsUIXamlProjections = false
|
||||||
|
build_property.EffectiveAnalysisLevelStyle = 8.0
|
||||||
|
build_property.EnableCodeStyleSeverity =
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
#pragma checksum "..\..\..\MainWindow.xaml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "BEEAE53C9D96DFAC0315F07E3682D21B80792BEB"
|
#pragma checksum "..\..\..\MainWindow.xaml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "9E813BA3492F961B4CE0E3DE4E78408D972934B6"
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// <auto-generated>
|
// <auto-generated>
|
||||||
// This code was generated by a tool.
|
// This code was generated by a tool.
|
||||||
@@ -8,8 +8,18 @@
|
|||||||
// </auto-generated>
|
// </auto-generated>
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
using Client;
|
using HandyControl.Controls;
|
||||||
using Sharp.Wpf.Behaviors;
|
using HandyControl.Data;
|
||||||
|
using HandyControl.Expression.Media;
|
||||||
|
using HandyControl.Expression.Shapes;
|
||||||
|
using HandyControl.Interactivity;
|
||||||
|
using HandyControl.Media.Animation;
|
||||||
|
using HandyControl.Media.Effects;
|
||||||
|
using HandyControl.Properties.Langs;
|
||||||
|
using HandyControl.Themes;
|
||||||
|
using HandyControl.Tools;
|
||||||
|
using HandyControl.Tools.Converter;
|
||||||
|
using HandyControl.Tools.Extension;
|
||||||
using Sharp.Wpf.Converters;
|
using Sharp.Wpf.Converters;
|
||||||
using Sharp.Wpf.Windowing;
|
using Sharp.Wpf.Windowing;
|
||||||
using System;
|
using System;
|
||||||
@@ -42,7 +52,7 @@ namespace Client {
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// MainWindow
|
/// MainWindow
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class MainWindow : System.Windows.Window, System.Windows.Markup.IComponentConnector {
|
public partial class MainWindow : HandyControl.Controls.GlowWindow, System.Windows.Markup.IComponentConnector {
|
||||||
|
|
||||||
private bool _contentLoaded;
|
private bool _contentLoaded;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,205 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace Config;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 描述默认配置构建时的约定。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ConfigDefaultsOptions
|
||||||
|
{
|
||||||
|
private readonly IList<ConfigSource> _fluentSources = new List<ConfigSource>();
|
||||||
|
|
||||||
|
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>
|
||||||
|
/// 通过流式 API 添加 JSON 配置文件(默认 optional)。
|
||||||
|
/// </summary>
|
||||||
|
public ConfigDefaultsOptions WithJson(string path, bool optional = true) =>
|
||||||
|
AddFluentSource(path, ConfigFormat.Json, optional);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 通过流式 API 添加 INI 配置文件(默认 optional)。
|
||||||
|
/// </summary>
|
||||||
|
public ConfigDefaultsOptions WithIni(string path, bool optional = true) =>
|
||||||
|
AddFluentSource(path, ConfigFormat.Ini, optional);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 通过流式 API 添加 XML 配置文件(默认 optional)。
|
||||||
|
/// </summary>
|
||||||
|
public ConfigDefaultsOptions WithXml(string path, bool optional = true) =>
|
||||||
|
AddFluentSource(path, ConfigFormat.Xml, optional);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 通过流式 API 添加 TOML 配置文件(默认 optional)。
|
||||||
|
/// </summary>
|
||||||
|
public ConfigDefaultsOptions WithToml(string path, bool optional = true) =>
|
||||||
|
AddFluentSource(path, ConfigFormat.Toml, optional);
|
||||||
|
|
||||||
|
private ConfigDefaultsOptions AddFluentSource(string path, ConfigFormat format, bool optional)
|
||||||
|
{
|
||||||
|
_fluentSources.Add(new ConfigSource
|
||||||
|
{
|
||||||
|
Path = path,
|
||||||
|
Format = format,
|
||||||
|
Optional = optional
|
||||||
|
});
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal IEnumerable<ConfigSource> GetFluentSources() => _fluentSources;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static T Load<T>(Action<ConfigDefaultsOptions> defaults,
|
||||||
|
bool validate = true,
|
||||||
|
string? section = null,
|
||||||
|
Action<ConfigOptions>? configure = null) where T : new()
|
||||||
|
{
|
||||||
|
return Load<T>(validate, section, defaults, configure);
|
||||||
|
}
|
||||||
|
|
||||||
|
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.GetFluentSources())
|
||||||
|
{
|
||||||
|
target.Sources.Add(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
|
||||||
namespace Config;
|
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
|
public static class ConfigServiceCollectionExtensions
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -17,5 +53,52 @@ public static class ConfigServiceCollectionExtensions
|
|||||||
});
|
});
|
||||||
return services;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
|
||||||
|
|
||||||
namespace Config;
|
|
||||||
|
|
||||||
public interface ISystemConfigAccessor
|
|
||||||
{
|
|
||||||
SystemConfig Value { get; }
|
|
||||||
}
|
|
||||||
|
|
||||||
internal sealed class SystemConfigAccessor : ISystemConfigAccessor, IDisposable
|
|
||||||
{
|
|
||||||
private readonly ConfigManager _cm;
|
|
||||||
private readonly IDisposable _subscription;
|
|
||||||
private SystemConfig _current;
|
|
||||||
|
|
||||||
public SystemConfigAccessor(ConfigManager cm)
|
|
||||||
{
|
|
||||||
_cm = cm;
|
|
||||||
_current = cm.Get<SystemConfig>(section: "System", validate: false);
|
|
||||||
_subscription = cm.OnReload(() =>
|
|
||||||
{
|
|
||||||
// Reload on change
|
|
||||||
_current = _cm.Get<SystemConfig>(section: "System", validate: false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public SystemConfig Value => _current;
|
|
||||||
|
|
||||||
public void Dispose() => _subscription.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class SystemConfigExtensions
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Registers SystemConfig accessor backed by ConfigManager under the "System" section.
|
|
||||||
/// </summary>
|
|
||||||
public static IServiceCollection AddSystemConfig(this IServiceCollection services, ConfigManager cm)
|
|
||||||
{
|
|
||||||
services.TryAddSingleton(cm);
|
|
||||||
services.TryAddSingleton<ISystemConfigAccessor, SystemConfigAccessor>();
|
|
||||||
return services;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -36,6 +36,31 @@
|
|||||||
|
|
||||||
## 使用方式
|
## 使用方式
|
||||||
|
|
||||||
|
### 0) 约定优于配置的快速接入
|
||||||
|
|
||||||
|
```
|
||||||
|
// 1 行代码拿到强类型配置,并做数据注解验证
|
||||||
|
var settings = ConfigDefaults.Load<AppSettings>();
|
||||||
|
|
||||||
|
// 或者先构建 ConfigManager,方便后续使用 IConfiguration
|
||||||
|
var manager = ConfigDefaults.BuildDefault(opt =>
|
||||||
|
{
|
||||||
|
opt.EnvironmentPrefix = "APP"; // 仅读取 APP__ 前缀的环境变量
|
||||||
|
opt.WithIni("extra.ini") // 同时加载 INI
|
||||||
|
.WithToml("feature.toml"); // 以及 TOML
|
||||||
|
});
|
||||||
|
var settings = manager.Get<AppSettings>(validate: true);
|
||||||
|
```
|
||||||
|
|
||||||
|
依赖注入中也可以一步完成“构建+绑定+热更新”:
|
||||||
|
|
||||||
|
```
|
||||||
|
services.AddDefaultConfigBinding<AppSettings>(defaults =>
|
||||||
|
{
|
||||||
|
defaults.EnvironmentName = "Production"; // 手动指定环境
|
||||||
|
}, section: "App", onReload: s => logger.LogInformation("配置已更新"));
|
||||||
|
```
|
||||||
|
|
||||||
### 1) 从文件构建(常见)
|
### 1) 从文件构建(常见)
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -183,4 +208,3 @@ var cm = provider.GetRequiredService<ConfigManager>();
|
|||||||
- 提供选项开关以切换 INI 映射策略(是否复制根键到各节)
|
- 提供选项开关以切换 INI 映射策略(是否复制根键到各节)
|
||||||
- 为 INI/TOML 增加自定义 `FileConfigurationProvider`,支持 `ReloadOnChange`
|
- 为 INI/TOML 增加自定义 `FileConfigurationProvider`,支持 `ReloadOnChange`
|
||||||
- 增强序列化器的集合/字典类型支持(当前对 INI 做了简化)
|
- 增强序列化器的集合/字典类型支持(当前对 INI 做了简化)
|
||||||
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
|
|
||||||
namespace Config;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Global system-level configuration, shared across modules.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class SystemConfig
|
|
||||||
{
|
|
||||||
[Required]
|
|
||||||
[RegularExpression(@"^([0-9]{1,3}\.){3}[0-9]{1,3}$|^localhost$|^[a-zA-Z0-9_.-]+$")]
|
|
||||||
public string HTTPIp { get; set; } = "127.0.0.1";
|
|
||||||
|
|
||||||
[Range(1, 65535)]
|
|
||||||
public int HttpPort { get; set; } = 8080;
|
|
||||||
|
|
||||||
[Range(1, 600)]
|
|
||||||
public int TimeoutSeconds { get; set; } = 30;
|
|
||||||
|
|
||||||
public string Scheme { get; set; } = "http";
|
|
||||||
}
|
|
||||||
|
|
||||||
+15
-10
@@ -5,17 +5,26 @@
|
|||||||
## 快速开始
|
## 快速开始
|
||||||
|
|
||||||
```
|
```
|
||||||
using Config;
|
// 直接按照约定加载 appsettings.json (+ 环境变体)
|
||||||
|
var settings = ConfigDefaults.Load<AppSettings>();
|
||||||
|
|
||||||
var cm = ConfigBuilder.BuildFromFiles(
|
// 如果需要继续访问 IConfiguration,可先拿到 ConfigManager
|
||||||
|
var cm = ConfigDefaults.BuildDefault(defaults =>
|
||||||
|
{
|
||||||
|
defaults.WithIni("extra.ini")
|
||||||
|
.WithToml("feature.toml");
|
||||||
|
defaults.EnvironmentPrefix = "APP";
|
||||||
|
});
|
||||||
|
var settings2 = cm.Get<AppSettings>(validate: true);
|
||||||
|
|
||||||
|
// 传统 BuildFromFiles 仍然可用
|
||||||
|
var cmManual = ConfigBuilder.BuildFromFiles(
|
||||||
new[] { "appsettings.json", "settings.ini", "extra.xml", "feature.toml" },
|
new[] { "appsettings.json", "settings.ini", "extra.xml", "feature.toml" },
|
||||||
reloadOnChange: true,
|
reloadOnChange: true,
|
||||||
includeEnvironment: true,
|
includeEnvironment: true,
|
||||||
environmentPrefix: "APP",
|
environmentPrefix: "APP",
|
||||||
basePath: AppContext.BaseDirectory
|
basePath: AppContext.BaseDirectory
|
||||||
);
|
);
|
||||||
|
|
||||||
var settings = cm.Get<AppSettings>(validate: true);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -95,13 +104,10 @@ using Microsoft.Extensions.DependencyInjection;
|
|||||||
using Config;
|
using Config;
|
||||||
|
|
||||||
var services = new ServiceCollection();
|
var services = new ServiceCollection();
|
||||||
services.AddConfig(opt =>
|
services.AddDefaultConfigBinding<AppSettings>(section: "App");
|
||||||
{
|
|
||||||
opt.BasePath = AppContext.BaseDirectory;
|
|
||||||
opt.Sources.Add(new ConfigSource { Path = "appsettings.json", Format = ConfigFormat.Json });
|
|
||||||
});
|
|
||||||
|
|
||||||
var sp = services.BuildServiceProvider();
|
var sp = services.BuildServiceProvider();
|
||||||
|
var settings = sp.GetRequiredService<AppSettings>();
|
||||||
var cm = sp.GetRequiredService<ConfigManager>();
|
var cm = sp.GetRequiredService<ConfigManager>();
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -111,4 +117,3 @@ var cm = sp.GetRequiredService<ConfigManager>();
|
|||||||
- INI 映射默认会将根键复制到一级节(若该节没定义同名键),有助于简化配置;如不符合你的期望,可在后续版本通过选项开关关闭
|
- INI 映射默认会将根键复制到一级节(若该节没定义同名键),有助于简化配置;如不符合你的期望,可在后续版本通过选项开关关闭
|
||||||
- TOML 暂不直接 `LoadFromFile<T>`,推荐通过配置管线 + 绑定
|
- TOML 暂不直接 `LoadFromFile<T>`,推荐通过配置管线 + 绑定
|
||||||
- JSON/XML 支持 `ReloadOnChange`;INI/TOML 默认不启用文件级热更新
|
- JSON/XML 支持 `ReloadOnChange`;INI/TOML 默认不启用文件级热更新
|
||||||
|
|
||||||
@@ -0,0 +1,503 @@
|
|||||||
|
using System.Data;
|
||||||
|
using System.Data.OleDb;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Sharp.Data.Access.Connections;
|
||||||
|
using Sharp.Data.Access.Initialization;
|
||||||
|
using Sharp.Data.Access.Internal;
|
||||||
|
using Sharp.Data.Access.Options;
|
||||||
|
using Sharp.Data.Access.Security;
|
||||||
|
|
||||||
|
namespace Sharp.Data.Access;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Access 数据库事务实现
|
||||||
|
/// </summary>
|
||||||
|
internal class AccessTransaction : IAccessTransaction
|
||||||
|
{
|
||||||
|
private readonly OleDbConnection _connection;
|
||||||
|
private readonly OleDbTransaction _transaction;
|
||||||
|
private readonly ILogger? _logger;
|
||||||
|
private readonly bool _ownsConnection;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public AccessTransaction(OleDbConnection connection, bool ownsConnection, ILogger? logger)
|
||||||
|
{
|
||||||
|
_connection = connection;
|
||||||
|
_ownsConnection = ownsConnection;
|
||||||
|
_transaction = connection.BeginTransaction();
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Execute(string sql, params OleDbParameter[] parameters)
|
||||||
|
{
|
||||||
|
using var command = new OleDbCommand(sql, _connection, _transaction);
|
||||||
|
if (parameters.Length > 0)
|
||||||
|
command.Parameters.AddRange(parameters);
|
||||||
|
|
||||||
|
return command.ExecuteNonQuery();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<T> Query<T>(string sql, params OleDbParameter[] parameters) where T : new()
|
||||||
|
{
|
||||||
|
using var command = new OleDbCommand(sql, _connection, _transaction);
|
||||||
|
if (parameters.Length > 0)
|
||||||
|
command.Parameters.AddRange(parameters);
|
||||||
|
|
||||||
|
var results = new List<T>();
|
||||||
|
using var reader = command.ExecuteReader();
|
||||||
|
|
||||||
|
while (reader.Read())
|
||||||
|
{
|
||||||
|
var item = MapReaderToObject<T>(reader);
|
||||||
|
results.Add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
public object? ExecuteScalar(string sql, params OleDbParameter[] parameters)
|
||||||
|
{
|
||||||
|
using var command = new OleDbCommand(sql, _connection, _transaction);
|
||||||
|
if (parameters.Length > 0)
|
||||||
|
command.Parameters.AddRange(parameters);
|
||||||
|
|
||||||
|
return command.ExecuteScalar();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Commit()
|
||||||
|
{
|
||||||
|
_transaction.Commit();
|
||||||
|
_logger?.LogDebug("事务已提交");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Rollback()
|
||||||
|
{
|
||||||
|
_transaction.Rollback();
|
||||||
|
_logger?.LogDebug("事务已回滚");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_transaction.Dispose();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_ownsConnection)
|
||||||
|
{
|
||||||
|
_connection.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static T MapReaderToObject<T>(IDataReader reader) where T : new()
|
||||||
|
{
|
||||||
|
var obj = new T();
|
||||||
|
var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
|
||||||
|
|
||||||
|
// 创建字段名到索引的映射(大小写不敏感)
|
||||||
|
var fieldMap = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
for (int i = 0; i < reader.FieldCount; i++)
|
||||||
|
{
|
||||||
|
var fieldName = reader.GetName(i);
|
||||||
|
fieldMap[fieldName] = i;
|
||||||
|
|
||||||
|
// Access 列名有时包含末尾空格,提前做一次修剪以便映射
|
||||||
|
var trimmed = fieldName.Trim();
|
||||||
|
if (!fieldMap.ContainsKey(trimmed))
|
||||||
|
{
|
||||||
|
fieldMap[trimmed] = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var property in properties)
|
||||||
|
{
|
||||||
|
if (!property.CanWrite) continue;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 尝试查找匹配的字段(大小写不敏感)
|
||||||
|
if (fieldMap.TryGetValue(property.Name, out int ordinal))
|
||||||
|
{
|
||||||
|
var value = reader.GetValue(ordinal);
|
||||||
|
|
||||||
|
if (value != DBNull.Value && value != null)
|
||||||
|
{
|
||||||
|
// 尝试类型转换
|
||||||
|
if (property.PropertyType == value.GetType())
|
||||||
|
{
|
||||||
|
property.SetValue(obj, value);
|
||||||
|
}
|
||||||
|
else if (property.PropertyType == typeof(string))
|
||||||
|
{
|
||||||
|
property.SetValue(obj, value.ToString());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 尝试使用 Convert.ChangeType
|
||||||
|
var convertedValue = Convert.ChangeType(value, property.PropertyType);
|
||||||
|
property.SetValue(obj, convertedValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// 字段不存在或类型转换失败,保持默认值
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Access 数据服务实现(基于 System.Data.OleDb)
|
||||||
|
/// </summary>
|
||||||
|
public class AccessDataService : IAccessDataService
|
||||||
|
{
|
||||||
|
private readonly AccessDatabaseOptions _options;
|
||||||
|
private readonly IAccessConnectionStringFactory _connectionFactory;
|
||||||
|
private readonly IAccessDatabaseInitializer _initializer;
|
||||||
|
private readonly IAccessPasswordDecryptor _decryptor;
|
||||||
|
private readonly ILogger<AccessDataService>? _logger;
|
||||||
|
private readonly string _connectionString;
|
||||||
|
|
||||||
|
public AccessDataService(
|
||||||
|
AccessDatabaseOptions options,
|
||||||
|
IAccessConnectionStringFactory? connectionFactory = null,
|
||||||
|
IAccessDatabaseInitializer? initializer = null,
|
||||||
|
IAccessPasswordDecryptor? decryptor = null,
|
||||||
|
ILogger<AccessDataService>? logger = null)
|
||||||
|
{
|
||||||
|
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||||
|
_options.Validate();
|
||||||
|
|
||||||
|
_connectionFactory = connectionFactory ?? new AccessConnectionStringFactory(decryptor ?? new PassthroughAccessPasswordDecryptor());
|
||||||
|
_initializer = initializer ?? new AccessDatabaseInitializer();
|
||||||
|
_decryptor = decryptor ?? new PassthroughAccessPasswordDecryptor();
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 确保数据库存在(如果配置了模板复制)
|
||||||
|
_initializer.EnsureDatabase(_options, NullServiceProvider.Instance);
|
||||||
|
|
||||||
|
// 构建连接字符串
|
||||||
|
_connectionString = _connectionFactory.Create(_options, NullServiceProvider.Instance);
|
||||||
|
|
||||||
|
_logger?.LogInformation("AccessDataService 已初始化,数据库: {DatabaseFile}, Provider: {Provider}",
|
||||||
|
_options.DatabaseFile, _options.ProviderName);
|
||||||
|
_logger?.LogDebug("实际连接字符串: {ConnectionString}", _connectionString);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger?.LogError(ex, "AccessDataService 初始化失败");
|
||||||
|
|
||||||
|
// 提供详细的错误诊断信息
|
||||||
|
if (ex.Message.Contains("找不到") || ex is FileNotFoundException)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"找不到 Access 数据库文件: {_options.DatabaseFile}。" +
|
||||||
|
$"请确保文件存在或配置 TemplateFile 以自动创建。", ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<T> Query<T>(string sql, params OleDbParameter[] parameters) where T : new()
|
||||||
|
{
|
||||||
|
_logger?.LogDebug("执行查询: {Sql}", sql);
|
||||||
|
|
||||||
|
return ExecuteWithRetry(() =>
|
||||||
|
{
|
||||||
|
using var connection = CreateConnection();
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
using var command = new OleDbCommand(sql, connection);
|
||||||
|
if (parameters.Length > 0)
|
||||||
|
{
|
||||||
|
// 克隆参数以避免重试时的"已包含在另一个集合中"错误
|
||||||
|
var clonedParams = parameters.Select(p => CloneParameter(p)).ToArray();
|
||||||
|
command.Parameters.AddRange(clonedParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
var results = new List<T>();
|
||||||
|
using var reader = command.ExecuteReader();
|
||||||
|
|
||||||
|
while (reader.Read())
|
||||||
|
{
|
||||||
|
var item = MapReaderToObject<T>(reader);
|
||||||
|
results.Add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger?.LogDebug("查询返回 {Count} 条记录", results.Count);
|
||||||
|
return results;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public T? QueryFirstOrDefault<T>(string sql, params OleDbParameter[] parameters) where T : new()
|
||||||
|
{
|
||||||
|
_logger?.LogDebug("执行单条查询: {Sql}", sql);
|
||||||
|
|
||||||
|
return ExecuteWithRetry(() =>
|
||||||
|
{
|
||||||
|
using var connection = CreateConnection();
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
using var command = new OleDbCommand(sql, connection);
|
||||||
|
if (parameters.Length > 0)
|
||||||
|
{
|
||||||
|
// 克隆参数以避免重试时的"已包含在另一个集合中"错误
|
||||||
|
var clonedParams = parameters.Select(p => CloneParameter(p)).ToArray();
|
||||||
|
command.Parameters.AddRange(clonedParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
using var reader = command.ExecuteReader();
|
||||||
|
|
||||||
|
if (reader.Read())
|
||||||
|
{
|
||||||
|
return MapReaderToObject<T>(reader);
|
||||||
|
}
|
||||||
|
|
||||||
|
return default;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public object? ExecuteScalar(string sql, params OleDbParameter[] parameters)
|
||||||
|
{
|
||||||
|
_logger?.LogDebug("执行标量查询: {Sql}", sql);
|
||||||
|
|
||||||
|
return ExecuteWithRetry(() =>
|
||||||
|
{
|
||||||
|
using var connection = CreateConnection();
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
using var command = new OleDbCommand(sql, connection);
|
||||||
|
if (parameters.Length > 0)
|
||||||
|
{
|
||||||
|
// 克隆参数以避免重试时的"已包含在另一个集合中"错误
|
||||||
|
var clonedParams = parameters.Select(p => CloneParameter(p)).ToArray();
|
||||||
|
command.Parameters.AddRange(clonedParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = command.ExecuteScalar();
|
||||||
|
_logger?.LogDebug("标量查询返回: {Result}", result);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Execute(string sql, params OleDbParameter[] parameters)
|
||||||
|
{
|
||||||
|
_logger?.LogDebug("执行命令: {Sql}", sql);
|
||||||
|
|
||||||
|
return ExecuteWithRetry(() =>
|
||||||
|
{
|
||||||
|
using var connection = CreateConnection();
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
using var command = new OleDbCommand(sql, connection);
|
||||||
|
if (parameters.Length > 0)
|
||||||
|
{
|
||||||
|
// 克隆参数以避免重试时的"已包含在另一个集合中"错误
|
||||||
|
var clonedParams = parameters.Select(p => CloneParameter(p)).ToArray();
|
||||||
|
command.Parameters.AddRange(clonedParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
var rowsAffected = command.ExecuteNonQuery();
|
||||||
|
_logger?.LogDebug("命令影响了 {RowsAffected} 行", rowsAffected);
|
||||||
|
return rowsAffected;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public IAccessTransaction BeginTransaction()
|
||||||
|
{
|
||||||
|
_logger?.LogDebug("开始事务");
|
||||||
|
var connection = CreateConnection();
|
||||||
|
connection.Open();
|
||||||
|
return new AccessTransaction(connection, ownsConnection: true, _logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ExecuteInTransaction(Action<IAccessTransaction> action)
|
||||||
|
{
|
||||||
|
_logger?.LogDebug("在事务中执行操作");
|
||||||
|
|
||||||
|
using var transaction = BeginTransaction();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
action(transaction);
|
||||||
|
transaction.Commit();
|
||||||
|
_logger?.LogDebug("事务执行成功");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger?.LogError(ex, "事务执行失败,正在回滚");
|
||||||
|
transaction.Rollback();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TestConnection()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger?.LogDebug("测试数据库连接");
|
||||||
|
using var connection = CreateConnection();
|
||||||
|
connection.Open();
|
||||||
|
_logger?.LogDebug("数据库连接成功");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger?.LogError(ex, "数据库连接失败");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private OleDbConnection CreateConnection()
|
||||||
|
{
|
||||||
|
return new OleDbConnection(_connectionString);
|
||||||
|
}
|
||||||
|
|
||||||
|
private T ExecuteWithRetry<T>(Func<T> operation)
|
||||||
|
{
|
||||||
|
var maxRetries = _options.MaxRetryCount;
|
||||||
|
var retryDelay = _options.RetryDelayMilliseconds;
|
||||||
|
|
||||||
|
for (int attempt = 0; attempt <= maxRetries; attempt++)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return operation();
|
||||||
|
}
|
||||||
|
catch (OleDbException ex) when (attempt < maxRetries && IsRetriableError(ex))
|
||||||
|
{
|
||||||
|
_logger?.LogWarning(ex, "数据库操作失败,正在重试 ({Attempt}/{MaxRetries})...", attempt + 1, maxRetries);
|
||||||
|
Thread.Sleep(retryDelay);
|
||||||
|
}
|
||||||
|
catch (OleDbException ex)
|
||||||
|
{
|
||||||
|
// 提供详细的错误诊断信息
|
||||||
|
var errorMessage = $"Access 数据库操作失败: {ex.Message}";
|
||||||
|
|
||||||
|
// 检查是否是 Provider 相关的错误
|
||||||
|
if (ex.Message.Contains("找不到") || ex.Message.Contains("未注册") ||
|
||||||
|
ex.Message.Contains("OLE DB") || ex.Message.Contains("多步"))
|
||||||
|
{
|
||||||
|
errorMessage += $"\n\n可能的原因:" +
|
||||||
|
$"\n1. 未安装 Access Database Engine:当前使用 Provider = {_options.ProviderName}" +
|
||||||
|
$"\n - 如果使用 16.0,需要安装 Access Database Engine 2016 或 Office 2016+" +
|
||||||
|
$"\n - 如果使用 12.0,需要安装 Access Database Engine 2010 或 Office 2010+" +
|
||||||
|
$"\n - 建议:优先使用 12.0 版本,兼容性最好" +
|
||||||
|
$"\n2. 32位/64位不匹配:进程架构必须与 Access Driver 架构一致" +
|
||||||
|
$"\n3. 数据库文件损坏或被其他程序锁定" +
|
||||||
|
$"\n4. 数据库文件路径不正确:{_options.DatabaseFile}";
|
||||||
|
|
||||||
|
_logger?.LogError(ex, "数据库连接失败,Provider: {Provider}", _options.ProviderName);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException(errorMessage, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 不会到这里,因为最后一次尝试会抛出异常
|
||||||
|
throw new InvalidOperationException("重试逻辑错误");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsRetriableError(OleDbException ex)
|
||||||
|
{
|
||||||
|
// Access 数据库常见的可重试错误
|
||||||
|
// -2147467259: 数据库被锁定
|
||||||
|
// -2147217865: 无法打开数据库(可能正在被其他进程使用)
|
||||||
|
return ex.ErrorCode == -2147467259 || ex.ErrorCode == -2147217865;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 克隆 OleDbParameter 以避免重试时参数被重复添加的问题
|
||||||
|
/// </summary>
|
||||||
|
private static OleDbParameter CloneParameter(OleDbParameter original)
|
||||||
|
{
|
||||||
|
var clone = new OleDbParameter
|
||||||
|
{
|
||||||
|
ParameterName = original.ParameterName,
|
||||||
|
Value = original.Value,
|
||||||
|
OleDbType = original.OleDbType,
|
||||||
|
Size = original.Size,
|
||||||
|
Direction = original.Direction,
|
||||||
|
IsNullable = original.IsNullable,
|
||||||
|
Precision = original.Precision,
|
||||||
|
Scale = original.Scale,
|
||||||
|
SourceColumn = original.SourceColumn,
|
||||||
|
SourceVersion = original.SourceVersion
|
||||||
|
};
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static T MapReaderToObject<T>(IDataReader reader) where T : new()
|
||||||
|
{
|
||||||
|
var obj = new T();
|
||||||
|
var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
|
||||||
|
|
||||||
|
// 创建字段名到索引的映射(大小写不敏感)
|
||||||
|
var fieldMap = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
for (int i = 0; i < reader.FieldCount; i++)
|
||||||
|
{
|
||||||
|
var fieldName = reader.GetName(i);
|
||||||
|
fieldMap[fieldName] = i;
|
||||||
|
|
||||||
|
// Access 列名有时包含末尾空格,提前做一次修剪以便映射
|
||||||
|
var trimmed = fieldName.Trim();
|
||||||
|
if (!fieldMap.ContainsKey(trimmed))
|
||||||
|
{
|
||||||
|
fieldMap[trimmed] = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var property in properties)
|
||||||
|
{
|
||||||
|
if (!property.CanWrite) continue;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 尝试查找匹配的字段(大小写不敏感)
|
||||||
|
if (fieldMap.TryGetValue(property.Name, out int ordinal))
|
||||||
|
{
|
||||||
|
var value = reader.GetValue(ordinal);
|
||||||
|
|
||||||
|
if (value != DBNull.Value && value != null)
|
||||||
|
{
|
||||||
|
// 尝试类型转换
|
||||||
|
if (property.PropertyType == value.GetType())
|
||||||
|
{
|
||||||
|
property.SetValue(obj, value);
|
||||||
|
}
|
||||||
|
else if (property.PropertyType == typeof(string))
|
||||||
|
{
|
||||||
|
property.SetValue(obj, value.ToString());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 尝试使用 Convert.ChangeType
|
||||||
|
var convertedValue = Convert.ChangeType(value, property.PropertyType);
|
||||||
|
property.SetValue(obj, convertedValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// 字段不存在或类型转换失败,保持默认值
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
using Sharp.Data.Access.Options;
|
||||||
|
using Sharp.Data.Access.Security;
|
||||||
|
using Sharp.Data.Access.Connections;
|
||||||
|
using Sharp.Data.Access.Initialization;
|
||||||
|
|
||||||
|
namespace Sharp.Data.Access;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Access 数据服务的依赖注入扩展
|
||||||
|
/// </summary>
|
||||||
|
public static class AccessDataServiceExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 添加 Access 数据服务(最简单方式 - 仅指定数据库路径)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="services">服务集合</param>
|
||||||
|
/// <param name="databasePath">数据库文件路径(绝对或相对路径)</param>
|
||||||
|
/// <param name="password">数据库密码(可选)</param>
|
||||||
|
/// <param name="provider">OLE DB 提供程序,默认:Microsoft.ACE.OLEDB.16.0</param>
|
||||||
|
/// <returns>服务集合</returns>
|
||||||
|
public static IServiceCollection AddAccessDataService(
|
||||||
|
this IServiceCollection services,
|
||||||
|
string databasePath,
|
||||||
|
string? password = null,
|
||||||
|
string? provider = null)
|
||||||
|
{
|
||||||
|
return services.AddAccessDataService(options =>
|
||||||
|
{
|
||||||
|
options.DatabaseFile = databasePath;
|
||||||
|
if (!string.IsNullOrEmpty(password))
|
||||||
|
options.Password = password;
|
||||||
|
if (!string.IsNullOrEmpty(provider))
|
||||||
|
options.ProviderName = provider;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 添加 Access 数据服务(使用配置委托)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="services">服务集合</param>
|
||||||
|
/// <param name="configureOptions">配置委托</param>
|
||||||
|
/// <returns>服务集合</returns>
|
||||||
|
public static IServiceCollection AddAccessDataService(
|
||||||
|
this IServiceCollection services,
|
||||||
|
Action<AccessDatabaseOptions> configureOptions)
|
||||||
|
{
|
||||||
|
if (services == null) throw new ArgumentNullException(nameof(services));
|
||||||
|
if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions));
|
||||||
|
|
||||||
|
var options = new AccessDatabaseOptions();
|
||||||
|
configureOptions(options);
|
||||||
|
options.Validate();
|
||||||
|
|
||||||
|
// 注册核心服务
|
||||||
|
services.TryAddSingleton<IAccessPasswordDecryptor, PassthroughAccessPasswordDecryptor>();
|
||||||
|
services.TryAddSingleton<IAccessConnectionStringFactory>(sp =>
|
||||||
|
new AccessConnectionStringFactory(sp.GetRequiredService<IAccessPasswordDecryptor>()));
|
||||||
|
services.TryAddSingleton<IAccessDatabaseInitializer, AccessDatabaseInitializer>();
|
||||||
|
|
||||||
|
// 注册配置
|
||||||
|
services.AddSingleton(options);
|
||||||
|
|
||||||
|
// 注册 AccessDataService
|
||||||
|
services.AddSingleton<IAccessDataService>(sp =>
|
||||||
|
new AccessDataService(
|
||||||
|
options,
|
||||||
|
sp.GetRequiredService<IAccessConnectionStringFactory>(),
|
||||||
|
sp.GetRequiredService<IAccessDatabaseInitializer>(),
|
||||||
|
sp.GetRequiredService<IAccessPasswordDecryptor>(),
|
||||||
|
sp.GetService<Microsoft.Extensions.Logging.ILogger<AccessDataService>>()));
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 添加 Access 数据服务(使用配置对象)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="services">服务集合</param>
|
||||||
|
/// <param name="options">配置选项</param>
|
||||||
|
/// <returns>服务集合</returns>
|
||||||
|
public static IServiceCollection AddAccessDataService(
|
||||||
|
this IServiceCollection services,
|
||||||
|
AccessDatabaseOptions options)
|
||||||
|
{
|
||||||
|
if (options == null) throw new ArgumentNullException(nameof(options));
|
||||||
|
|
||||||
|
return services.AddAccessDataService(_ => { });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 注册自定义密码解密器
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TDecryptor">解密器类型</typeparam>
|
||||||
|
/// <param name="services">服务集合</param>
|
||||||
|
/// <returns>服务集合</returns>
|
||||||
|
public static IServiceCollection AddAccessPasswordDecryptor<TDecryptor>(this IServiceCollection services)
|
||||||
|
where TDecryptor : class, IAccessPasswordDecryptor
|
||||||
|
{
|
||||||
|
services.AddSingleton<IAccessPasswordDecryptor, TDecryptor>();
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 注册自定义密码解密器(使用工厂方法)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="services">服务集合</param>
|
||||||
|
/// <param name="implementationFactory">解密器工厂</param>
|
||||||
|
/// <returns>服务集合</returns>
|
||||||
|
public static IServiceCollection AddAccessPasswordDecryptor(
|
||||||
|
this IServiceCollection services,
|
||||||
|
Func<IServiceProvider, IAccessPasswordDecryptor> implementationFactory)
|
||||||
|
{
|
||||||
|
services.AddSingleton(implementationFactory);
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
using System;
|
||||||
|
using System.Data.OleDb;
|
||||||
|
using Sharp.Data.Access.Internal;
|
||||||
|
using Sharp.Data.Access.Options;
|
||||||
|
using Sharp.Data.Access.Security;
|
||||||
|
|
||||||
|
namespace Sharp.Data.Access.Connections;
|
||||||
|
|
||||||
|
internal sealed class AccessConnectionStringFactory : IAccessConnectionStringFactory
|
||||||
|
{
|
||||||
|
private readonly IAccessPasswordDecryptor _decryptor;
|
||||||
|
|
||||||
|
public AccessConnectionStringFactory(IAccessPasswordDecryptor decryptor)
|
||||||
|
{
|
||||||
|
_decryptor = decryptor ?? throw new ArgumentNullException(nameof(decryptor));
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Create(AccessDatabaseOptions options, IServiceProvider services)
|
||||||
|
{
|
||||||
|
var configuration = AccessDatabaseConfiguration.FromOptions(options, services);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(configuration.ConnectionString))
|
||||||
|
{
|
||||||
|
return configuration.ConnectionString;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(configuration.DatabaseFile))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Access 数据库需要提供 DatabaseFile 或 ConnectionString。");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 基础连接字符串
|
||||||
|
// 注意:Connect Timeout 参数在某些 ACE 版本中不稳定,已移除
|
||||||
|
var builder = new OleDbConnectionStringBuilder
|
||||||
|
{
|
||||||
|
["Provider"] = configuration.ProviderName,
|
||||||
|
["Data Source"] = AccessPathResolver.Resolve(configuration.DatabaseFile, configuration.DataDirectory)
|
||||||
|
};
|
||||||
|
|
||||||
|
var password = configuration.Password;
|
||||||
|
if (string.IsNullOrEmpty(password) && !string.IsNullOrWhiteSpace(configuration.EncryptedPassword))
|
||||||
|
{
|
||||||
|
password = _decryptor.Decrypt(configuration.EncryptedPassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(password))
|
||||||
|
{
|
||||||
|
builder["Jet OLEDB:Database Password"] = password;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var parameter in configuration.AdditionalParameters)
|
||||||
|
{
|
||||||
|
builder[parameter.Key] = parameter.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.ConnectionString;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using System;
|
||||||
|
using Sharp.Data.Access.Options;
|
||||||
|
|
||||||
|
namespace Sharp.Data.Access.Connections;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构建 Access 连接字符串的工厂。
|
||||||
|
/// </summary>
|
||||||
|
public interface IAccessConnectionStringFactory
|
||||||
|
{
|
||||||
|
string Create(AccessDatabaseOptions options, IServiceProvider services);
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<RootNamespace>Sharp.Data.Access</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="EntityFrameworkCore.Jet" Version="8.0.0-preview1" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
|
||||||
|
<PackageReference Include="System.Data.OleDb" Version="8.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Data\Data.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
using Sharp.Data.Access.Connections;
|
||||||
|
using Sharp.Data.Access.Initialization;
|
||||||
|
using Sharp.Data.Access.Internal;
|
||||||
|
using Sharp.Data.Access.Options;
|
||||||
|
using Sharp.Data.Access.Security;
|
||||||
|
using Sharp.Data.Context;
|
||||||
|
|
||||||
|
namespace Sharp.Data.Access.DependencyInjection;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Access 模块的依赖注入扩展。
|
||||||
|
/// </summary>
|
||||||
|
public static class AccessServiceCollectionExtensions
|
||||||
|
{
|
||||||
|
public static IServiceCollection AddAccessDataContext<TContext>(this IServiceCollection services,
|
||||||
|
Action<AccessContextOptions<TContext>> configure,
|
||||||
|
bool registerDataContext = true,
|
||||||
|
ServiceLifetime dataContextLifetime = ServiceLifetime.Scoped)
|
||||||
|
where TContext : DbContext
|
||||||
|
{
|
||||||
|
if (configure == null) throw new ArgumentNullException(nameof(configure));
|
||||||
|
|
||||||
|
return services.AddAccessDataContext(_ =>
|
||||||
|
{
|
||||||
|
var options = new AccessContextOptions<TContext>();
|
||||||
|
configure(options);
|
||||||
|
return options;
|
||||||
|
}, registerDataContext, dataContextLifetime);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IServiceCollection AddAccessDataContext<TContext>(this IServiceCollection services,
|
||||||
|
Func<IServiceProvider, AccessContextOptions<TContext>> configure,
|
||||||
|
bool registerDataContext = true,
|
||||||
|
ServiceLifetime dataContextLifetime = ServiceLifetime.Scoped)
|
||||||
|
where TContext : DbContext
|
||||||
|
{
|
||||||
|
if (services == null) throw new ArgumentNullException(nameof(services));
|
||||||
|
if (configure == null) throw new ArgumentNullException(nameof(configure));
|
||||||
|
|
||||||
|
services.TryAddSingleton<IAccessPasswordDecryptor, PassthroughAccessPasswordDecryptor>();
|
||||||
|
services.TryAddSingleton<IAccessConnectionStringFactory>(sp =>
|
||||||
|
new AccessConnectionStringFactory(sp.GetRequiredService<IAccessPasswordDecryptor>()));
|
||||||
|
services.TryAddSingleton<IAccessDatabaseInitializer, AccessDatabaseInitializer>();
|
||||||
|
|
||||||
|
services.TryAddSingleton(provider =>
|
||||||
|
{
|
||||||
|
var options = configure(provider) ?? throw new InvalidOperationException("AccessContextOptions 不能为空。");
|
||||||
|
options.Validate();
|
||||||
|
return options;
|
||||||
|
});
|
||||||
|
|
||||||
|
services.AddScoped<IAccessDataContextFactory<TContext>>(sp =>
|
||||||
|
new AccessDataContextFactory<TContext>(
|
||||||
|
sp,
|
||||||
|
sp.GetRequiredService<AccessContextOptions<TContext>>(),
|
||||||
|
sp.GetRequiredService<IAccessConnectionStringFactory>(),
|
||||||
|
sp.GetRequiredService<IAccessDatabaseInitializer>()));
|
||||||
|
|
||||||
|
if (registerDataContext)
|
||||||
|
{
|
||||||
|
services.Add(new ServiceDescriptor(typeof(IDataContext),
|
||||||
|
sp => sp.GetRequiredService<IAccessDataContextFactory<TContext>>().Create(),
|
||||||
|
dataContextLifetime));
|
||||||
|
}
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
using System;
|
||||||
|
using EntityFrameworkCore.Jet;
|
||||||
|
using EntityFrameworkCore.Jet.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Sharp.Data.Access.Connections;
|
||||||
|
using Sharp.Data.Access.Initialization;
|
||||||
|
using Sharp.Data.Access.Internal;
|
||||||
|
using Sharp.Data.Access.Options;
|
||||||
|
using Sharp.Data.Access.Security;
|
||||||
|
using Sharp.Data;
|
||||||
|
|
||||||
|
namespace Sharp.Data.Access.Extensions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 手动使用 DataContextBuilder 时的 Access Helper。
|
||||||
|
/// </summary>
|
||||||
|
public static class AccessDataContextBuilderExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 通过现成的连接字符串配置 Access。
|
||||||
|
/// </summary>
|
||||||
|
public static DataContextBuilder<TContext> UseAccessDatabase<TContext>(
|
||||||
|
this DataContextBuilder<TContext> builder,
|
||||||
|
string connectionString,
|
||||||
|
Action<JetDbContextOptionsBuilder>? configure = null)
|
||||||
|
where TContext : DbContext
|
||||||
|
{
|
||||||
|
if (builder == null) throw new ArgumentNullException(nameof(builder));
|
||||||
|
if (string.IsNullOrWhiteSpace(connectionString))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("连接字符串不能为空。", nameof(connectionString));
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.ConfigureDbContext(options => options.UseJet(connectionString, configure));
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 通过 AccessDatabaseOptions 配置 Access。
|
||||||
|
/// </summary>
|
||||||
|
public static DataContextBuilder<TContext> UseAccessDatabase<TContext>(
|
||||||
|
this DataContextBuilder<TContext> builder,
|
||||||
|
Action<AccessDatabaseOptions> configureOptions,
|
||||||
|
IServiceProvider? services = null,
|
||||||
|
IAccessPasswordDecryptor? decryptor = null,
|
||||||
|
Action<JetDbContextOptionsBuilder>? configure = null)
|
||||||
|
where TContext : DbContext
|
||||||
|
{
|
||||||
|
if (builder == null) throw new ArgumentNullException(nameof(builder));
|
||||||
|
if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions));
|
||||||
|
|
||||||
|
var options = new AccessDatabaseOptions();
|
||||||
|
configureOptions(options);
|
||||||
|
options.Validate();
|
||||||
|
|
||||||
|
var serviceProvider = services ?? NullServiceProvider.Instance;
|
||||||
|
var initializer = new AccessDatabaseInitializer();
|
||||||
|
var connectionFactory = new AccessConnectionStringFactory(decryptor ?? new PassthroughAccessPasswordDecryptor());
|
||||||
|
|
||||||
|
initializer.EnsureDatabase(options, serviceProvider);
|
||||||
|
var connectionString = connectionFactory.Create(options, serviceProvider);
|
||||||
|
builder.ConfigureDbContext(o => o.UseJet(connectionString, configure));
|
||||||
|
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Sharp.Data.Context;
|
||||||
|
|
||||||
|
namespace Sharp.Data.Access;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Access 数据上下文工厂,避免直接依赖具体实现。
|
||||||
|
/// </summary>
|
||||||
|
public interface IAccessDataContextFactory<TContext> where TContext : DbContext
|
||||||
|
{
|
||||||
|
IDataContext Create();
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
using System.Data.OleDb;
|
||||||
|
|
||||||
|
namespace Sharp.Data.Access;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Access 数据库事务接口
|
||||||
|
/// </summary>
|
||||||
|
public interface IAccessTransaction : IDisposable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 执行非查询命令(INSERT, UPDATE, DELETE)
|
||||||
|
/// </summary>
|
||||||
|
int Execute(string sql, params OleDbParameter[] parameters);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询多条记录
|
||||||
|
/// </summary>
|
||||||
|
List<T> Query<T>(string sql, params OleDbParameter[] parameters) where T : new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行标量查询(COUNT, SUM等)
|
||||||
|
/// </summary>
|
||||||
|
object? ExecuteScalar(string sql, params OleDbParameter[] parameters);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 提交事务
|
||||||
|
/// </summary>
|
||||||
|
void Commit();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 回滚事务
|
||||||
|
/// </summary>
|
||||||
|
void Rollback();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Access 数据服务接口(基于 System.Data.OleDb)
|
||||||
|
/// </summary>
|
||||||
|
public interface IAccessDataService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 查询多条记录
|
||||||
|
/// </summary>
|
||||||
|
List<T> Query<T>(string sql, params OleDbParameter[] parameters) where T : new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询单条记录
|
||||||
|
/// </summary>
|
||||||
|
T? QueryFirstOrDefault<T>(string sql, params OleDbParameter[] parameters) where T : new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行标量查询(COUNT, SUM等)
|
||||||
|
/// </summary>
|
||||||
|
object? ExecuteScalar(string sql, params OleDbParameter[] parameters);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行非查询命令(INSERT, UPDATE, DELETE)
|
||||||
|
/// </summary>
|
||||||
|
int Execute(string sql, params OleDbParameter[] parameters);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 开始事务
|
||||||
|
/// </summary>
|
||||||
|
IAccessTransaction BeginTransaction();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 在事务中执行操作(自动提交/回滚)
|
||||||
|
/// </summary>
|
||||||
|
void ExecuteInTransaction(Action<IAccessTransaction> action);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试数据库连接
|
||||||
|
/// </summary>
|
||||||
|
bool TestConnection();
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using Sharp.Data.Access.Internal;
|
||||||
|
using Sharp.Data.Access.Options;
|
||||||
|
|
||||||
|
namespace Sharp.Data.Access.Initialization;
|
||||||
|
|
||||||
|
internal sealed class AccessDatabaseInitializer : IAccessDatabaseInitializer
|
||||||
|
{
|
||||||
|
public void EnsureDatabase(AccessDatabaseOptions options, IServiceProvider services)
|
||||||
|
{
|
||||||
|
var configuration = AccessDatabaseConfiguration.FromOptions(options, services);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(configuration.ConnectionString) ||
|
||||||
|
string.IsNullOrWhiteSpace(configuration.DatabaseFile))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var databasePath = AccessPathResolver.Resolve(configuration.DatabaseFile, configuration.DataDirectory);
|
||||||
|
var directory = Path.GetDirectoryName(databasePath);
|
||||||
|
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(directory);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (File.Exists(databasePath))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configuration.CopyTemplateWhenMissing && !string.IsNullOrWhiteSpace(configuration.TemplateFile))
|
||||||
|
{
|
||||||
|
var templatePath = AccessPathResolver.Resolve(configuration.TemplateFile, configuration.DataDirectory);
|
||||||
|
if (!File.Exists(templatePath))
|
||||||
|
{
|
||||||
|
throw new FileNotFoundException($"Access 模板文件 \"{templatePath}\" 不存在。", templatePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
File.Copy(templatePath, databasePath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configuration.ThrowIfMissing)
|
||||||
|
{
|
||||||
|
throw new FileNotFoundException($"Access 数据库 \"{databasePath}\" 不存在。", databasePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using System;
|
||||||
|
using Sharp.Data.Access.Options;
|
||||||
|
|
||||||
|
namespace Sharp.Data.Access.Initialization;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Access 数据库初始化器(确保目录/数据库文件准备就绪)。
|
||||||
|
/// </summary>
|
||||||
|
public interface IAccessDatabaseInitializer
|
||||||
|
{
|
||||||
|
void EnsureDatabase(AccessDatabaseOptions options, IServiceProvider services);
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
using System;
|
||||||
|
using EntityFrameworkCore.Jet;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Sharp.Data;
|
||||||
|
using Sharp.Data.Access.Connections;
|
||||||
|
using Sharp.Data.Access.Initialization;
|
||||||
|
using Sharp.Data.Access.Options;
|
||||||
|
using Sharp.Data.Context;
|
||||||
|
|
||||||
|
namespace Sharp.Data.Access.Internal;
|
||||||
|
|
||||||
|
internal sealed class AccessDataContextFactory<TContext> : IAccessDataContextFactory<TContext>
|
||||||
|
where TContext : DbContext
|
||||||
|
{
|
||||||
|
private readonly IServiceProvider _services;
|
||||||
|
private readonly AccessContextOptions<TContext> _options;
|
||||||
|
private readonly IAccessConnectionStringFactory _connectionFactory;
|
||||||
|
private readonly IAccessDatabaseInitializer _initializer;
|
||||||
|
|
||||||
|
public AccessDataContextFactory(
|
||||||
|
IServiceProvider services,
|
||||||
|
AccessContextOptions<TContext> options,
|
||||||
|
IAccessConnectionStringFactory connectionFactory,
|
||||||
|
IAccessDatabaseInitializer initializer)
|
||||||
|
{
|
||||||
|
_services = services;
|
||||||
|
_options = options;
|
||||||
|
_connectionFactory = connectionFactory;
|
||||||
|
_initializer = initializer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IDataContext Create()
|
||||||
|
{
|
||||||
|
var builder = new DataContextBuilder<TContext>();
|
||||||
|
|
||||||
|
_initializer.EnsureDatabase(_options.Database, _services);
|
||||||
|
|
||||||
|
var connectionString = _connectionFactory.Create(_options.Database, _services);
|
||||||
|
builder.ConfigureDbContext(options =>
|
||||||
|
{
|
||||||
|
options.UseJet(connectionString, jet => _options.ConfigureJet?.Invoke(jet));
|
||||||
|
});
|
||||||
|
|
||||||
|
_options.ConfigureDataContext?.Invoke(_services, builder);
|
||||||
|
|
||||||
|
return builder.Build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Sharp.Data.Access.Options;
|
||||||
|
|
||||||
|
namespace Sharp.Data.Access.Internal;
|
||||||
|
|
||||||
|
internal sealed record AccessDatabaseConfiguration(
|
||||||
|
string? ConnectionString,
|
||||||
|
string? DatabaseFile,
|
||||||
|
string ProviderName,
|
||||||
|
string? Password,
|
||||||
|
string? EncryptedPassword,
|
||||||
|
string? TemplateFile,
|
||||||
|
string? DataDirectory,
|
||||||
|
int ConnectionTimeout,
|
||||||
|
int MaxRetryCount,
|
||||||
|
int RetryDelayMilliseconds,
|
||||||
|
IReadOnlyDictionary<string, string> AdditionalParameters,
|
||||||
|
bool CopyTemplateWhenMissing,
|
||||||
|
bool ThrowIfMissing)
|
||||||
|
{
|
||||||
|
public static AccessDatabaseConfiguration FromOptions(AccessDatabaseOptions options, IServiceProvider services)
|
||||||
|
{
|
||||||
|
if (options == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(options));
|
||||||
|
}
|
||||||
|
|
||||||
|
var connectionString = Resolve(options.ConnectionString, options.ConnectionStringFactory, services);
|
||||||
|
var databaseFile = Resolve(options.DatabaseFile, options.DatabaseFileFactory, services);
|
||||||
|
var templateFile = Resolve(options.TemplateFile, options.TemplateFileFactory, services);
|
||||||
|
var dataDirectory = Resolve(options.DataDirectory, options.DataDirectoryFactory, services);
|
||||||
|
var password = Resolve(options.Password, options.PasswordFactory, services);
|
||||||
|
var encryptedPassword = Resolve(options.EncryptedPassword, options.EncryptedPasswordFactory, services);
|
||||||
|
|
||||||
|
return new AccessDatabaseConfiguration(
|
||||||
|
connectionString,
|
||||||
|
databaseFile,
|
||||||
|
string.IsNullOrWhiteSpace(options.ProviderName) ? AccessProviderNames.AceOleDb16 : options.ProviderName,
|
||||||
|
password,
|
||||||
|
encryptedPassword,
|
||||||
|
templateFile,
|
||||||
|
dataDirectory,
|
||||||
|
options.ConnectionTimeout,
|
||||||
|
options.MaxRetryCount,
|
||||||
|
options.RetryDelayMilliseconds,
|
||||||
|
new Dictionary<string, string>(options.AdditionalParameters, StringComparer.OrdinalIgnoreCase),
|
||||||
|
options.CopyTemplateWhenMissing,
|
||||||
|
options.ThrowIfMissing);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? Resolve(string? value, Func<IServiceProvider, string?>? factory, IServiceProvider services)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return factory?.Invoke(services);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace Sharp.Data.Access.Internal;
|
||||||
|
|
||||||
|
internal static class AccessPathResolver
|
||||||
|
{
|
||||||
|
public static string Resolve(string path, string? baseDirectory)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(path))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Access 数据库路径不能为空。");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Path.IsPathFullyQualified(path))
|
||||||
|
{
|
||||||
|
return Path.GetFullPath(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
var root = string.IsNullOrWhiteSpace(baseDirectory)
|
||||||
|
? AppContext.BaseDirectory
|
||||||
|
: baseDirectory!;
|
||||||
|
|
||||||
|
return Path.GetFullPath(Path.Combine(root, path));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Sharp.Data.Access.Internal;
|
||||||
|
|
||||||
|
internal sealed class NullServiceProvider : IServiceProvider
|
||||||
|
{
|
||||||
|
public static readonly NullServiceProvider Instance = new();
|
||||||
|
|
||||||
|
private NullServiceProvider()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public object? GetService(Type serviceType) => null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using System;
|
||||||
|
using EntityFrameworkCore.Jet.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Sharp.Data;
|
||||||
|
|
||||||
|
namespace Sharp.Data.Access.Options;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Access 数据上下文注册配置。
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TContext">EF Core DbContext 类型。</typeparam>
|
||||||
|
public sealed class AccessContextOptions<TContext> where TContext : DbContext
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Access 数据库配置(连接方式、密码等)。
|
||||||
|
/// </summary>
|
||||||
|
public AccessDatabaseOptions Database { get; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 允许调用方在 Build 之前继续配置 DataContextBuilder(例如启用审计、软删除、当前用户等)。
|
||||||
|
/// </summary>
|
||||||
|
public Action<IServiceProvider, DataContextBuilder<TContext>>? ConfigureDataContext { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 细化 Jet Provider 选项。
|
||||||
|
/// </summary>
|
||||||
|
public Action<JetDbContextOptionsBuilder>? ConfigureJet { get; set; }
|
||||||
|
|
||||||
|
internal void Validate() => Database.Validate();
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Sharp.Data.Access.Options;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Access 数据库统一配置项,同时支持 OleDb 和 EF Core 两种方式。
|
||||||
|
/// </summary>
|
||||||
|
public class AccessDatabaseOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 完整的连接字符串,可选。如果提供则优先使用,不再重新构建。
|
||||||
|
/// </summary>
|
||||||
|
public string? ConnectionString { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 通过依赖注入动态生成连接字符串。
|
||||||
|
/// </summary>
|
||||||
|
public Func<IServiceProvider, string?>? ConnectionStringFactory { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Access 数据库文件路径(相对或绝对)。
|
||||||
|
/// </summary>
|
||||||
|
public string? DatabaseFile { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 通过依赖注入动态解析数据库文件路径。
|
||||||
|
/// </summary>
|
||||||
|
public Func<IServiceProvider, string?>? DatabaseFileFactory { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 数据文件的基础目录(如果数据库路径为相对路径则会拼接)。
|
||||||
|
/// 默认使用 <see cref="AppContext.BaseDirectory" />。
|
||||||
|
/// </summary>
|
||||||
|
public string? DataDirectory { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 通过依赖注入动态解析数据目录。
|
||||||
|
/// </summary>
|
||||||
|
public Func<IServiceProvider, string?>? DataDirectoryFactory { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 明文密码,可选。用于访问加密的 Access 数据库。
|
||||||
|
/// </summary>
|
||||||
|
public string? Password { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 通过依赖注入动态解析明文密码。
|
||||||
|
/// </summary>
|
||||||
|
public Func<IServiceProvider, string?>? PasswordFactory { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 加密后的密码,可选。将通过 <see cref="Security.IAccessPasswordDecryptor" /> 解密。
|
||||||
|
/// </summary>
|
||||||
|
public string? EncryptedPassword { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 通过依赖注入动态解析加密密码。
|
||||||
|
/// </summary>
|
||||||
|
public Func<IServiceProvider, string?>? EncryptedPasswordFactory { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 数据库模板(*.mdb / *.accdb),在文件不存在时复制一份。
|
||||||
|
/// </summary>
|
||||||
|
public string? TemplateFile { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 通过依赖注入动态解析模板文件路径。
|
||||||
|
/// </summary>
|
||||||
|
public Func<IServiceProvider, string?>? TemplateFileFactory { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 当数据库文件缺失时是否尝试从模板复制。
|
||||||
|
/// </summary>
|
||||||
|
public bool CopyTemplateWhenMissing { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 当数据库文件缺失(且无法复制)时是否抛出异常。
|
||||||
|
/// </summary>
|
||||||
|
public bool ThrowIfMissing { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provider 名称,默认为 ACE OLE DB 16.0(功能最完整)。
|
||||||
|
/// 可选值:
|
||||||
|
/// - AccessProviderNames.AceOleDb16 (Microsoft.ACE.OLEDB.16.0) - 推荐,需要 Office 2016+
|
||||||
|
/// - AccessProviderNames.AceOleDb12 (Microsoft.ACE.OLEDB.12.0) - 需要 Office 2010+,不支持 Connect Timeout
|
||||||
|
/// - AccessProviderNames.JetOleDb4 (Microsoft.Jet.OLEDB.4.0) - 仅支持 32 位和旧版 Access
|
||||||
|
/// </summary>
|
||||||
|
public string ProviderName { get; set; } = AccessProviderNames.AceOleDb16;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 连接超时时间(秒),默认 15 秒。
|
||||||
|
/// </summary>
|
||||||
|
public int ConnectionTimeout { get; set; } = 15;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 最大重试次数,默认 3 次。用于增强稳健性。
|
||||||
|
/// </summary>
|
||||||
|
public int MaxRetryCount { get; set; } = 3;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 重试延迟(毫秒),默认 100 毫秒。
|
||||||
|
/// </summary>
|
||||||
|
public int RetryDelayMilliseconds { get; set; } = 100;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 额外的 OLE DB 参数,按需添加。
|
||||||
|
/// </summary>
|
||||||
|
public IDictionary<string, string> AdditionalParameters { get; } =
|
||||||
|
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
internal void Validate()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(ConnectionString) &&
|
||||||
|
ConnectionStringFactory is null &&
|
||||||
|
string.IsNullOrWhiteSpace(DatabaseFile) &&
|
||||||
|
DatabaseFileFactory is null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"AccessDatabaseOptions 需要连接字符串或数据库文件路径。");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(ProviderName))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("必须指定 Access Provider。");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ConnectionTimeout <= 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("ConnectionTimeout 必须大于 0。");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MaxRetryCount < 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("MaxRetryCount 不能小于 0。");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (RetryDelayMilliseconds < 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("RetryDelayMilliseconds 不能小于 0。");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
namespace Sharp.Data.Access.Options;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 常用 Access 数据库 Provider 名称。
|
||||||
|
/// </summary>
|
||||||
|
public static class AccessProviderNames
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Microsoft Access Database Engine 2016 (推荐)。
|
||||||
|
/// </summary>
|
||||||
|
public const string AceOleDb16 = "Microsoft.ACE.OLEDB.16.0";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Microsoft Access Database Engine 2010。
|
||||||
|
/// </summary>
|
||||||
|
public const string AceOleDb12 = "Microsoft.ACE.OLEDB.12.0";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 老旧的 Jet 4.0 Provider(仅限 32 位进程)。
|
||||||
|
/// </summary>
|
||||||
|
public const string JetOleDb4 = "Microsoft.Jet.OLEDB.4.0";
|
||||||
|
}
|
||||||
@@ -0,0 +1,374 @@
|
|||||||
|
# AccessDataService 简化 API 使用指南
|
||||||
|
|
||||||
|
## 🎯 设计目标
|
||||||
|
|
||||||
|
Data.Access 模块的核心目标是**让调用者使用起来更简单**,将连接管理、SQL 执行、参数处理、事务管理等复杂度封装起来。
|
||||||
|
|
||||||
|
## 📊 代码对比
|
||||||
|
|
||||||
|
### ❌ 之前:直接使用 OleDb(复杂)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 每次都要手动创建连接
|
||||||
|
private OleDbConnection CreateConnection()
|
||||||
|
{
|
||||||
|
var dbPath = Path.GetFullPath("dbDwDmsSetup.accdb");
|
||||||
|
var connectionString = $"Provider=Microsoft.ACE.OLEDB.12.0;Data Source={dbPath}";
|
||||||
|
return new OleDbConnection(connectionString);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询用户 - 需要写大量样板代码
|
||||||
|
private async Task<List<UserInfoDto>> LoadUsersAsync()
|
||||||
|
{
|
||||||
|
using var connection = CreateConnection();
|
||||||
|
await connection.OpenAsync();
|
||||||
|
|
||||||
|
using var command = new OleDbCommand("SELECT UserName, Password FROM UserInfo", connection);
|
||||||
|
using var reader = await Task.Run(() => command.ExecuteReader());
|
||||||
|
|
||||||
|
var users = new List<UserInfoDto>();
|
||||||
|
while (await Task.Run(() => reader.Read()))
|
||||||
|
{
|
||||||
|
var user = new UserInfoDto
|
||||||
|
{
|
||||||
|
UserName = reader["UserName"]?.ToString() ?? "",
|
||||||
|
Password = reader["Password"]?.ToString() ?? ""
|
||||||
|
};
|
||||||
|
users.Add(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
return users;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除用户 - 需要手动管理事务
|
||||||
|
private async Task DeleteUserAsync(string userName)
|
||||||
|
{
|
||||||
|
using var connection = CreateConnection();
|
||||||
|
await connection.OpenAsync();
|
||||||
|
using var transaction = connection.BeginTransaction();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (var deletePermCmd = new OleDbCommand(
|
||||||
|
"DELETE FROM UserPermission WHERE UserInfo_UserName = ?", connection, transaction))
|
||||||
|
{
|
||||||
|
deletePermCmd.Parameters.AddWithValue("@UserName", userName);
|
||||||
|
await Task.Run(() => deletePermCmd.ExecuteNonQuery());
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var deleteUserCmd = new OleDbCommand(
|
||||||
|
"DELETE FROM UserInfo WHERE UserName = ?", connection, transaction))
|
||||||
|
{
|
||||||
|
deleteUserCmd.Parameters.AddWithValue("@UserName", userName);
|
||||||
|
await Task.Run(() => deleteUserCmd.ExecuteNonQuery());
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction.Commit();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
transaction.Rollback();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**问题**:
|
||||||
|
- 大量重复的连接管理代码
|
||||||
|
- 手动映射查询结果到对象
|
||||||
|
- 手动管理事务的提交和回滚
|
||||||
|
- 代码冗长,不易维护
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ✅ 现在:使用 AccessDataService(简洁)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 注入服务,无需手动创建连接
|
||||||
|
private readonly IAccessDataService _accessDataService;
|
||||||
|
|
||||||
|
// 查询用户 - 一行代码!自动映射!
|
||||||
|
private async Task<List<UserInfoDto>> LoadUsersAsync()
|
||||||
|
{
|
||||||
|
return await _accessDataService.QueryAsync<UserInfoDto>(
|
||||||
|
"SELECT UserName, [Password] FROM UserInfo");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除用户 - 自动管理事务!
|
||||||
|
private async Task DeleteUserAsync(string userName)
|
||||||
|
{
|
||||||
|
await _accessDataService.ExecuteInTransactionAsync(async transaction =>
|
||||||
|
{
|
||||||
|
await transaction.ExecuteAsync(
|
||||||
|
"DELETE FROM UserPermission WHERE UserInfo_UserName = ?",
|
||||||
|
new OleDbParameter("@UserName", userName));
|
||||||
|
|
||||||
|
await transaction.ExecuteAsync(
|
||||||
|
"DELETE FROM UserInfo WHERE UserName = ?",
|
||||||
|
new OleDbParameter("@UserName", userName));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**优势**:
|
||||||
|
- ✅ 无需手动管理连接
|
||||||
|
- ✅ 自动泛型映射查询结果
|
||||||
|
- ✅ 自动管理事务(提交/回滚)
|
||||||
|
- ✅ 代码简洁,易于维护
|
||||||
|
- ✅ 从 60+ 行减少到 10 行左右
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 快速开始
|
||||||
|
|
||||||
|
### 1. 初始化服务
|
||||||
|
|
||||||
|
在 `App.xaml.cs` 中初始化:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private void InitializeAccessDataService()
|
||||||
|
{
|
||||||
|
var databasePath = Path.GetFullPath("dbDwDmsSetup.accdb");
|
||||||
|
var options = new AccessDataServiceOptions
|
||||||
|
{
|
||||||
|
DatabasePath = databasePath
|
||||||
|
};
|
||||||
|
|
||||||
|
AccessDataService = new AccessDataService(options, LoggerFactory.CreateLogger<AccessDataService>());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 在窗口中使用
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public partial class AccessDatabaseWindowOleDb : Window
|
||||||
|
{
|
||||||
|
private readonly IAccessDataService _accessDataService;
|
||||||
|
|
||||||
|
public AccessDatabaseWindowOleDb()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
|
||||||
|
var app = (App)Application.Current;
|
||||||
|
_accessDataService = app.AccessDataService;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 API 使用示例
|
||||||
|
|
||||||
|
### 1. 查询多条记录
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 自动映射到对象
|
||||||
|
var users = await _accessDataService.QueryAsync<UserInfoDto>(
|
||||||
|
"SELECT UserName, [Password] FROM UserInfo");
|
||||||
|
|
||||||
|
// 带参数的查询
|
||||||
|
var users = await _accessDataService.QueryAsync<UserInfoDto>(
|
||||||
|
"SELECT UserName, [Password] FROM UserInfo WHERE UserName LIKE ?",
|
||||||
|
new OleDbParameter("@Pattern", "%admin%"));
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 查询单条记录
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var user = await _accessDataService.QueryFirstOrDefaultAsync<UserInfoDto>(
|
||||||
|
"SELECT UserName, [Password] FROM UserInfo WHERE UserName = ?",
|
||||||
|
new OleDbParameter("@UserName", "admin"));
|
||||||
|
|
||||||
|
if (user != null)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"找到用户: {user.UserName}");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 执行标量查询
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 查询数量
|
||||||
|
var count = await _accessDataService.ExecuteScalarAsync(
|
||||||
|
"SELECT COUNT(*) FROM UserInfo WHERE UserName = ?",
|
||||||
|
new OleDbParameter("@UserName", "admin"));
|
||||||
|
|
||||||
|
int userCount = Convert.ToInt32(count);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 插入数据
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var rowsAffected = await _accessDataService.ExecuteAsync(
|
||||||
|
"INSERT INTO UserInfo (UserName, [Password]) VALUES (?, ?)",
|
||||||
|
new OleDbParameter("@UserName", "newuser"),
|
||||||
|
new OleDbParameter("@Password", "password123"));
|
||||||
|
|
||||||
|
Console.WriteLine($"插入了 {rowsAffected} 行");
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 更新数据
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var rowsAffected = await _accessDataService.ExecuteAsync(
|
||||||
|
"UPDATE UserInfo SET [Password] = ? WHERE UserName = ?",
|
||||||
|
new OleDbParameter("@Password", "newpassword"),
|
||||||
|
new OleDbParameter("@UserName", "admin"));
|
||||||
|
|
||||||
|
Console.WriteLine($"更新了 {rowsAffected} 行");
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. 删除数据
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var rowsAffected = await _accessDataService.ExecuteAsync(
|
||||||
|
"DELETE FROM UserInfo WHERE UserName = ?",
|
||||||
|
new OleDbParameter("@UserName", "testuser"));
|
||||||
|
|
||||||
|
Console.WriteLine($"删除了 {rowsAffected} 行");
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. 使用事务(自动提交/回滚)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 推荐:使用 ExecuteInTransactionAsync(自动管理)
|
||||||
|
await _accessDataService.ExecuteInTransactionAsync(async transaction =>
|
||||||
|
{
|
||||||
|
// 删除权限
|
||||||
|
await transaction.ExecuteAsync(
|
||||||
|
"DELETE FROM UserPermission WHERE UserInfo_UserName = ?",
|
||||||
|
new OleDbParameter("@UserName", userName));
|
||||||
|
|
||||||
|
// 删除用户
|
||||||
|
await transaction.ExecuteAsync(
|
||||||
|
"DELETE FROM UserInfo WHERE UserName = ?",
|
||||||
|
new OleDbParameter("@UserName", userName));
|
||||||
|
|
||||||
|
// 自动提交!如果有异常,自动回滚!
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8. 手动管理事务
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 如果需要手动控制
|
||||||
|
using var transaction = _accessDataService.BeginTransaction();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await transaction.ExecuteAsync("INSERT INTO ...", ...);
|
||||||
|
await transaction.ExecuteAsync("UPDATE ...", ...);
|
||||||
|
|
||||||
|
transaction.Commit(); // 手动提交
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
transaction.Rollback(); // 手动回滚
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9. 测试连接
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var success = await _accessDataService.TestConnectionAsync();
|
||||||
|
if (success)
|
||||||
|
{
|
||||||
|
Console.WriteLine("数据库连接成功!");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 完整示例
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class UserService
|
||||||
|
{
|
||||||
|
private readonly IAccessDataService _accessDataService;
|
||||||
|
|
||||||
|
public UserService(IAccessDataService accessDataService)
|
||||||
|
{
|
||||||
|
_accessDataService = accessDataService;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取所有用户
|
||||||
|
public async Task<List<UserInfoDto>> GetAllUsersAsync()
|
||||||
|
{
|
||||||
|
return await _accessDataService.QueryAsync<UserInfoDto>(
|
||||||
|
"SELECT UserName, [Password] FROM UserInfo");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户权限数量
|
||||||
|
public async Task<int> GetUserPermissionCountAsync(string userName)
|
||||||
|
{
|
||||||
|
var count = await _accessDataService.ExecuteScalarAsync(
|
||||||
|
"SELECT COUNT(*) FROM UserPermission WHERE UserInfo_UserName = ?",
|
||||||
|
new OleDbParameter("@UserName", userName));
|
||||||
|
|
||||||
|
return Convert.ToInt32(count);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加用户
|
||||||
|
public async Task<bool> AddUserAsync(string userName, string password)
|
||||||
|
{
|
||||||
|
// 检查是否已存在
|
||||||
|
var exists = await _accessDataService.ExecuteScalarAsync(
|
||||||
|
"SELECT COUNT(*) FROM UserInfo WHERE UserName = ?",
|
||||||
|
new OleDbParameter("@UserName", userName));
|
||||||
|
|
||||||
|
if (Convert.ToInt32(exists) > 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// 插入用户
|
||||||
|
await _accessDataService.ExecuteAsync(
|
||||||
|
"INSERT INTO UserInfo (UserName, [Password]) VALUES (?, ?)",
|
||||||
|
new OleDbParameter("@UserName", userName),
|
||||||
|
new OleDbParameter("@Password", password));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除用户(包含权限)
|
||||||
|
public async Task DeleteUserAsync(string userName)
|
||||||
|
{
|
||||||
|
await _accessDataService.ExecuteInTransactionAsync(async transaction =>
|
||||||
|
{
|
||||||
|
// 先删除权限
|
||||||
|
await transaction.ExecuteAsync(
|
||||||
|
"DELETE FROM UserPermission WHERE UserInfo_UserName = ?",
|
||||||
|
new OleDbParameter("@UserName", userName));
|
||||||
|
|
||||||
|
// 再删除用户
|
||||||
|
await transaction.ExecuteAsync(
|
||||||
|
"DELETE FROM UserInfo WHERE UserName = ?",
|
||||||
|
new OleDbParameter("@UserName", userName));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 核心优势总结
|
||||||
|
|
||||||
|
| 特性 | 直接使用 OleDb | 使用 AccessDataService |
|
||||||
|
|------|---------------|----------------------|
|
||||||
|
| 连接管理 | 手动创建、打开、释放 | ✅ 自动管理 |
|
||||||
|
| 结果映射 | 手动读取 DataReader | ✅ 自动泛型映射 |
|
||||||
|
| 事务管理 | 手动 BeginTransaction/Commit/Rollback | ✅ 自动管理 |
|
||||||
|
| 代码量 | 60+ 行 | ✅ 10 行左右 |
|
||||||
|
| 可维护性 | 低(大量重复代码) | ✅ 高 |
|
||||||
|
| 易用性 | 低(需要理解 ADO.NET) | ✅ 高 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📖 参考文件
|
||||||
|
|
||||||
|
- **核心实现**: `Data.Access/AccessDataService.cs`
|
||||||
|
- **接口定义**: `Data.Access/IAccessDataService.cs`
|
||||||
|
- **配置选项**: `Data.Access/AccessDataServiceOptions.cs`
|
||||||
|
- **使用示例**: `Server/Views/AccessDatabaseWindowOleDb.xaml.cs`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**总结**:Data.Access 模块通过封装 OleDb 的复杂度,提供了简洁优雅的 API,让调用者可以用更少的代码完成相同的功能!
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
# Sharp.Data.Access
|
||||||
|
|
||||||
|
基于 EF Core + Access (Jet) 的数据库基础设施。遵循 “依赖大于配置” 的理念,提供易于集成的连接串构建、密码解密、数据库初始化以及依赖注入支持,让其他项目只专注于提供配置即可。
|
||||||
|
|
||||||
|
> **定位**:仅提供 Access 数据层能力(连接、仓储、上下文构建),不含任何业务逻辑。
|
||||||
|
|
||||||
|
## 特性
|
||||||
|
|
||||||
|
- ✅ **DI 集成** – 一行 `AddAccessDataContext<TContext>()` 注册,其他模块通过构造函数获取 `IDataContext`。
|
||||||
|
- ✅ **最小配置** – 只需要提供数据库文件或连接字符串即可完成集成。
|
||||||
|
- ✅ **密码解密** – 内置 `IAccessPasswordDecryptor`,可注入自定义实现来解密加密密码。
|
||||||
|
- ✅ **数据库初始化** – 自动创建目录、可选基于模板复制空库文件,避免运行期错误。
|
||||||
|
- ✅ **灵活扩展** – 支持通过委托访问 `IServiceProvider`,从任意配置中心或密钥管理系统读取参数。
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Sharp.Data.Access.DependencyInjection;
|
||||||
|
using Sharp.Data.Context;
|
||||||
|
|
||||||
|
builder.Services.AddAccessDataContext<AppDbContext>(options =>
|
||||||
|
{
|
||||||
|
options.Database.DatabaseFile = "data/app.accdb";
|
||||||
|
options.Database.EncryptedPassword = configuration["Access:EncryptedPassword"];
|
||||||
|
// 配置 Data 模块能力
|
||||||
|
options.ConfigureDataContext = (sp, ctx) =>
|
||||||
|
{
|
||||||
|
ctx.UseAudit()
|
||||||
|
.UseSoftDelete()
|
||||||
|
.UseCurrentUserProvider(() => sp.GetService<ICurrentUser>()?.Id);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
随后只需要注入 `IDataContext` 或 `IAccessDataContextFactory<AppDbContext>` 即可使用:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class UserService
|
||||||
|
{
|
||||||
|
private readonly IDataContext _db;
|
||||||
|
|
||||||
|
public UserService(IDataContext db) => _db = db;
|
||||||
|
|
||||||
|
public Task<List<User>> GetUsersAsync() => _db.Repository<User>().GetAllAsync();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 依赖注入示例(带模板/密码)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
|
||||||
|
services.AddSingleton<ISecretProvider, SecretProvider>(); // 自定义的密钥服务
|
||||||
|
|
||||||
|
services.AddAccessDataContext<AppDbContext>(options =>
|
||||||
|
{
|
||||||
|
options.Database.DatabaseFile = "data/app.accdb";
|
||||||
|
options.Database.TemplateFile = "data/template.accdb";
|
||||||
|
options.Database.CopyTemplateWhenMissing = true;
|
||||||
|
options.Database.EncryptedPasswordFactory = sp =>
|
||||||
|
sp.GetRequiredService<ISecretProvider>().Get("AccessDbPassword");
|
||||||
|
|
||||||
|
options.ConfigureDataContext = (sp, ctx) =>
|
||||||
|
{
|
||||||
|
ctx.UseAudit()
|
||||||
|
.UseSoftDelete()
|
||||||
|
.UseCurrentUserProvider(() => sp.GetRequiredService<ICurrentUserAccessor>().Id);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
await using var provider = services.BuildServiceProvider();
|
||||||
|
var factory = provider.GetRequiredService<IAccessDataContextFactory<AppDbContext>>();
|
||||||
|
await using var dataContext = factory.Create();
|
||||||
|
```
|
||||||
|
|
||||||
|
> 在实际业务项目中可以把 `IAccessDataContextFactory<TContext>` 注入到需要“短生命周期”数据库连接的服务中,或直接注入 `IDataContext`(作用域作用域内共享同一实例)。
|
||||||
|
|
||||||
|
## 处理加密密码
|
||||||
|
|
||||||
|
实现 `IAccessPasswordDecryptor` 并注册到 DI:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
builder.Services.AddSingleton<IAccessPasswordDecryptor, AesPasswordDecryptor>();
|
||||||
|
```
|
||||||
|
|
||||||
|
`AccessDatabaseOptions.EncryptedPassword` 会在构建连接字符串前自动调用该服务完成解密,也可以直接提供明文 `Password`。
|
||||||
|
|
||||||
|
## 模板与初始化
|
||||||
|
|
||||||
|
- `CopyTemplateWhenMissing = true`(默认)时,如果指定了 `TemplateFile`,将在首次运行时复制模板数据库。
|
||||||
|
- `ThrowIfMissing = true` 可在缺少数据库文件时立即抛出异常,便于早期发现部署问题。
|
||||||
|
- 所有路径都支持相对路径(基于 `AppContext.BaseDirectory`),也可以通过 `DataDirectoryFactory` 自定义。
|
||||||
|
|
||||||
|
## 手动使用(无依赖注入)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var builder = new DataContextBuilder<AppDbContext>();
|
||||||
|
builder.UseAccessDatabase(options =>
|
||||||
|
{
|
||||||
|
options.DatabaseFile = "data/app.accdb";
|
||||||
|
options.Password = "secret";
|
||||||
|
});
|
||||||
|
|
||||||
|
var db = builder.Build();
|
||||||
|
```
|
||||||
|
|
||||||
|
## 可扩展点
|
||||||
|
|
||||||
|
- `IAccessPasswordDecryptor`:自定义密码加解密。
|
||||||
|
- `IAccessConnectionStringFactory`:自定义连接串构建逻辑。
|
||||||
|
- `IAccessDatabaseInitializer`:自定义数据库初始化策略(检查锁、备份等)。
|
||||||
|
- `AccessContextOptions.ConfigureDataContext`:复用 Data 模块的审计、软删除、自动迁移等能力。
|
||||||
|
|
||||||
|
通过这些基础设施,Access 支持可在任何项目中复用,同时保持代码简洁、低耦合。
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
namespace Sharp.Data.Access.Security;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Access 密码解密接口,调用方可替换以实现自定义加解密策略。
|
||||||
|
/// </summary>
|
||||||
|
public interface IAccessPasswordDecryptor
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 解密 Access 数据库密码。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="encryptedPassword">加密后的密码。</param>
|
||||||
|
/// <returns>返回明文密码,如果无法解密则返回 null。</returns>
|
||||||
|
string? Decrypt(string? encryptedPassword);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Sharp.Data.Access.Security;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 默认的密码解密器,直接返回原始值。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PassthroughAccessPasswordDecryptor : IAccessPasswordDecryptor
|
||||||
|
{
|
||||||
|
public string? Decrypt(string? encryptedPassword) => encryptedPassword;
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
||||||
|
using Sharp.Data.Entities;
|
||||||
|
using Sharp.Data.Repository;
|
||||||
|
|
||||||
|
namespace Sharp.Data.Context;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 数据上下文实现
|
||||||
|
/// </summary>
|
||||||
|
internal class DataContext : IDataContext
|
||||||
|
{
|
||||||
|
private readonly DbContext _context;
|
||||||
|
private readonly DataContextOptions _options;
|
||||||
|
private readonly Dictionary<Type, object> _repositories = new();
|
||||||
|
|
||||||
|
public DataContext(DbContext context, DataContextOptions options)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DbContext Context => _context;
|
||||||
|
|
||||||
|
public IRepository<TEntity> Repository<TEntity>() where TEntity : class
|
||||||
|
{
|
||||||
|
var type = typeof(TEntity);
|
||||||
|
|
||||||
|
if (_repositories.ContainsKey(type))
|
||||||
|
{
|
||||||
|
return (IRepository<TEntity>)_repositories[type];
|
||||||
|
}
|
||||||
|
|
||||||
|
var repository = new Repository<TEntity>(_context, _options.EnableSoftDelete);
|
||||||
|
_repositories[type] = repository;
|
||||||
|
|
||||||
|
return repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> SaveChangesAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
// 应用审计
|
||||||
|
if (_options.EnableAudit)
|
||||||
|
{
|
||||||
|
ApplyAuditInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用软删除
|
||||||
|
if (_options.EnableSoftDelete)
|
||||||
|
{
|
||||||
|
ApplySoftDelete();
|
||||||
|
}
|
||||||
|
|
||||||
|
return await _context.SaveChangesAsync(ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyAuditInfo()
|
||||||
|
{
|
||||||
|
var entries = _context.ChangeTracker.Entries()
|
||||||
|
.Where(e => e.Entity is IAuditableEntity &&
|
||||||
|
(e.State == EntityState.Added || e.State == EntityState.Modified));
|
||||||
|
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
var userId = _options.CurrentUserId;
|
||||||
|
|
||||||
|
foreach (var entry in entries)
|
||||||
|
{
|
||||||
|
var entity = (IAuditableEntity)entry.Entity;
|
||||||
|
|
||||||
|
if (entry.State == EntityState.Added)
|
||||||
|
{
|
||||||
|
entity.CreatedAt = now;
|
||||||
|
entity.CreatedBy = userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
entity.UpdatedAt = now;
|
||||||
|
entity.UpdatedBy = userId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplySoftDelete()
|
||||||
|
{
|
||||||
|
var entries = _context.ChangeTracker.Entries()
|
||||||
|
.Where(e => e.Entity is ISoftDeletable && e.State == EntityState.Deleted);
|
||||||
|
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
var userId = _options.CurrentUserId;
|
||||||
|
|
||||||
|
foreach (var entry in entries)
|
||||||
|
{
|
||||||
|
entry.State = EntityState.Modified;
|
||||||
|
var entity = (ISoftDeletable)entry.Entity;
|
||||||
|
entity.IsDeleted = true;
|
||||||
|
entity.DeletedAt = now;
|
||||||
|
entity.DeletedBy = userId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_context.Dispose();
|
||||||
|
_repositories.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Sharp.Data.Repository;
|
||||||
|
|
||||||
|
namespace Sharp.Data.Context;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 数据上下文接口 - 提供仓储访问和保存功能
|
||||||
|
/// </summary>
|
||||||
|
public interface IDataContext : IDisposable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取仓储
|
||||||
|
/// </summary>
|
||||||
|
IRepository<TEntity> Repository<TEntity>() where TEntity : class;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保存更改
|
||||||
|
/// </summary>
|
||||||
|
Task<int> SaveChangesAsync(CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 访问底层 DbContext(高级场景)
|
||||||
|
/// </summary>
|
||||||
|
DbContext Context { get; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<RootNamespace>Sharp.Data</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<!-- EF Core -->
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.0" />
|
||||||
|
|
||||||
|
<!-- 数据库提供者 -->
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0" />
|
||||||
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0" />
|
||||||
|
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.0" />
|
||||||
|
|
||||||
|
<!-- 依赖注入 -->
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<!-- 引用 Common 模块,复用基础功能 -->
|
||||||
|
<ProjectReference Include="..\Common\Common.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Pomelo.EntityFrameworkCore.MySql.Infrastructure;
|
||||||
|
using Sharp.Data.Context;
|
||||||
|
using Sharp.Data.Entities;
|
||||||
|
|
||||||
|
namespace Sharp.Data;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 数据上下文构建器 - 提供类似 HttpServerBuilder 的 Fluent API
|
||||||
|
/// </summary>
|
||||||
|
public class DataContextBuilder<TContext> where TContext : DbContext
|
||||||
|
{
|
||||||
|
private readonly DbContextOptionsBuilder<TContext> _optionsBuilder;
|
||||||
|
private readonly DataContextOptions _dataOptions;
|
||||||
|
private bool _databaseConfigured;
|
||||||
|
|
||||||
|
public DataContextBuilder()
|
||||||
|
{
|
||||||
|
_optionsBuilder = new DbContextOptionsBuilder<TContext>();
|
||||||
|
_dataOptions = new DataContextOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用 SQLite 数据库
|
||||||
|
/// </summary>
|
||||||
|
public DataContextBuilder<TContext> UseSqlite(string connectionString)
|
||||||
|
{
|
||||||
|
_optionsBuilder.UseSqlite(connectionString);
|
||||||
|
_databaseConfigured = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用 SQL Server 数据库
|
||||||
|
/// </summary>
|
||||||
|
public DataContextBuilder<TContext> UseSqlServer(string connectionString, Action<SqlServerDbContextOptionsBuilder>? configure = null)
|
||||||
|
{
|
||||||
|
_optionsBuilder.UseSqlServer(connectionString, configure);
|
||||||
|
_databaseConfigured = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用 MySQL 数据库
|
||||||
|
/// </summary>
|
||||||
|
public DataContextBuilder<TContext> UseMySql(string connectionString, string? serverVersion = null, Action<MySqlDbContextOptionsBuilder>? configure = null)
|
||||||
|
{
|
||||||
|
var version = string.IsNullOrEmpty(serverVersion)
|
||||||
|
? ServerVersion.AutoDetect(connectionString)
|
||||||
|
: ServerVersion.Parse(serverVersion);
|
||||||
|
|
||||||
|
_optionsBuilder.UseMySql(connectionString, version, configure);
|
||||||
|
_databaseConfigured = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用 PostgreSQL 数据库
|
||||||
|
/// </summary>
|
||||||
|
public DataContextBuilder<TContext> UseNpgsql(string connectionString, Action<Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.NpgsqlDbContextOptionsBuilder>? configure = null)
|
||||||
|
{
|
||||||
|
_optionsBuilder.UseNpgsql(connectionString, configure);
|
||||||
|
_databaseConfigured = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用内存数据库(用于测试)
|
||||||
|
/// </summary>
|
||||||
|
public DataContextBuilder<TContext> UseInMemoryDatabase(string databaseName = "TestDb")
|
||||||
|
{
|
||||||
|
_optionsBuilder.UseInMemoryDatabase(databaseName);
|
||||||
|
_databaseConfigured = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 启用审计功能(默认启用)
|
||||||
|
/// </summary>
|
||||||
|
public DataContextBuilder<TContext> UseAudit(bool enable = true)
|
||||||
|
{
|
||||||
|
_dataOptions.EnableAudit = enable;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 启用软删除功能(默认启用)
|
||||||
|
/// </summary>
|
||||||
|
public DataContextBuilder<TContext> UseSoftDelete(bool enable = true)
|
||||||
|
{
|
||||||
|
_dataOptions.EnableSoftDelete = enable;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 启用自动查询过滤器(自动过滤已删除数据,默认启用)
|
||||||
|
/// </summary>
|
||||||
|
public DataContextBuilder<TContext> UseAutoQueryFilter(bool enable = true)
|
||||||
|
{
|
||||||
|
_dataOptions.EnableAutoQueryFilter = enable;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 启用自动迁移(启动时自动执行 Migrate)
|
||||||
|
/// </summary>
|
||||||
|
public DataContextBuilder<TContext> UseAutoMigration(bool enable = true)
|
||||||
|
{
|
||||||
|
_dataOptions.EnableAutoMigration = enable;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设置当前用户 ID(用于审计)
|
||||||
|
/// </summary>
|
||||||
|
public DataContextBuilder<TContext> UseCurrentUser(string userId)
|
||||||
|
{
|
||||||
|
_dataOptions.CurrentUserId = userId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设置当前用户 ID 提供者(动态获取)
|
||||||
|
/// </summary>
|
||||||
|
public DataContextBuilder<TContext> UseCurrentUserProvider(Func<string?> provider)
|
||||||
|
{
|
||||||
|
_dataOptions.CurrentUserIdProvider = provider;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 启用详细错误信息
|
||||||
|
/// </summary>
|
||||||
|
public DataContextBuilder<TContext> EnableDetailedErrors(bool enable = true)
|
||||||
|
{
|
||||||
|
_optionsBuilder.EnableDetailedErrors(enable);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 启用敏感数据日志(仅开发环境)
|
||||||
|
/// </summary>
|
||||||
|
public DataContextBuilder<TContext> EnableSensitiveDataLogging(bool enable = true)
|
||||||
|
{
|
||||||
|
_optionsBuilder.EnableSensitiveDataLogging(enable);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 自定义配置 DbContext 选项
|
||||||
|
/// </summary>
|
||||||
|
public DataContextBuilder<TContext> ConfigureDbContext(Action<DbContextOptionsBuilder<TContext>> configure)
|
||||||
|
{
|
||||||
|
configure(_optionsBuilder);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构建数据上下文
|
||||||
|
/// </summary>
|
||||||
|
public IDataContext Build()
|
||||||
|
{
|
||||||
|
// 如果没有配置数据库,默认使用 SQLite 内存数据库
|
||||||
|
if (!_databaseConfigured)
|
||||||
|
{
|
||||||
|
_optionsBuilder.UseSqlite("Data Source=:memory:");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建 DbContext 实例
|
||||||
|
var context = (TContext)Activator.CreateInstance(typeof(TContext), _optionsBuilder.Options)!;
|
||||||
|
|
||||||
|
// 自动迁移
|
||||||
|
if (_dataOptions.EnableAutoMigration)
|
||||||
|
{
|
||||||
|
context.Database.Migrate();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 确保数据库已创建
|
||||||
|
context.Database.EnsureCreated();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前用户 ID
|
||||||
|
if (_dataOptions.CurrentUserIdProvider != null)
|
||||||
|
{
|
||||||
|
_dataOptions.CurrentUserId = _dataOptions.CurrentUserIdProvider();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DataContext(context, _dataOptions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
namespace Sharp.Data;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 数据上下文配置选项
|
||||||
|
/// </summary>
|
||||||
|
public class DataContextOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 启用审计(自动填充 CreatedAt/By, UpdatedAt/By)
|
||||||
|
/// </summary>
|
||||||
|
public bool EnableAudit { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 启用软删除(逻辑删除)
|
||||||
|
/// </summary>
|
||||||
|
public bool EnableSoftDelete { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 启用自动查询过滤器(自动过滤 IsDeleted=true 的数据)
|
||||||
|
/// </summary>
|
||||||
|
public bool EnableAutoQueryFilter { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 启用自动迁移(启动时自动执行 Migrate)
|
||||||
|
/// </summary>
|
||||||
|
public bool EnableAutoMigration { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 当前用户 ID(用于审计)
|
||||||
|
/// </summary>
|
||||||
|
public string? CurrentUserId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设置当前用户 ID 的委托
|
||||||
|
/// </summary>
|
||||||
|
public Func<string?>? CurrentUserIdProvider { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
namespace Sharp.Data.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 审计实体基类 - 包含审计和软删除功能
|
||||||
|
/// </summary>
|
||||||
|
public abstract class AuditableEntity<TKey> : Entity<TKey>, IAuditableEntity, ISoftDeletable
|
||||||
|
where TKey : IEquatable<TKey>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 创建时间
|
||||||
|
/// </summary>
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建人
|
||||||
|
/// </summary>
|
||||||
|
public string? CreatedBy { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 最后修改时间
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? UpdatedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 最后修改人
|
||||||
|
/// </summary>
|
||||||
|
public string? UpdatedBy { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否已删除
|
||||||
|
/// </summary>
|
||||||
|
public bool IsDeleted { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除时间
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? DeletedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除人
|
||||||
|
/// </summary>
|
||||||
|
public string? DeletedBy { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Guid 主键审计实体基类(最常用)
|
||||||
|
/// </summary>
|
||||||
|
public abstract class AuditableEntity : AuditableEntity<Guid>
|
||||||
|
{
|
||||||
|
protected AuditableEntity()
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Int 主键审计实体基类
|
||||||
|
/// </summary>
|
||||||
|
public abstract class AuditableEntityInt : AuditableEntity<int>
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Long 主键审计实体基类
|
||||||
|
/// </summary>
|
||||||
|
public abstract class AuditableEntityLong : AuditableEntity<long>
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
namespace Sharp.Data.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 实体基类 - 提供主键支持
|
||||||
|
/// </summary>
|
||||||
|
public abstract class Entity<TKey> where TKey : IEquatable<TKey>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 主键
|
||||||
|
/// </summary>
|
||||||
|
public TKey Id { get; set; } = default!;
|
||||||
|
|
||||||
|
public override bool Equals(object? obj)
|
||||||
|
{
|
||||||
|
if (obj is not Entity<TKey> other)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (ReferenceEquals(this, other))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (GetType() != other.GetType())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (Id.Equals(default(TKey)) || other.Id.Equals(default(TKey)))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return Id.Equals(other.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
return (GetType().ToString() + Id).GetHashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool operator ==(Entity<TKey>? a, Entity<TKey>? b)
|
||||||
|
{
|
||||||
|
if (a is null && b is null)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (a is null || b is null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return a.Equals(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool operator !=(Entity<TKey>? a, Entity<TKey>? b)
|
||||||
|
{
|
||||||
|
return !(a == b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Guid 主键实体基类(最常用)
|
||||||
|
/// </summary>
|
||||||
|
public abstract class Entity : Entity<Guid>
|
||||||
|
{
|
||||||
|
protected Entity()
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Int 主键实体基类
|
||||||
|
/// </summary>
|
||||||
|
public abstract class EntityInt : Entity<int>
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Long 主键实体基类
|
||||||
|
/// </summary>
|
||||||
|
public abstract class EntityLong : Entity<long>
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
namespace Sharp.Data.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 审计实体接口 - 自动记录创建和修改信息
|
||||||
|
/// </summary>
|
||||||
|
public interface IAuditableEntity
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 创建时间
|
||||||
|
/// </summary>
|
||||||
|
DateTime CreatedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建人
|
||||||
|
/// </summary>
|
||||||
|
string? CreatedBy { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 最后修改时间
|
||||||
|
/// </summary>
|
||||||
|
DateTime? UpdatedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 最后修改人
|
||||||
|
/// </summary>
|
||||||
|
string? UpdatedBy { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
namespace Sharp.Data.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 软删除接口 - 支持逻辑删除
|
||||||
|
/// </summary>
|
||||||
|
public interface ISoftDeletable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 是否已删除
|
||||||
|
/// </summary>
|
||||||
|
bool IsDeleted { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除时间
|
||||||
|
/// </summary>
|
||||||
|
DateTime? DeletedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除人
|
||||||
|
/// </summary>
|
||||||
|
string? DeletedBy { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
using System.Linq.Expressions;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Common.ExpandFun;
|
||||||
|
|
||||||
|
namespace Sharp.Data.Extensions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// IQueryable 查询扩展方法
|
||||||
|
/// </summary>
|
||||||
|
public static class QueryableExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 分页查询
|
||||||
|
/// </summary>
|
||||||
|
public static async Task<PagedResult<T>> ToPagedResultAsync<T>(
|
||||||
|
this IQueryable<T> query,
|
||||||
|
int page,
|
||||||
|
int pageSize,
|
||||||
|
CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
if (page < 1) page = 1;
|
||||||
|
if (pageSize < 1) pageSize = 20;
|
||||||
|
|
||||||
|
var totalCount = await query.CountAsync(ct);
|
||||||
|
|
||||||
|
var items = await query
|
||||||
|
.Skip((page - 1) * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
|
.ToListAsync(ct);
|
||||||
|
|
||||||
|
return new PagedResult<T>
|
||||||
|
{
|
||||||
|
Items = items,
|
||||||
|
TotalCount = totalCount,
|
||||||
|
PageIndex = page,
|
||||||
|
PageSize = pageSize
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 条件查询(仅当 condition 为 true 时应用 predicate)
|
||||||
|
/// </summary>
|
||||||
|
public static IQueryable<T> WhereIf<T>(
|
||||||
|
this IQueryable<T> query,
|
||||||
|
bool condition,
|
||||||
|
Expression<Func<T, bool>> predicate)
|
||||||
|
{
|
||||||
|
return condition ? query.Where(predicate) : query;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 动态排序
|
||||||
|
/// </summary>
|
||||||
|
public static IQueryable<T> OrderByProperty<T>(
|
||||||
|
this IQueryable<T> query,
|
||||||
|
string propertyName,
|
||||||
|
bool descending = false)
|
||||||
|
{
|
||||||
|
var parameter = Expression.Parameter(typeof(T), "x");
|
||||||
|
var property = Expression.Property(parameter, propertyName);
|
||||||
|
var lambda = Expression.Lambda(property, parameter);
|
||||||
|
|
||||||
|
var methodName = descending ? "OrderByDescending" : "OrderBy";
|
||||||
|
var resultExpression = Expression.Call(
|
||||||
|
typeof(Queryable),
|
||||||
|
methodName,
|
||||||
|
new[] { typeof(T), property.Type },
|
||||||
|
query.Expression,
|
||||||
|
Expression.Quote(lambda));
|
||||||
|
|
||||||
|
return query.Provider.CreateQuery<T>(resultExpression);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,452 @@
|
|||||||
|
# Sharp.Data
|
||||||
|
|
||||||
|
让 EF Core 使用更简单的数据访问模块,提供开箱即用的仓储模式、审计、软删除等基础设施。
|
||||||
|
|
||||||
|
> **设计理念**:Data 模块专注于数据访问基础设施,不包含业务逻辑。实体基类是**可选的**,你可以使用自己的实体定义。
|
||||||
|
|
||||||
|
## 特性
|
||||||
|
|
||||||
|
- ✅ **极简 API** - 3 行代码启动,类似 NetServer 的设计理念
|
||||||
|
- ✅ **泛型仓储** - 提供常用 CRUD 操作,无需重复编写
|
||||||
|
- ✅ **自动审计** - 可选的审计功能,自动记录创建/修改信息
|
||||||
|
- ✅ **软删除** - 可选的软删除支持,逻辑删除而非物理删除
|
||||||
|
- ✅ **分页查询** - 复用 Common 模块的分页功能
|
||||||
|
- ✅ **多数据库** - 支持 SQLite、SQL Server、MySQL、PostgreSQL、InMemory
|
||||||
|
- ✅ **低耦合** - 仅依赖 EF Core 和 Common 模块
|
||||||
|
- ✅ **灵活扩展** - 所有功能都是可选的,按需启用
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 1. 定义实体(可选使用基类)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Sharp.Data.Entities;
|
||||||
|
|
||||||
|
// 方式1:使用提供的基类(可选)
|
||||||
|
public class User : AuditableEntity // 包含审计和软删除
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string Email { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 方式2:完全自定义(不使用基类)
|
||||||
|
public class Product
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public decimal Price { get; set; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 定义 DbContext
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
public class AppDbContext : DbContext
|
||||||
|
{
|
||||||
|
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
|
||||||
|
|
||||||
|
public DbSet<User> Users => Set<User>();
|
||||||
|
public DbSet<Product> Products => Set<Product>();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 启动和使用(3 行代码)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Sharp.Data;
|
||||||
|
|
||||||
|
// 创建构建器并配置
|
||||||
|
var builder = new DataContextBuilder<AppDbContext>();
|
||||||
|
builder.UseSqlite("Data Source=app.db");
|
||||||
|
var db = builder.Build();
|
||||||
|
|
||||||
|
// 使用仓储
|
||||||
|
var userRepo = db.Repository<User>();
|
||||||
|
|
||||||
|
// 添加数据
|
||||||
|
await userRepo.AddAsync(new User
|
||||||
|
{
|
||||||
|
Name = "张三",
|
||||||
|
Email = "zhangsan@example.com"
|
||||||
|
});
|
||||||
|
await db.SaveChangesAsync(); // 如果使用 AuditableEntity,会自动记录 CreatedAt
|
||||||
|
|
||||||
|
// 查询数据
|
||||||
|
var users = await userRepo.GetAllAsync();
|
||||||
|
var user = await userRepo.FirstOrDefaultAsync(u => u.Email == "zhangsan@example.com");
|
||||||
|
|
||||||
|
// 分页查询(使用 Common 模块的 PagedResult)
|
||||||
|
var pagedUsers = await userRepo.GetPagedAsync(
|
||||||
|
page: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
predicate: u => u.Name.Contains("张"),
|
||||||
|
orderBy: q => q.OrderByDescending(u => u.Id));
|
||||||
|
|
||||||
|
Console.WriteLine($"Total: {pagedUsers.TotalCount}, Pages: {pagedUsers.TotalPages}");
|
||||||
|
```
|
||||||
|
|
||||||
|
## 模块依赖
|
||||||
|
|
||||||
|
```
|
||||||
|
Sharp.Data
|
||||||
|
├── EF Core 8.0 (数据库访问)
|
||||||
|
└── Sharp.Common (复用基础功能)
|
||||||
|
├── PagedResult<T> (分页结果)
|
||||||
|
├── Result<T> (错误处理,可选)
|
||||||
|
└── 各种扩展方法
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置选项
|
||||||
|
|
||||||
|
### 使用不同数据库
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var builder = new DataContextBuilder<AppDbContext>();
|
||||||
|
|
||||||
|
// SQLite
|
||||||
|
builder.UseSqlite("Data Source=app.db");
|
||||||
|
|
||||||
|
// SQL Server
|
||||||
|
builder.UseSqlServer("Server=.;Database=MyApp;Trusted_Connection=True;");
|
||||||
|
|
||||||
|
// MySQL
|
||||||
|
builder.UseMySql("server=localhost;database=myapp;user=root;password=123456", "8.0.35");
|
||||||
|
|
||||||
|
// PostgreSQL
|
||||||
|
builder.UseNpgsql("Host=localhost;Database=myapp;Username=postgres;Password=123456");
|
||||||
|
|
||||||
|
// 内存数据库(用于测试)
|
||||||
|
builder.UseInMemoryDatabase("TestDb");
|
||||||
|
```
|
||||||
|
|
||||||
|
### 配置功能(所有功能都是可选的)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var builder = new DataContextBuilder<AppDbContext>();
|
||||||
|
|
||||||
|
builder.UseSqlite("Data Source=app.db")
|
||||||
|
.UseAudit(true) // 启用审计(仅对实现 IAuditableEntity 的实体生效)
|
||||||
|
.UseSoftDelete(true) // 启用软删除(仅对实现 ISoftDeletable 的实体生效)
|
||||||
|
.UseAutoMigration(true) // 启动时自动迁移
|
||||||
|
.UseCurrentUser("user123") // 设置当前用户(用于审计)
|
||||||
|
.EnableDetailedErrors() // 启用详细错误信息
|
||||||
|
.EnableSensitiveDataLogging(); // 启用敏感数据日志(仅开发)
|
||||||
|
|
||||||
|
var db = builder.Build();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 动态获取当前用户
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
builder.UseCurrentUserProvider(() =>
|
||||||
|
{
|
||||||
|
// 从你的认证系统获取当前用户
|
||||||
|
return HttpContext.Current?.User?.Identity?.Name ?? "system";
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 实体基类(可选)
|
||||||
|
|
||||||
|
Data 模块提供了可选的实体基类,你**不是必须**使用它们:
|
||||||
|
|
||||||
|
### Entity<TKey> - 基础实体(可选)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Guid 主键(最常用)
|
||||||
|
public class Product : Entity { }
|
||||||
|
|
||||||
|
// Int 主键
|
||||||
|
public class Category : EntityInt { }
|
||||||
|
|
||||||
|
// Long 主键
|
||||||
|
public class Log : EntityLong { }
|
||||||
|
|
||||||
|
// 自定义主键类型
|
||||||
|
public class CustomEntity : Entity<string> { }
|
||||||
|
|
||||||
|
// 或者完全自定义
|
||||||
|
public class MyEntity
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### AuditableEntity<TKey> - 审计实体(可选)
|
||||||
|
|
||||||
|
如果继承此基类,会自动管理以下字段:
|
||||||
|
- `Id` - 主键
|
||||||
|
- `CreatedAt` - 创建时间
|
||||||
|
- `CreatedBy` - 创建人
|
||||||
|
- `UpdatedAt` - 更新时间
|
||||||
|
- `UpdatedBy` - 更新人
|
||||||
|
- `IsDeleted` - 是否删除
|
||||||
|
- `DeletedAt` - 删除时间
|
||||||
|
- `DeletedBy` - 删除人
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 使用审计基类
|
||||||
|
public class User : AuditableEntity
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string Email { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 或者自己实现审计接口
|
||||||
|
public class MyEntity : IAuditableEntity, ISoftDeletable
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public string? CreatedBy { get; set; }
|
||||||
|
// ... 实现其他接口属性
|
||||||
|
}
|
||||||
|
|
||||||
|
// 或者完全自定义(不使用审计)
|
||||||
|
public class SimpleEntity
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 仓储方法
|
||||||
|
|
||||||
|
### 查询方法
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var repo = db.Repository<User>();
|
||||||
|
|
||||||
|
// 获取可查询对象
|
||||||
|
var query = repo.Query(trackChanges: false);
|
||||||
|
var activeUsers = await query.Where(u => u.Name.Contains("张")).ToListAsync();
|
||||||
|
|
||||||
|
// 根据主键获取
|
||||||
|
var user = await repo.GetByIdAsync(userId);
|
||||||
|
|
||||||
|
// 获取第一个符合条件的
|
||||||
|
var user = await repo.FirstOrDefaultAsync(u => u.Email == "test@example.com");
|
||||||
|
|
||||||
|
// 获取所有
|
||||||
|
var allUsers = await repo.GetAllAsync();
|
||||||
|
|
||||||
|
// 查找符合条件的
|
||||||
|
var users = await repo.FindAsync(u => u.Name.Contains("张"));
|
||||||
|
|
||||||
|
// 分页查询(返回 Common.ExpandFun.PagedResult<T>)
|
||||||
|
var pagedResult = await repo.GetPagedAsync(1, 20);
|
||||||
|
Console.WriteLine($"Total: {pagedResult.TotalCount}, Pages: {pagedResult.TotalPages}");
|
||||||
|
|
||||||
|
// 统计
|
||||||
|
var count = await repo.CountAsync(u => u.Name.Contains("张"));
|
||||||
|
var exists = await repo.AnyAsync(u => u.Email == "test@example.com");
|
||||||
|
```
|
||||||
|
|
||||||
|
### 修改方法
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 添加
|
||||||
|
var user = new User { Name = "张三", Email = "zhang@example.com" };
|
||||||
|
await repo.AddAsync(user);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
// 批量添加
|
||||||
|
var users = new List<User> { user1, user2, user3 };
|
||||||
|
await repo.AddRangeAsync(users);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
// 更新
|
||||||
|
user.Name = "李四";
|
||||||
|
repo.Update(user);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
// 删除(如果实体实现 ISoftDeletable,会软删除)
|
||||||
|
await repo.RemoveAsync(userId);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
// 物理删除(对于未实现 ISoftDeletable 的实体)
|
||||||
|
repo.Remove(user);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
```
|
||||||
|
|
||||||
|
## 查询扩展(复用 Common 模块)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Sharp.Data.Extensions;
|
||||||
|
using Common.ExpandFun; // PagedResult
|
||||||
|
|
||||||
|
// 分页扩展
|
||||||
|
var query = db.Context.Set<User>().Where(u => u.Name.Contains("张"));
|
||||||
|
var pagedResult = await query.ToPagedResultAsync(page: 1, pageSize: 20);
|
||||||
|
|
||||||
|
// 条件查询(来自 Common 模块的扩展)
|
||||||
|
var query = db.Context.Set<User>()
|
||||||
|
.WhereIf(!string.IsNullOrEmpty(searchTerm), u => u.Name.Contains(searchTerm));
|
||||||
|
|
||||||
|
// 动态排序
|
||||||
|
var query = db.Context.Set<User>()
|
||||||
|
.OrderByProperty("CreatedAt", descending: true);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 最佳实践
|
||||||
|
|
||||||
|
### 1. 控制台应用
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Sharp.Data;
|
||||||
|
|
||||||
|
var builder = new DataContextBuilder<AppDbContext>();
|
||||||
|
builder.UseSqlite("Data Source=app.db");
|
||||||
|
using var db = builder.Build();
|
||||||
|
|
||||||
|
// 使用 db 进行数据操作
|
||||||
|
var repo = db.Repository<User>();
|
||||||
|
await repo.AddAsync(new User { Name = "Test" });
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
// 自动释放资源
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. WPF 应用
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// App.xaml.cs
|
||||||
|
public partial class App : Application
|
||||||
|
{
|
||||||
|
public static IDataContext Database { get; private set; } = null!;
|
||||||
|
|
||||||
|
protected override void OnStartup(StartupEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnStartup(e);
|
||||||
|
|
||||||
|
var builder = new DataContextBuilder<AppDbContext>();
|
||||||
|
builder.UseSqlite("Data Source=app.db")
|
||||||
|
.UseCurrentUserProvider(() => GetCurrentUser());
|
||||||
|
|
||||||
|
Database = builder.Build();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnExit(ExitEventArgs e)
|
||||||
|
{
|
||||||
|
Database?.Dispose();
|
||||||
|
base.OnExit(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. ASP.NET Core 应用
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Program.cs
|
||||||
|
builder.Services.AddScoped<IDataContext>(sp =>
|
||||||
|
{
|
||||||
|
var dataBuilder = new DataContextBuilder<AppDbContext>();
|
||||||
|
|
||||||
|
dataBuilder.UseSqlServer(builder.Configuration.GetConnectionString("Default"))
|
||||||
|
.UseCurrentUserProvider(() =>
|
||||||
|
{
|
||||||
|
var httpContext = sp.GetService<IHttpContextAccessor>()?.HttpContext;
|
||||||
|
return httpContext?.User?.Identity?.Name ?? "anonymous";
|
||||||
|
});
|
||||||
|
|
||||||
|
return dataBuilder.Build();
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddHttpContextAccessor();
|
||||||
|
```
|
||||||
|
|
||||||
|
## 与其他 Sharp 模块集成
|
||||||
|
|
||||||
|
### 与 Sharp.Common 集成(已内置)
|
||||||
|
|
||||||
|
Data 模块已经引用了 Common 模块,可以直接使用:
|
||||||
|
- `PagedResult<T>` - 分页结果
|
||||||
|
- `Result<T>` - 错误处理(可选)
|
||||||
|
- 各种扩展方法
|
||||||
|
|
||||||
|
### 与 Sharp.Log 集成
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Sharp.Log;
|
||||||
|
|
||||||
|
var loggerFactory = SharpLogDefaults.CreateLoggerFactory();
|
||||||
|
|
||||||
|
builder.ConfigureDbContext(options =>
|
||||||
|
{
|
||||||
|
options.UseLoggerFactory(loggerFactory);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 高级功能
|
||||||
|
|
||||||
|
### 访问底层 DbContext
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var db = builder.Build();
|
||||||
|
|
||||||
|
// 执行原生 SQL
|
||||||
|
await db.Context.Database.ExecuteSqlRawAsync(
|
||||||
|
"DELETE FROM Logs WHERE CreatedAt < @p0",
|
||||||
|
DateTime.Now.AddDays(-30));
|
||||||
|
|
||||||
|
// 开始事务
|
||||||
|
using var transaction = await db.Context.Database.BeginTransactionAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
await transaction.CommitAsync();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await transaction.RollbackAsync();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### Q: 我必须使用 Entity/AuditableEntity 基类吗?
|
||||||
|
|
||||||
|
**不是!** 基类是**完全可选**的。你可以:
|
||||||
|
- 使用自己的实体定义
|
||||||
|
- 实现 `IAuditableEntity` 或 `ISoftDeletable` 接口
|
||||||
|
- 完全不使用审计和软删除功能
|
||||||
|
|
||||||
|
### Q: 如何使用自定义实体?
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 完全自定义,不使用任何基类
|
||||||
|
public class MyEntity
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 正常使用仓储
|
||||||
|
var repo = db.Repository<MyEntity>();
|
||||||
|
await repo.AddAsync(new MyEntity { Name = "Test" });
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q: PagedResult 在哪里定义的?
|
||||||
|
|
||||||
|
`PagedResult<T>` 来自 `Sharp.Common` 模块(`Common.ExpandFun` 命名空间),Data 模块复用它以避免重复。
|
||||||
|
|
||||||
|
### Q: 如何禁用审计或软删除?
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
builder.UseAudit(false) // 禁用审计
|
||||||
|
.UseSoftDelete(false); // 禁用软删除
|
||||||
|
```
|
||||||
|
|
||||||
|
## 性能建议
|
||||||
|
|
||||||
|
1. 查询时使用 `Query(trackChanges: false)` 提升性能
|
||||||
|
2. 批量操作使用 `AddRangeAsync`/`UpdateRange`
|
||||||
|
3. 生产环境关闭 `EnableSensitiveDataLogging`
|
||||||
|
4. 使用分页查询避免一次加载大量数据
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
MIT License
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
using System.Linq.Expressions;
|
||||||
|
using Common.ExpandFun;
|
||||||
|
|
||||||
|
namespace Sharp.Data.Repository;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 仓储接口 - 提供常用的数据访问方法
|
||||||
|
/// </summary>
|
||||||
|
public interface IRepository<TEntity> where TEntity : class
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取可查询对象
|
||||||
|
/// </summary>
|
||||||
|
IQueryable<TEntity> Query(bool trackChanges = false);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据主键获取实体
|
||||||
|
/// </summary>
|
||||||
|
Task<TEntity?> GetByIdAsync(object id, CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取第一个符合条件的实体
|
||||||
|
/// </summary>
|
||||||
|
Task<TEntity?> FirstOrDefaultAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取所有实体
|
||||||
|
/// </summary>
|
||||||
|
Task<List<TEntity>> GetAllAsync(CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找符合条件的实体列表
|
||||||
|
/// </summary>
|
||||||
|
Task<List<TEntity>> FindAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 分页查询
|
||||||
|
/// </summary>
|
||||||
|
Task<PagedResult<TEntity>> GetPagedAsync(
|
||||||
|
int page,
|
||||||
|
int pageSize,
|
||||||
|
Expression<Func<TEntity, bool>>? predicate = null,
|
||||||
|
Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>>? orderBy = null,
|
||||||
|
CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 统计数量
|
||||||
|
/// </summary>
|
||||||
|
Task<int> CountAsync(Expression<Func<TEntity, bool>>? predicate = null, CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断是否存在
|
||||||
|
/// </summary>
|
||||||
|
Task<bool> AnyAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 添加实体
|
||||||
|
/// </summary>
|
||||||
|
Task<TEntity> AddAsync(TEntity entity, CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 批量添加实体
|
||||||
|
/// </summary>
|
||||||
|
Task AddRangeAsync(IEnumerable<TEntity> entities, CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新实体
|
||||||
|
/// </summary>
|
||||||
|
void Update(TEntity entity);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 批量更新实体
|
||||||
|
/// </summary>
|
||||||
|
void UpdateRange(IEnumerable<TEntity> entities);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除实体
|
||||||
|
/// </summary>
|
||||||
|
void Remove(TEntity entity);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 批量删除实体
|
||||||
|
/// </summary>
|
||||||
|
void RemoveRange(IEnumerable<TEntity> entities);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据主键删除实体(支持软删除)
|
||||||
|
/// </summary>
|
||||||
|
Task RemoveAsync(object id, CancellationToken ct = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
using System.Linq.Expressions;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Common.ExpandFun;
|
||||||
|
using Sharp.Data.Entities;
|
||||||
|
|
||||||
|
namespace Sharp.Data.Repository;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 仓储实现
|
||||||
|
/// </summary>
|
||||||
|
internal class Repository<TEntity> : IRepository<TEntity> where TEntity : class
|
||||||
|
{
|
||||||
|
private readonly DbContext _context;
|
||||||
|
private readonly DbSet<TEntity> _dbSet;
|
||||||
|
private readonly bool _enableSoftDelete;
|
||||||
|
|
||||||
|
public Repository(DbContext context, bool enableSoftDelete)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_dbSet = context.Set<TEntity>();
|
||||||
|
_enableSoftDelete = enableSoftDelete;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IQueryable<TEntity> Query(bool trackChanges = false)
|
||||||
|
{
|
||||||
|
return trackChanges ? _dbSet : _dbSet.AsNoTracking();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TEntity?> GetByIdAsync(object id, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await _dbSet.FindAsync(new[] { id }, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TEntity?> FirstOrDefaultAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await _dbSet.FirstOrDefaultAsync(predicate, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<TEntity>> GetAllAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await _dbSet.ToListAsync(ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<TEntity>> FindAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await _dbSet.Where(predicate).ToListAsync(ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PagedResult<TEntity>> GetPagedAsync(
|
||||||
|
int page,
|
||||||
|
int pageSize,
|
||||||
|
Expression<Func<TEntity, bool>>? predicate = null,
|
||||||
|
Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>>? orderBy = null,
|
||||||
|
CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
if (page < 1) page = 1;
|
||||||
|
if (pageSize < 1) pageSize = 20;
|
||||||
|
|
||||||
|
var query = _dbSet.AsQueryable();
|
||||||
|
|
||||||
|
if (predicate != null)
|
||||||
|
query = query.Where(predicate);
|
||||||
|
|
||||||
|
var totalCount = await query.CountAsync(ct);
|
||||||
|
|
||||||
|
if (orderBy != null)
|
||||||
|
query = orderBy(query);
|
||||||
|
|
||||||
|
var items = await query
|
||||||
|
.Skip((page - 1) * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
|
.ToListAsync(ct);
|
||||||
|
|
||||||
|
return new PagedResult<TEntity>
|
||||||
|
{
|
||||||
|
Items = items,
|
||||||
|
TotalCount = totalCount,
|
||||||
|
PageIndex = page,
|
||||||
|
PageSize = pageSize
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> CountAsync(Expression<Func<TEntity, bool>>? predicate = null, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return predicate == null
|
||||||
|
? await _dbSet.CountAsync(ct)
|
||||||
|
: await _dbSet.CountAsync(predicate, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> AnyAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await _dbSet.AnyAsync(predicate, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TEntity> AddAsync(TEntity entity, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await _dbSet.AddAsync(entity, ct);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task AddRangeAsync(IEnumerable<TEntity> entities, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await _dbSet.AddRangeAsync(entities, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Update(TEntity entity)
|
||||||
|
{
|
||||||
|
_dbSet.Update(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateRange(IEnumerable<TEntity> entities)
|
||||||
|
{
|
||||||
|
_dbSet.UpdateRange(entities);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Remove(TEntity entity)
|
||||||
|
{
|
||||||
|
// 如果启用软删除且实体支持软删除
|
||||||
|
if (_enableSoftDelete && entity is ISoftDeletable softDeletable)
|
||||||
|
{
|
||||||
|
softDeletable.IsDeleted = true;
|
||||||
|
softDeletable.DeletedAt = DateTime.UtcNow;
|
||||||
|
_dbSet.Update(entity);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_dbSet.Remove(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveRange(IEnumerable<TEntity> entities)
|
||||||
|
{
|
||||||
|
if (_enableSoftDelete && typeof(ISoftDeletable).IsAssignableFrom(typeof(TEntity)))
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
foreach (var entity in entities)
|
||||||
|
{
|
||||||
|
if (entity is ISoftDeletable softDeletable)
|
||||||
|
{
|
||||||
|
softDeletable.IsDeleted = true;
|
||||||
|
softDeletable.DeletedAt = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_dbSet.UpdateRange(entities);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_dbSet.RemoveRange(entities);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RemoveAsync(object id, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var entity = await GetByIdAsync(id, ct);
|
||||||
|
if (entity != null)
|
||||||
|
{
|
||||||
|
Remove(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,498 @@
|
|||||||
|
# Sharp.Data 使用指南
|
||||||
|
|
||||||
|
本指南展示如何在不同场景下使用 Sharp.Data 模块。
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
- [快速开始](#快速开始)
|
||||||
|
- [控制台应用](#控制台应用)
|
||||||
|
- [WPF 应用](#wpf-应用)
|
||||||
|
- [ASP.NET Core 应用](#aspnet-core-应用)
|
||||||
|
- [常见场景](#常见场景)
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 1. 定义实体
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Sharp.Data.Entities;
|
||||||
|
|
||||||
|
// 简单实体
|
||||||
|
public class Product : Entity
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public decimal Price { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 审计实体(包含创建/修改信息)
|
||||||
|
public class User : AuditableEntity
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string Email { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 定义 DbContext
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
public class AppDbContext : DbContext
|
||||||
|
{
|
||||||
|
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
|
||||||
|
|
||||||
|
public DbSet<User> Users => Set<User>();
|
||||||
|
public DbSet<Product> Products => Set<Product>();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 使用
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Sharp.Data;
|
||||||
|
|
||||||
|
// 创建并配置
|
||||||
|
var builder = new DataContextBuilder<AppDbContext>();
|
||||||
|
builder.UseSqlite("Data Source=app.db");
|
||||||
|
var db = builder.Build();
|
||||||
|
|
||||||
|
// 使用仓储
|
||||||
|
var userRepo = db.Repository<User>();
|
||||||
|
await userRepo.AddAsync(new User { Name = "张三", Email = "zhang@example.com" });
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
```
|
||||||
|
|
||||||
|
## 控制台应用
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Sharp.Data;
|
||||||
|
using Sharp.Data.Entities;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
// 定义实体
|
||||||
|
public class Todo : AuditableEntity
|
||||||
|
{
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
public bool IsCompleted { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义 DbContext
|
||||||
|
public class TodoDbContext : DbContext
|
||||||
|
{
|
||||||
|
public TodoDbContext(DbContextOptions<TodoDbContext> options) : base(options) { }
|
||||||
|
public DbSet<Todo> Todos => Set<Todo>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Program.cs
|
||||||
|
class Program
|
||||||
|
{
|
||||||
|
static async Task Main(string[] args)
|
||||||
|
{
|
||||||
|
// 创建数据上下文
|
||||||
|
var builder = new DataContextBuilder<TodoDbContext>();
|
||||||
|
builder.UseSqlite("Data Source=todos.db")
|
||||||
|
.UseCurrentUser("console-user");
|
||||||
|
|
||||||
|
using var db = builder.Build();
|
||||||
|
var todoRepo = db.Repository<Todo>();
|
||||||
|
|
||||||
|
// 添加待办事项
|
||||||
|
await todoRepo.AddAsync(new Todo
|
||||||
|
{
|
||||||
|
Title = "学习 Sharp.Data",
|
||||||
|
IsCompleted = false
|
||||||
|
});
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
// 查询待办事项
|
||||||
|
var todos = await todoRepo.FindAsync(t => !t.IsCompleted);
|
||||||
|
foreach (var todo in todos)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"- {todo.Title}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## WPF 应用
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// App.xaml.cs
|
||||||
|
public partial class App : Application
|
||||||
|
{
|
||||||
|
public static IDataContext Database { get; private set; } = null!;
|
||||||
|
|
||||||
|
protected override void OnStartup(StartupEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnStartup(e);
|
||||||
|
|
||||||
|
// 初始化数据库
|
||||||
|
var builder = new DataContextBuilder<AppDbContext>();
|
||||||
|
builder.UseSqlite("Data Source=app.db")
|
||||||
|
.UseCurrentUserProvider(() => GetCurrentUser())
|
||||||
|
.UseAudit(true)
|
||||||
|
.UseSoftDelete(true);
|
||||||
|
|
||||||
|
Database = builder.Build();
|
||||||
|
|
||||||
|
// 显示主窗口
|
||||||
|
var mainWindow = new MainWindow();
|
||||||
|
mainWindow.Show();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnExit(ExitEventArgs e)
|
||||||
|
{
|
||||||
|
Database?.Dispose();
|
||||||
|
base.OnExit(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetCurrentUser()
|
||||||
|
{
|
||||||
|
// 从你的登录系统获取当前用户
|
||||||
|
return "current-user";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MainWindow.xaml.cs
|
||||||
|
public partial class MainWindow : Window
|
||||||
|
{
|
||||||
|
private readonly IRepository<User> _userRepo;
|
||||||
|
|
||||||
|
public MainWindow()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
_userRepo = App.Database.Repository<User>();
|
||||||
|
LoadUsers();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void LoadUsers()
|
||||||
|
{
|
||||||
|
var users = await _userRepo.GetAllAsync();
|
||||||
|
UsersListView.ItemsSource = users;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void AddUser_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var user = new User
|
||||||
|
{
|
||||||
|
Name = NameTextBox.Text,
|
||||||
|
Email = EmailTextBox.Text
|
||||||
|
};
|
||||||
|
|
||||||
|
await _userRepo.AddAsync(user);
|
||||||
|
await App.Database.SaveChangesAsync();
|
||||||
|
|
||||||
|
LoadUsers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## ASP.NET Core 应用
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Program.cs
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
// 注册 DataContext
|
||||||
|
builder.Services.AddScoped<IDataContext>(sp =>
|
||||||
|
{
|
||||||
|
var dataBuilder = new DataContextBuilder<AppDbContext>();
|
||||||
|
|
||||||
|
dataBuilder.UseSqlServer(builder.Configuration.GetConnectionString("Default"))
|
||||||
|
.UseCurrentUserProvider(() =>
|
||||||
|
{
|
||||||
|
var httpContext = sp.GetService<IHttpContextAccessor>()?.HttpContext;
|
||||||
|
return httpContext?.User?.Identity?.Name ?? "anonymous";
|
||||||
|
})
|
||||||
|
.UseAudit(true)
|
||||||
|
.UseSoftDelete(true)
|
||||||
|
.UseAutoMigration(true);
|
||||||
|
|
||||||
|
return dataBuilder.Build();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 注册 HttpContextAccessor(用于获取当前用户)
|
||||||
|
builder.Services.AddHttpContextAccessor();
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
app.MapControllers();
|
||||||
|
app.Run();
|
||||||
|
|
||||||
|
// Controllers/UsersController.cs
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
public class UsersController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IDataContext _db;
|
||||||
|
|
||||||
|
public UsersController(IDataContext db)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> GetUsers(
|
||||||
|
[FromQuery] int page = 1,
|
||||||
|
[FromQuery] int pageSize = 20,
|
||||||
|
[FromQuery] string? search = null)
|
||||||
|
{
|
||||||
|
var repo = _db.Repository<User>();
|
||||||
|
|
||||||
|
var result = await repo.GetPagedAsync(
|
||||||
|
page: page,
|
||||||
|
pageSize: pageSize,
|
||||||
|
predicate: string.IsNullOrEmpty(search)
|
||||||
|
? null
|
||||||
|
: u => u.Name.Contains(search) || u.Email.Contains(search),
|
||||||
|
orderBy: q => q.OrderByDescending(u => u.CreatedAt));
|
||||||
|
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{id}")]
|
||||||
|
public async Task<IActionResult> GetUser(Guid id)
|
||||||
|
{
|
||||||
|
var repo = _db.Repository<User>();
|
||||||
|
var user = await repo.GetByIdAsync(id);
|
||||||
|
|
||||||
|
if (user == null)
|
||||||
|
return NotFound();
|
||||||
|
|
||||||
|
return Ok(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<IActionResult> CreateUser([FromBody] CreateUserDto dto)
|
||||||
|
{
|
||||||
|
var repo = _db.Repository<User>();
|
||||||
|
|
||||||
|
var user = new User
|
||||||
|
{
|
||||||
|
Name = dto.Name,
|
||||||
|
Email = dto.Email
|
||||||
|
};
|
||||||
|
|
||||||
|
await repo.AddAsync(user);
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
|
||||||
|
return CreatedAtAction(nameof(GetUser), new { id = user.Id }, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut("{id}")]
|
||||||
|
public async Task<IActionResult> UpdateUser(Guid id, [FromBody] UpdateUserDto dto)
|
||||||
|
{
|
||||||
|
var repo = _db.Repository<User>();
|
||||||
|
var user = await repo.GetByIdAsync(id);
|
||||||
|
|
||||||
|
if (user == null)
|
||||||
|
return NotFound();
|
||||||
|
|
||||||
|
user.Name = dto.Name;
|
||||||
|
user.Email = dto.Email;
|
||||||
|
|
||||||
|
repo.Update(user);
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
|
||||||
|
return Ok(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("{id}")]
|
||||||
|
public async Task<IActionResult> DeleteUser(Guid id)
|
||||||
|
{
|
||||||
|
var repo = _db.Repository<User>();
|
||||||
|
await repo.RemoveAsync(id); // 软删除
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见场景
|
||||||
|
|
||||||
|
### 1. 事务处理
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using var transaction = await db.Context.Database.BeginTransactionAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var userRepo = db.Repository<User>();
|
||||||
|
var orderRepo = db.Repository<Order>();
|
||||||
|
|
||||||
|
// 创建用户
|
||||||
|
var user = new User { Name = "张三", Email = "zhang@example.com" };
|
||||||
|
await userRepo.AddAsync(user);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
// 创建订单
|
||||||
|
var order = new Order { UserId = user.Id, Total = 100 };
|
||||||
|
await orderRepo.AddAsync(order);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
await transaction.CommitAsync();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await transaction.RollbackAsync();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 复杂查询
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Sharp.Data.Extensions;
|
||||||
|
|
||||||
|
var repo = db.Repository<User>();
|
||||||
|
|
||||||
|
// 使用 Query 进行复杂查询
|
||||||
|
var query = repo.Query(trackChanges: false)
|
||||||
|
.Where(u => u.IsActive)
|
||||||
|
.WhereIf(!string.IsNullOrEmpty(searchTerm), u => u.Name.Contains(searchTerm))
|
||||||
|
.OrderByProperty("CreatedAt", descending: true);
|
||||||
|
|
||||||
|
var result = await query.ToPagedResultAsync(page: 1, pageSize: 20);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 批量操作
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var repo = db.Repository<User>();
|
||||||
|
|
||||||
|
// 批量添加
|
||||||
|
var users = new List<User>
|
||||||
|
{
|
||||||
|
new User { Name = "用户1", Email = "user1@example.com" },
|
||||||
|
new User { Name = "用户2", Email = "user2@example.com" },
|
||||||
|
new User { Name = "用户3", Email = "user3@example.com" }
|
||||||
|
};
|
||||||
|
await repo.AddRangeAsync(users);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
// 批量更新
|
||||||
|
foreach (var user in users)
|
||||||
|
{
|
||||||
|
user.IsActive = true;
|
||||||
|
}
|
||||||
|
repo.UpdateRange(users);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 原生 SQL 查询
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 执行原生 SQL
|
||||||
|
await db.Context.Database.ExecuteSqlRawAsync(
|
||||||
|
"UPDATE Users SET IsActive = 0 WHERE LastLoginDate < @p0",
|
||||||
|
DateTime.Now.AddYears(-1));
|
||||||
|
|
||||||
|
// 查询
|
||||||
|
var users = await db.Context.Set<User>()
|
||||||
|
.FromSqlRaw("SELECT * FROM Users WHERE Age > {0}", 18)
|
||||||
|
.ToListAsync();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 多数据库支持
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 开发环境:使用 SQLite
|
||||||
|
if (env.IsDevelopment())
|
||||||
|
{
|
||||||
|
builder.UseSqlite("Data Source=dev.db");
|
||||||
|
}
|
||||||
|
// 生产环境:使用 SQL Server
|
||||||
|
else
|
||||||
|
{
|
||||||
|
builder.UseSqlServer(configuration.GetConnectionString("Production"));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. 测试场景
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 使用内存数据库进行单元测试
|
||||||
|
var builder = new DataContextBuilder<AppDbContext>();
|
||||||
|
builder.UseInMemoryDatabase("TestDb");
|
||||||
|
|
||||||
|
using var db = builder.Build();
|
||||||
|
|
||||||
|
// 测试代码
|
||||||
|
var repo = db.Repository<User>();
|
||||||
|
await repo.AddAsync(new User { Name = "Test User", Email = "test@example.com" });
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
var users = await repo.GetAllAsync();
|
||||||
|
Assert.Single(users);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 最佳实践
|
||||||
|
|
||||||
|
### 1. 使用 using 释放资源
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using var db = builder.Build();
|
||||||
|
// 使用 db
|
||||||
|
// 自动释放资源
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 查询时禁用跟踪
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 只读查询,禁用跟踪以提升性能
|
||||||
|
var users = await repo.Query(trackChanges: false)
|
||||||
|
.Where(u => u.IsActive)
|
||||||
|
.ToListAsync();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 分页查询
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 始终使用分页,避免一次加载大量数据
|
||||||
|
var result = await repo.GetPagedAsync(page: 1, pageSize: 20);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 统一异常处理
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
catch (DbUpdateConcurrencyException)
|
||||||
|
{
|
||||||
|
// 处理并发冲突
|
||||||
|
}
|
||||||
|
catch (DbUpdateException ex)
|
||||||
|
{
|
||||||
|
// 处理数据库更新异常
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 性能优化
|
||||||
|
|
||||||
|
1. **查询优化**:使用 `AsNoTracking()` 或 `Query(trackChanges: false)`
|
||||||
|
2. **批量操作**:使用 `AddRangeAsync`/`UpdateRange` 代替逐个操作
|
||||||
|
3. **分页查询**:避免一次加载大量数据
|
||||||
|
4. **索引**:为常用查询字段添加索引
|
||||||
|
5. **禁用日志**:生产环境关闭 `EnableSensitiveDataLogging`
|
||||||
|
|
||||||
|
## 故障排除
|
||||||
|
|
||||||
|
### Q: 软删除不生效?
|
||||||
|
|
||||||
|
确保实体继承 `AuditableEntity` 或实现 `ISoftDeletable` 接口。
|
||||||
|
|
||||||
|
### Q: 审计字段没有自动填充?
|
||||||
|
|
||||||
|
检查是否调用了 `UseAudit(true)` 并设置了当前用户。
|
||||||
|
|
||||||
|
### Q: 查询性能慢?
|
||||||
|
|
||||||
|
- 使用 `AsNoTracking()`
|
||||||
|
- 添加适当的索引
|
||||||
|
- 使用分页查询
|
||||||
|
|
||||||
|
## 参考资料
|
||||||
|
|
||||||
|
- [README.md](README.md) - 完整文档
|
||||||
|
- [Examples](Examples/) - 示例代码
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
|
||||||
<PackageReference Include="Serilog" Version="4.0.0" />
|
<PackageReference Include="Serilog" Version="4.0.0" />
|
||||||
|
|||||||
+15
-1
@@ -32,6 +32,21 @@ app.UseSharpRequestLogging(); // 可选:请求日志
|
|||||||
app.Run();
|
app.Run();
|
||||||
```
|
```
|
||||||
|
|
||||||
|
// WPF/WinForms/任意桌面
|
||||||
|
```csharp
|
||||||
|
using Sharp.Log;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
|
var configuration = new ConfigurationBuilder()
|
||||||
|
.AddJsonFile("appsettings.json", optional: true)
|
||||||
|
.AddEnvironmentVariables()
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
using var loggerFactory = SharpLogDefaults.CreateLoggerFactory(configuration);
|
||||||
|
var logger = loggerFactory.CreateLogger<App>();
|
||||||
|
logger.LogInformation("桌面程序已启动");
|
||||||
|
```
|
||||||
|
|
||||||
无需任何 `appsettings.json`,即可获得:
|
无需任何 `appsettings.json`,即可获得:
|
||||||
- Console 文本输出
|
- Console 文本输出
|
||||||
- 文件 JSON 输出:`logs/app-.log`(按天滚动,保留 30 个文件,异步写)
|
- 文件 JSON 输出:`logs/app-.log`(按天滚动,保留 30 个文件,异步写)
|
||||||
@@ -98,4 +113,3 @@ app.Run();
|
|||||||
```
|
```
|
||||||
日志要简单可靠:一行接入,默认即用,可配更强。
|
日志要简单可靠:一行接入,默认即用,可配更强。
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.FileProviders;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Sharp.Log.Extensions;
|
||||||
|
using Sharp.Log.Options;
|
||||||
|
|
||||||
|
namespace Sharp.Log;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 提供针对桌面程序/简单控制台的“开箱即用”日志工厂。
|
||||||
|
/// </summary>
|
||||||
|
public static class SharpLogDefaults
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 创建一个已预置 Sharp.Log 的 <see cref="ILoggerFactory"/>。
|
||||||
|
/// 适用于无需完整 HostBuilder 的场景(WPF、WinForms、单元测试等)。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="configuration">可选:传入已加载的 IConfiguration,用于绑定 SharpLog 节点。</param>
|
||||||
|
/// <param name="environmentName">可选:覆盖环境名,默认读取 DOTNET/ASPNETCORE_ENVIRONMENT。</param>
|
||||||
|
/// <param name="configure">可选:对 <see cref="SharpLogOptions"/> 做额外调整。</param>
|
||||||
|
public static ILoggerFactory CreateLoggerFactory(
|
||||||
|
IConfiguration? configuration = null,
|
||||||
|
string? environmentName = null,
|
||||||
|
Action<SharpLogOptions>? configure = null)
|
||||||
|
{
|
||||||
|
var config = configuration ?? new ConfigurationBuilder().Build();
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
|
||||||
|
services.AddSingleton(config);
|
||||||
|
services.AddSingleton<IHostEnvironment>(new SimpleHostEnvironment(environmentName));
|
||||||
|
|
||||||
|
services.AddLogging(builder =>
|
||||||
|
{
|
||||||
|
builder.ClearProviders();
|
||||||
|
builder.AddSharpLog(configure);
|
||||||
|
});
|
||||||
|
|
||||||
|
var provider = services.BuildServiceProvider();
|
||||||
|
return new DisposableLoggerFactory(provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class DisposableLoggerFactory : ILoggerFactory
|
||||||
|
{
|
||||||
|
private readonly ServiceProvider _provider;
|
||||||
|
private readonly ILoggerFactory _inner;
|
||||||
|
|
||||||
|
public DisposableLoggerFactory(ServiceProvider provider)
|
||||||
|
{
|
||||||
|
_provider = provider;
|
||||||
|
_inner = provider.GetRequiredService<ILoggerFactory>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddProvider(ILoggerProvider provider) => _inner.AddProvider(provider);
|
||||||
|
|
||||||
|
public ILogger CreateLogger(string categoryName) => _inner.CreateLogger(categoryName);
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_inner.Dispose();
|
||||||
|
_provider.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class SimpleHostEnvironment : IHostEnvironment
|
||||||
|
{
|
||||||
|
public SimpleHostEnvironment(string? environmentName)
|
||||||
|
{
|
||||||
|
EnvironmentName = environmentName
|
||||||
|
?? Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT")
|
||||||
|
?? Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")
|
||||||
|
?? Environments.Production;
|
||||||
|
ApplicationName = TryGetEntryAssemblyName()
|
||||||
|
?? AppDomain.CurrentDomain.FriendlyName
|
||||||
|
?? "SharpApp";
|
||||||
|
ContentRootPath = AppContext.BaseDirectory;
|
||||||
|
ContentRootFileProvider = new NullFileProvider();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string EnvironmentName { get; set; }
|
||||||
|
public string ApplicationName { get; set; }
|
||||||
|
public string ContentRootPath { get; set; }
|
||||||
|
public IFileProvider ContentRootFileProvider { get; set; }
|
||||||
|
|
||||||
|
private static string? TryGetEntryAssemblyName()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return System.Reflection.Assembly.GetEntryAssembly()?.GetName().Name;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,7 +32,24 @@ app.MapGet("/", () => Results.Ok("ok"));
|
|||||||
app.Run();
|
app.Run();
|
||||||
```
|
```
|
||||||
|
|
||||||
## 3. 可选配置(appsettings.json)
|
## 3. 桌面/WPF/WinForms
|
||||||
|
```csharp
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Sharp.Log;
|
||||||
|
|
||||||
|
var configuration = new ConfigurationBuilder()
|
||||||
|
.AddJsonFile("appsettings.json", optional: true)
|
||||||
|
.AddEnvironmentVariables()
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
using var loggerFactory = SharpLogDefaults.CreateLoggerFactory(configuration);
|
||||||
|
var logger = loggerFactory.CreateLogger<App>();
|
||||||
|
logger.LogInformation("桌面程序已启动");
|
||||||
|
```
|
||||||
|
|
||||||
|
无需 HostBuilder,即可获得默认 Console + 滚动文件输出;同样支持 `SharpLog` 配置节/环境变量。
|
||||||
|
|
||||||
|
## 4. 可选配置(appsettings.json)
|
||||||
添加 `SharpLog` 配置节(可省略)。
|
添加 `SharpLog` 配置节(可省略)。
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -66,7 +83,7 @@ app.Run();
|
|||||||
- `SHARPLOG__FILE__PATH=/var/logs/app-.log`
|
- `SHARPLOG__FILE__PATH=/var/logs/app-.log`
|
||||||
- `SHARPLOG__FILE__RETAINEDFILECOUNTLIMIT=7`
|
- `SHARPLOG__FILE__RETAINEDFILECOUNTLIMIT=7`
|
||||||
|
|
||||||
## 4. 常见定制
|
## 5. 常见定制
|
||||||
- 仅文件输出(关闭 Console):
|
- 仅文件输出(关闭 Console):
|
||||||
```csharp
|
```csharp
|
||||||
builder.Logging.AddSharpLog(o => { o.UseConsole = false; });
|
builder.Logging.AddSharpLog(o => { o.UseConsole = false; });
|
||||||
@@ -84,17 +101,17 @@ app.Run();
|
|||||||
builder.Logging.AddSharpLog(o => { o.Enrich.Application = "MyApp"; });
|
builder.Logging.AddSharpLog(o => { o.Enrich.Application = "MyApp"; });
|
||||||
```
|
```
|
||||||
|
|
||||||
## 5. 与现有 Provider 协作
|
## 6. 与现有 Provider 协作
|
||||||
- 默认不清空 Provider;若希望仅用 Sharp.Log,可在应用中先执行 `ClearProviders()` 再调用 `AddSharpLog()`。
|
- 默认不清空 Provider;若希望仅用 Sharp.Log,可在应用中先执行 `ClearProviders()` 再调用 `AddSharpLog()`。
|
||||||
- 如将 `UseSerilog` 设为 `false`,不会注入 Serilog Provider,请保留框架自带 Console Provider(不要 `ClearProviders()`)。
|
- 如将 `UseSerilog` 设为 `false`,不会注入 Serilog Provider,请保留框架自带 Console Provider(不要 `ClearProviders()`)。
|
||||||
|
|
||||||
## 6. 请求日志中间件
|
## 7. 请求日志中间件
|
||||||
启用后会记录:请求开始、结束(状态码与耗时)、异常。
|
启用后会记录:请求开始、结束(状态码与耗时)、异常。
|
||||||
```csharp
|
```csharp
|
||||||
app.UseSharpRequestLogging();
|
app.UseSharpRequestLogging();
|
||||||
```
|
```
|
||||||
|
|
||||||
## 7. 结构化与上下文
|
## 8. 结构化与上下文
|
||||||
日志条目自动包含:
|
日志条目自动包含:
|
||||||
- `Application`、`Environment`、`MachineName`、`ProcessId`、`ThreadId`
|
- `Application`、`Environment`、`MachineName`、`ProcessId`、`ThreadId`
|
||||||
- `TraceId`、`SpanId`(来自 `Activity.Current`)
|
- `TraceId`、`SpanId`(来自 `Activity.Current`)
|
||||||
@@ -105,7 +122,7 @@ using var scope = logger.BeginScope(new Dictionary<string, object>{ ["UserId"] =
|
|||||||
logger.LogInformation("User {UserId} logged in", userId);
|
logger.LogInformation("User {UserId} logged in", userId);
|
||||||
```
|
```
|
||||||
|
|
||||||
## 8. 测试建议
|
## 9. 测试建议
|
||||||
- 在测试中为文件输出设置临时目录:
|
- 在测试中为文件输出设置临时目录:
|
||||||
```csharp
|
```csharp
|
||||||
builder.Logging.AddSharpLog(o =>
|
builder.Logging.AddSharpLog(o =>
|
||||||
@@ -117,4 +134,3 @@ logger.LogInformation("User {UserId} logged in", userId);
|
|||||||
```
|
```
|
||||||
|
|
||||||
如需更多示例,请参考本仓库的测试项目 `test/Log.Test`。
|
如需更多示例,请参考本仓库的测试项目 `test/Log.Test`。
|
||||||
|
|
||||||
@@ -2,6 +2,7 @@ using System;
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Config;
|
using Config;
|
||||||
using NetClient.Options;
|
using NetClient.Options;
|
||||||
|
using Sharp.Infrastructure.Configuration;
|
||||||
|
|
||||||
namespace NetClient.DependencyInjection;
|
namespace NetClient.DependencyInjection;
|
||||||
|
|
||||||
@@ -44,4 +45,3 @@ public static class ConfigurationExtensions
|
|||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Common\Common.csproj" />
|
<ProjectReference Include="..\Common\Common.csproj" />
|
||||||
<ProjectReference Include="..\Config\Config.csproj" />
|
<ProjectReference Include="..\Config\Config.csproj" />
|
||||||
|
<ProjectReference Include="..\Sharp.Infrastructure\Sharp.Infrastructure.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using Config;
|
using Config;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
using Sharp.Infrastructure.Configuration;
|
||||||
using Sharp.NetServer.Server;
|
using Sharp.NetServer.Server;
|
||||||
|
|
||||||
namespace Sharp.NetServer.DependencyInjection;
|
namespace Sharp.NetServer.DependencyInjection;
|
||||||
@@ -26,4 +27,3 @@ public static class ServiceCollectionExtensions
|
|||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Common\Common.csproj" />
|
<ProjectReference Include="..\Common\Common.csproj" />
|
||||||
<ProjectReference Include="..\Config\Config.csproj" />
|
<ProjectReference Include="..\Config\Config.csproj" />
|
||||||
|
<ProjectReference Include="..\Sharp.Infrastructure\Sharp.Infrastructure.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -294,9 +294,20 @@ public class HttpServer
|
|||||||
var controller = _serviceProvider.GetService(controllerType);
|
var controller = _serviceProvider.GetService(controllerType);
|
||||||
if (controller != null)
|
if (controller != null)
|
||||||
return controller;
|
return controller;
|
||||||
|
|
||||||
|
// 使用 ActivatorUtilities 创建实例(可以解析构造函数参数)
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return ActivatorUtilities.CreateInstance(_serviceProvider, controllerType);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger?.LogError(ex, "Failed to create controller {ControllerType} with DI", controllerType.Name);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用激活器创建
|
// 没有 DI 容器时,使用默认激活器创建(需要无参构造)
|
||||||
return Activator.CreateInstance(controllerType)!;
|
return Activator.CreateInstance(controllerType)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Pomelo.EntityFrameworkCore.MySql.Infrastructure;
|
||||||
|
using Repository.Options;
|
||||||
|
|
||||||
|
namespace Repository;
|
||||||
|
|
||||||
|
internal static class DatabaseConfigurator
|
||||||
|
{
|
||||||
|
public static void Configure(DbContextOptionsBuilder builder, DatabaseConnectionOptions options)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(options.ConnectionString))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Database connection '{options.ConnectionName ?? "Default"}' has an empty connection string.");
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.EnableDetailedErrors(options.EnableDetailedErrors);
|
||||||
|
builder.EnableSensitiveDataLogging(options.EnableSensitiveLogging);
|
||||||
|
|
||||||
|
switch (options.Provider)
|
||||||
|
{
|
||||||
|
case DatabaseProvider.SqlServer:
|
||||||
|
builder.UseSqlServer(options.ConnectionString, sql =>
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(options.MigrationsAssembly))
|
||||||
|
{
|
||||||
|
sql.MigrationsAssembly(options.MigrationsAssembly);
|
||||||
|
}
|
||||||
|
if (options.EnableRetryOnFailure)
|
||||||
|
{
|
||||||
|
sql.EnableRetryOnFailure(options.RetryCount, options.RetryDelay, errorNumbersToAdd: null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case DatabaseProvider.PostgreSql:
|
||||||
|
builder.UseNpgsql(options.ConnectionString, npgsql =>
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(options.MigrationsAssembly))
|
||||||
|
{
|
||||||
|
npgsql.MigrationsAssembly(options.MigrationsAssembly);
|
||||||
|
}
|
||||||
|
if (options.EnableRetryOnFailure)
|
||||||
|
{
|
||||||
|
npgsql.EnableRetryOnFailure(options.RetryCount, options.RetryDelay, null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case DatabaseProvider.MySql:
|
||||||
|
var serverVersion = string.IsNullOrWhiteSpace(options.ServerVersion)
|
||||||
|
? ServerVersion.AutoDetect(options.ConnectionString)
|
||||||
|
: ServerVersion.Parse(options.ServerVersion);
|
||||||
|
builder.UseMySql(options.ConnectionString, serverVersion, mysql =>
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(options.MigrationsAssembly))
|
||||||
|
{
|
||||||
|
mysql.MigrationsAssembly(options.MigrationsAssembly);
|
||||||
|
}
|
||||||
|
if (options.EnableRetryOnFailure)
|
||||||
|
{
|
||||||
|
mysql.EnableRetryOnFailure(options.RetryCount, options.RetryDelay, null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case DatabaseProvider.Sqlite:
|
||||||
|
builder.UseSqlite(options.ConnectionString, sqlite =>
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(options.MigrationsAssembly))
|
||||||
|
{
|
||||||
|
sqlite.MigrationsAssembly(options.MigrationsAssembly);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new NotSupportedException($"Unsupported database provider '{options.Provider}'.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
using Config;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Repository.Initialization;
|
||||||
|
using Repository.Options;
|
||||||
|
|
||||||
|
namespace Repository.DependencyInjection;
|
||||||
|
|
||||||
|
public static class RepositoryServiceCollectionExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 依据配置中心 (ConfigManager) 注册 DbContext,自动选择数据库 Provider 并可选执行迁移。
|
||||||
|
/// </summary>
|
||||||
|
public static IServiceCollection AddSharpDbContext<TContext>(this IServiceCollection services,
|
||||||
|
ConfigManager configManager,
|
||||||
|
string section = "Database",
|
||||||
|
string? connectionName = null,
|
||||||
|
bool initializeOnStartup = true,
|
||||||
|
Action<DbContextOptionsBuilder>? configureBuilder = null,
|
||||||
|
ServiceLifetime contextLifetime = ServiceLifetime.Scoped,
|
||||||
|
ServiceLifetime optionsLifetime = ServiceLifetime.Scoped)
|
||||||
|
where TContext : DbContext
|
||||||
|
{
|
||||||
|
var databaseOptions = configManager.Get<DatabaseOptions>(section, validate: false) ?? new DatabaseOptions();
|
||||||
|
var connection = databaseOptions.Resolve(connectionName);
|
||||||
|
return services.AddSharpDbContext<TContext>(connection, initializeOnStartup, configureBuilder, contextLifetime, optionsLifetime);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 直接基于手动提供的 DatabaseConnectionOptions 注册 DbContext。
|
||||||
|
/// </summary>
|
||||||
|
public static IServiceCollection AddSharpDbContext<TContext>(this IServiceCollection services,
|
||||||
|
DatabaseConnectionOptions connection,
|
||||||
|
bool initializeOnStartup = true,
|
||||||
|
Action<DbContextOptionsBuilder>? configureBuilder = null,
|
||||||
|
ServiceLifetime contextLifetime = ServiceLifetime.Scoped,
|
||||||
|
ServiceLifetime optionsLifetime = ServiceLifetime.Scoped)
|
||||||
|
where TContext : DbContext
|
||||||
|
{
|
||||||
|
var resolved = connection.Clone();
|
||||||
|
services.AddDbContext<TContext>((sp, builder) =>
|
||||||
|
{
|
||||||
|
DatabaseConfigurator.Configure(builder, resolved);
|
||||||
|
configureBuilder?.Invoke(builder);
|
||||||
|
}, contextLifetime, optionsLifetime);
|
||||||
|
|
||||||
|
services.TryAddSingleton<IDatabaseInitializer<TContext>>(sp =>
|
||||||
|
new DatabaseInitializer<TContext>(sp, resolved, initializeOnStartup));
|
||||||
|
|
||||||
|
if (initializeOnStartup)
|
||||||
|
{
|
||||||
|
services.TryAddEnumerable(ServiceDescriptor.Singleton<IHostedService>(sp =>
|
||||||
|
(IHostedService)sp.GetRequiredService<IDatabaseInitializer<TContext>>()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Repository.Options;
|
||||||
|
|
||||||
|
namespace Repository.Initialization;
|
||||||
|
|
||||||
|
internal sealed class DatabaseInitializer<TContext> : IDatabaseInitializer<TContext>, IHostedService
|
||||||
|
where TContext : DbContext
|
||||||
|
{
|
||||||
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
private readonly DatabaseConnectionOptions _options;
|
||||||
|
private readonly bool _runOnStartup;
|
||||||
|
|
||||||
|
public DatabaseInitializer(IServiceProvider serviceProvider, DatabaseConnectionOptions options, bool runOnStartup)
|
||||||
|
{
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
|
_options = options;
|
||||||
|
_runOnStartup = runOnStartup;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task InitializeAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
using var scope = _serviceProvider.CreateScope();
|
||||||
|
var context = scope.ServiceProvider.GetRequiredService<TContext>();
|
||||||
|
|
||||||
|
if (_options.AutoMigrate)
|
||||||
|
{
|
||||||
|
await context.Database.MigrateAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await context.Database.EnsureCreatedAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (!_runOnStartup)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
return InitializeAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Repository.Initialization;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 提供数据库初始化(建库/迁移)能力。
|
||||||
|
/// </summary>
|
||||||
|
public interface IDatabaseInitializer<in TContext> where TContext : DbContext
|
||||||
|
{
|
||||||
|
Task InitializeAsync(CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<Window x:Class="Repository.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:Repository"
|
|
||||||
mc:Ignorable="d"
|
|
||||||
Title="MainWindow" Height="450" Width="800">
|
|
||||||
<Grid>
|
|
||||||
|
|
||||||
</Grid>
|
|
||||||
</Window>
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
using System.Text;
|
|
||||||
using System.Windows;
|
|
||||||
using System.Windows.Controls;
|
|
||||||
using System.Windows.Data;
|
|
||||||
using System.Windows.Documents;
|
|
||||||
using System.Windows.Input;
|
|
||||||
using System.Windows.Media;
|
|
||||||
using System.Windows.Media.Imaging;
|
|
||||||
using System.Windows.Navigation;
|
|
||||||
using System.Windows.Shapes;
|
|
||||||
|
|
||||||
namespace Repository
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Interaction logic for MainWindow.xaml
|
|
||||||
/// </summary>
|
|
||||||
public partial class MainWindow : Window
|
|
||||||
{
|
|
||||||
public MainWindow()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Repository.Options;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 单个数据库连接的配置。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DatabaseConnectionOptions
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public string ConnectionString { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public DatabaseProvider Provider { get; set; } = DatabaseProvider.Sqlite;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 针对 MySQL/Pomelo 的服务器版本。留空则自动检测。
|
||||||
|
/// </summary>
|
||||||
|
public string? ServerVersion { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 指定迁移程序集;留空则使用 DbContext 所在程序集。
|
||||||
|
/// </summary>
|
||||||
|
public string? MigrationsAssembly { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否在启动时自动执行迁移(默认为 true)。
|
||||||
|
/// </summary>
|
||||||
|
public bool AutoMigrate { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 启用失败重试。
|
||||||
|
/// </summary>
|
||||||
|
public bool EnableRetryOnFailure { get; set; } = true;
|
||||||
|
|
||||||
|
public int RetryCount { get; set; } = 5;
|
||||||
|
|
||||||
|
public TimeSpan RetryDelay { get; set; } = TimeSpan.FromSeconds(5);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 打印敏感日志(调试阶段可启用)。
|
||||||
|
/// </summary>
|
||||||
|
public bool EnableSensitiveLogging { get; set; }
|
||||||
|
|
||||||
|
public bool EnableDetailedErrors { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 供内部追踪当前连接名称。
|
||||||
|
/// </summary>
|
||||||
|
public string? ConnectionName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 克隆当前选项,避免多处引用同一实例。
|
||||||
|
/// </summary>
|
||||||
|
public DatabaseConnectionOptions Clone()
|
||||||
|
{
|
||||||
|
return new DatabaseConnectionOptions
|
||||||
|
{
|
||||||
|
ConnectionString = ConnectionString,
|
||||||
|
Provider = Provider,
|
||||||
|
ServerVersion = ServerVersion,
|
||||||
|
MigrationsAssembly = MigrationsAssembly,
|
||||||
|
AutoMigrate = AutoMigrate,
|
||||||
|
EnableRetryOnFailure = EnableRetryOnFailure,
|
||||||
|
RetryCount = RetryCount,
|
||||||
|
RetryDelay = RetryDelay,
|
||||||
|
EnableSensitiveLogging = EnableSensitiveLogging,
|
||||||
|
EnableDetailedErrors = EnableDetailedErrors,
|
||||||
|
ConnectionName = ConnectionName
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
namespace Repository.Options;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 数据库配置根节点,支持多命名连接。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DatabaseOptions
|
||||||
|
{
|
||||||
|
public string DefaultConnection { get; set; } = "Default";
|
||||||
|
|
||||||
|
public IDictionary<string, DatabaseConnectionOptions> Connections { get; set; } =
|
||||||
|
new Dictionary<string, DatabaseConnectionOptions>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["Default"] = new DatabaseConnectionOptions
|
||||||
|
{
|
||||||
|
Provider = DatabaseProvider.Sqlite,
|
||||||
|
ConnectionString = "Data Source=app.db"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public DatabaseConnectionOptions Resolve(string? connectionName = null)
|
||||||
|
{
|
||||||
|
var name = string.IsNullOrWhiteSpace(connectionName) ? DefaultConnection : connectionName;
|
||||||
|
if (!Connections.TryGetValue(name, out var options))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Connection '{name}' is not configured.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var clone = options.Clone();
|
||||||
|
clone.ConnectionName = name;
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace Repository.Options;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支持的数据库提供器。
|
||||||
|
/// </summary>
|
||||||
|
public enum DatabaseProvider
|
||||||
|
{
|
||||||
|
SqlServer,
|
||||||
|
PostgreSql,
|
||||||
|
MySql,
|
||||||
|
Sqlite
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
# Sharp.Repository
|
||||||
|
|
||||||
|
面向 Sharp 套件的数据库集成模块,围绕 EF Core 封装“约定优于配置”的默认体验:通过一个配置段即可在 MySQL / SQL Server / PostgreSQL / Sqlite 之间切换,自动注册 `DbContext` 并在启动时执行 `Migration`/`EnsureCreated`。
|
||||||
|
|
||||||
|
## 功能
|
||||||
|
|
||||||
|
- **多 Provider**:内置 SQL Server、PostgreSQL、MySQL (Pomelo)、Sqlite,按配置自动选择 `UseXXX`.
|
||||||
|
- **多命名连接**:`Database.Connections` 支持任意命名,默认 `Default`.
|
||||||
|
- **自动迁移**:默认在应用启动(或手动调用)时执行 `context.Database.MigrateAsync()`。
|
||||||
|
- **DI 集成**:`services.AddSharpDbContext<TContext>(ConfigManager …)` 一行搞定配置绑定 + DbContext 注册 + 初始化器。
|
||||||
|
- **可扩展**:可选择禁用自动迁移、开启敏感日志/详细错误、指定迁移程序集、配置 MySQL ServerVersion。
|
||||||
|
|
||||||
|
## 配置示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Database": {
|
||||||
|
"DefaultConnection": "Main",
|
||||||
|
"Connections": {
|
||||||
|
"Main": {
|
||||||
|
"Provider": "Sqlite",
|
||||||
|
"ConnectionString": "Data Source=app.db"
|
||||||
|
},
|
||||||
|
"Reporting": {
|
||||||
|
"Provider": "SqlServer",
|
||||||
|
"ConnectionString": "Server=.;Database=Sharp;Trusted_Connection=True;",
|
||||||
|
"EnableRetryOnFailure": true,
|
||||||
|
"MigrationsAssembly": "Server"
|
||||||
|
},
|
||||||
|
"Analytics": {
|
||||||
|
"Provider": "PostgreSql",
|
||||||
|
"ConnectionString": "Host=localhost;Database=analytics;Username=postgres;Password=123456"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 快速接入
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Config;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Repository.DependencyInjection;
|
||||||
|
|
||||||
|
var cm = ConfigDefaults.BuildDefault();
|
||||||
|
|
||||||
|
services.AddSharpDbContext<AppDbContext>(cm, section: "Database", connectionName: "Main");
|
||||||
|
|
||||||
|
// 可选:如果没有 Host,可在启动时手动执行迁移
|
||||||
|
var initializer = serviceProvider.GetRequiredService<IDatabaseInitializer<AppDbContext>>();
|
||||||
|
await initializer.InitializeAsync();
|
||||||
|
```
|
||||||
|
|
||||||
|
默认会在 Host 启动时自动调用 `InitializeAsync`(借助 `IHostedService`)。若不想自动迁移,可在配置里设置 `AutoMigrate=false`,或者调用 `services.AddSharpDbContext(... initializeOnStartup: false)`。
|
||||||
|
|
||||||
|
## 手动選擇 Provider
|
||||||
|
|
||||||
|
若想在没有 Config 模块的场景手动注册:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var connection = new DatabaseConnectionOptions
|
||||||
|
{
|
||||||
|
Provider = DatabaseProvider.MySql,
|
||||||
|
ConnectionString = "server=localhost;database=app;user=root;password=123456",
|
||||||
|
ServerVersion = "8.0.35",
|
||||||
|
AutoMigrate = true
|
||||||
|
};
|
||||||
|
|
||||||
|
services.AddSharpDbContext<AppDbContext>(connection);
|
||||||
|
```
|
||||||
|
|
||||||
|
## DbContext 模板
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class AppDbContext : DbContext
|
||||||
|
{
|
||||||
|
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
|
||||||
|
|
||||||
|
public DbSet<User> Users => Set<User>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class User
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public string UserName { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
将 `Repository` 模块与 `Config` 结合后,使用者只需在 `appsettings.json` 填写连接串,即可获得可用的 `DbContext`,更复杂的配置(如连接池、命名策略)仍可沿用 EF Core 原本的扩展方法。
|
||||||
@@ -1,11 +1,22 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>WinExe</OutputType>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<TargetFramework>net8.0-windows</TargetFramework>
|
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<UseWPF>true</UseWPF>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.8" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.8" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.8" />
|
||||||
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4" />
|
||||||
|
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Config\Config.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<Application x:Class="Repository.App"
|
<Application x:Class="Server.App"
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:local="clr-namespace:Repository"
|
xmlns:local="clr-namespace:Server"
|
||||||
StartupUri="MainWindow.xaml">
|
StartupUri="MainWindow.xaml">
|
||||||
<Application.Resources>
|
<Application.Resources>
|
||||||
|
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Windows;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Config;
|
||||||
|
using Sharp.Log;
|
||||||
|
using Sharp.Data;
|
||||||
|
using Sharp.Data.Context;
|
||||||
|
using Sharp.Data.Access;
|
||||||
|
using Sharp.Data.Access.Options;
|
||||||
|
using Server.Config;
|
||||||
|
using Server.Services;
|
||||||
|
using Server.Data;
|
||||||
|
|
||||||
|
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; }
|
||||||
|
public IDataContext? DataContext { get; private set; }
|
||||||
|
public IAccessDataService? AccessDataService { get; private set; }
|
||||||
|
|
||||||
|
protected override void OnStartup(StartupEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnStartup(e);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 1. 初始化配置
|
||||||
|
InitializeConfiguration();
|
||||||
|
|
||||||
|
// 2. 初始化日志系统
|
||||||
|
InitializeLogging();
|
||||||
|
|
||||||
|
// 3. 初始化数据库
|
||||||
|
InitializeDatabase();
|
||||||
|
|
||||||
|
// 4. 初始化 Access 数据服务
|
||||||
|
InitializeAccessDataService();
|
||||||
|
|
||||||
|
// 5. 初始化服务器服务
|
||||||
|
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 (ConfigManager == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("配置未初始化");
|
||||||
|
}
|
||||||
|
|
||||||
|
LoggerFactory = SharpLogDefaults.CreateLoggerFactory(
|
||||||
|
configuration: ConfigManager.Configuration,
|
||||||
|
configure: options =>
|
||||||
|
{
|
||||||
|
options.Enrich.Application ??= "SharpServer";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeDatabase()
|
||||||
|
{
|
||||||
|
if (LoggerFactory == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("日志系统未初始化");
|
||||||
|
}
|
||||||
|
|
||||||
|
var logger = LoggerFactory.CreateLogger<App>();
|
||||||
|
logger.LogInformation("正在初始化数据库...");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var builder = new DataContextBuilder<AppDbContext>();
|
||||||
|
builder.UseSqlite("Data Source=server.db")
|
||||||
|
.UseAudit(true)
|
||||||
|
.UseSoftDelete(true)
|
||||||
|
.UseCurrentUser("system")
|
||||||
|
.EnableDetailedErrors(true);
|
||||||
|
|
||||||
|
DataContext = builder.Build();
|
||||||
|
|
||||||
|
logger.LogInformation("数据库初始化成功");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "数据库初始化失败");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeAccessDataService()
|
||||||
|
{
|
||||||
|
if (LoggerFactory == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("日志系统未初始化");
|
||||||
|
}
|
||||||
|
|
||||||
|
var logger = LoggerFactory.CreateLogger<App>();
|
||||||
|
logger.LogInformation("正在初始化 Access 数据服务...");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 诊断:输出当前目录信息
|
||||||
|
logger.LogInformation("AppContext.BaseDirectory: {BaseDirectory}", AppContext.BaseDirectory);
|
||||||
|
logger.LogInformation("Environment.CurrentDirectory: {CurrentDirectory}", Environment.CurrentDirectory);
|
||||||
|
logger.LogInformation("数据库文件在输出目录: {DbPath}",
|
||||||
|
Path.Combine(AppContext.BaseDirectory, "dbDwDmsSetup.accdb"));
|
||||||
|
logger.LogInformation("数据库文件是否存在: {Exists}",
|
||||||
|
File.Exists(Path.Combine(AppContext.BaseDirectory, "dbDwDmsSetup.accdb")));
|
||||||
|
|
||||||
|
// 配置 Access 数据库选项
|
||||||
|
var options = new AccessDatabaseOptions
|
||||||
|
{
|
||||||
|
DatabaseFile = "dbDwDmsSetup.accdb",
|
||||||
|
|
||||||
|
// 使用 ACE OLE DB 16.0(功能最完整,支持所有连接字符串参数)
|
||||||
|
// 注意:12.0 不支持 Connect Timeout 参数,会导致 0x80040E21 错误
|
||||||
|
ProviderName = AccessProviderNames.AceOleDb16,
|
||||||
|
|
||||||
|
// 如果数据库有密码,可以在这里设置:
|
||||||
|
// Password = "your_password",
|
||||||
|
|
||||||
|
// 或者使用加密密码(需要先注册自定义解密器):
|
||||||
|
// EncryptedPassword = "encrypted_password",
|
||||||
|
|
||||||
|
// 可选:调整稳健性配置
|
||||||
|
// MaxRetryCount = 3, // 最大重试次数
|
||||||
|
// RetryDelayMilliseconds = 100, // 重试延迟
|
||||||
|
// ConnectionTimeout = 15, // 连接超时(秒)
|
||||||
|
};
|
||||||
|
|
||||||
|
// 创建 Access 数据服务
|
||||||
|
AccessDataService = new AccessDataService(
|
||||||
|
options,
|
||||||
|
logger: LoggerFactory.CreateLogger<AccessDataService>());
|
||||||
|
|
||||||
|
logger.LogInformation("Access 数据服务初始化成功");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Access 数据服务初始化失败");
|
||||||
|
// 不抛出异常,因为 Access 数据库是可选的
|
||||||
|
logger.LogWarning("Access 数据服务初始化失败,但应用程序将继续运行");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeServerService()
|
||||||
|
{
|
||||||
|
if (AppConfig == null || LoggerFactory == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("配置或日志系统未初始化");
|
||||||
|
}
|
||||||
|
|
||||||
|
ServerService = new ServerService(AppConfig, LoggerFactory);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnExit(ExitEventArgs e)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ServerService?.Dispose();
|
||||||
|
DataContext?.Dispose();
|
||||||
|
|
||||||
|
var logger = LoggerFactory?.CreateLogger<App>();
|
||||||
|
logger?.LogInformation("应用程序正常退出");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// 忽略退出时的异常
|
||||||
|
Console.WriteLine($"退出时出错: {ex.Message}");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
LoggerFactory?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
base.OnExit(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
File renamed without changes.
@@ -0,0 +1,27 @@
|
|||||||
|
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 AppConfig
|
||||||
|
{
|
||||||
|
public ServerConfig Server { get; set; } = new();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
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 data = new
|
||||||
|
{
|
||||||
|
Count = items.Count,
|
||||||
|
Items = items
|
||||||
|
};
|
||||||
|
|
||||||
|
await WriteSuccessResponseAsync(context, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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))
|
||||||
|
{
|
||||||
|
await WriteSuccessResponseAsync(context, item);
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
|
||||||
|
await WriteSuccessResponseAsync(context, item, "Data created successfully");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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;
|
||||||
|
}
|
||||||
|
|
||||||
|
await WriteSuccessResponseAsync(context, item, "Data updated successfully");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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))
|
||||||
|
{
|
||||||
|
await WriteSuccessResponseAsync(context, item, "Data deleted successfully");
|
||||||
|
}
|
||||||
|
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 data = new { DeletedCount = count };
|
||||||
|
await WriteSuccessResponseAsync(context, data, "All data cleared");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#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 data = new
|
||||||
|
{
|
||||||
|
Status = "Healthy",
|
||||||
|
Timestamp = DateTime.UtcNow,
|
||||||
|
Uptime = DateTime.UtcNow - Process.GetCurrentProcess().StartTime
|
||||||
|
};
|
||||||
|
|
||||||
|
await WriteSuccessResponseAsync(context, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 详细健康检查
|
||||||
|
/// GET /api/health/detailed
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("/detailed")]
|
||||||
|
public async Task DetailedCheck(HttpContext context)
|
||||||
|
{
|
||||||
|
var process = Process.GetCurrentProcess();
|
||||||
|
var data = 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, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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 data = 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, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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 = 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 data = 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, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Server.Data.Access;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Access 数据库上下文
|
||||||
|
/// </summary>
|
||||||
|
public class AccessDbContext : DbContext
|
||||||
|
{
|
||||||
|
public AccessDbContext(DbContextOptions<AccessDbContext> options) : base(options)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public DbSet<UserInfo> UserInfos => Set<UserInfo>();
|
||||||
|
public DbSet<UserPermission> UserPermissions => Set<UserPermission>();
|
||||||
|
|
||||||
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
base.OnModelCreating(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity<UserInfo>(entity =>
|
||||||
|
{
|
||||||
|
entity.HasKey(e => e.UserName);
|
||||||
|
entity.Property(e => e.UserName).IsRequired().HasMaxLength(255);
|
||||||
|
entity.Property(e => e.Password).IsRequired().HasMaxLength(255);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<UserPermission>(entity =>
|
||||||
|
{
|
||||||
|
entity.HasKey(e => new { e.UserInfo_UserName, e.Permission_Code });
|
||||||
|
entity.Property(e => e.UserInfo_UserName).IsRequired().HasMaxLength(255);
|
||||||
|
entity.Property(e => e.Permission_Code).IsRequired().HasMaxLength(255);
|
||||||
|
|
||||||
|
entity.HasOne(e => e.UserInfo)
|
||||||
|
.WithMany(u => u.Permissions)
|
||||||
|
.HasForeignKey(e => e.UserInfo_UserName)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace Server.Data.Access;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// UserInfo 表实体
|
||||||
|
/// </summary>
|
||||||
|
[Table("UserInfo")]
|
||||||
|
public class UserInfo
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
[Required]
|
||||||
|
[MaxLength(255)]
|
||||||
|
public string UserName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[MaxLength(255)]
|
||||||
|
public string Password { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public ICollection<UserPermission> Permissions { get; set; } = new List<UserPermission>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// UserPermission 表实体
|
||||||
|
/// </summary>
|
||||||
|
[Table("UserPermission")]
|
||||||
|
public class UserPermission
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
[MaxLength(255)]
|
||||||
|
public string UserInfo_UserName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[MaxLength(255)]
|
||||||
|
public string Permission_Code { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[ForeignKey(nameof(UserInfo_UserName))]
|
||||||
|
public UserInfo? UserInfo { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,438 @@
|
|||||||
|
# Access 数据库 CRUD 完整示例
|
||||||
|
|
||||||
|
使用 **System.Data.OleDb** 对 Access 数据库进行增删改查操作。
|
||||||
|
|
||||||
|
## 📦 连接字符串
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var connectionString = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=dbDwDmsSetup.accdb";
|
||||||
|
using var connection = new OleDbConnection(connectionString);
|
||||||
|
connection.Open();
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1️⃣ 查询(SELECT)
|
||||||
|
|
||||||
|
### 查询所有用户
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using var connection = new OleDbConnection(connectionString);
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
var users = new List<UserInfo>();
|
||||||
|
using var command = new OleDbCommand("SELECT UserName, Password FROM UserInfo", connection);
|
||||||
|
using var reader = command.ExecuteReader();
|
||||||
|
|
||||||
|
while (reader.Read())
|
||||||
|
{
|
||||||
|
var user = new UserInfo
|
||||||
|
{
|
||||||
|
UserName = reader["UserName"]?.ToString() ?? "",
|
||||||
|
Password = reader["Password"]?.ToString() ?? ""
|
||||||
|
};
|
||||||
|
users.Add(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"查询到 {users.Count} 个用户");
|
||||||
|
```
|
||||||
|
|
||||||
|
### 条件查询
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 根据用户名查询
|
||||||
|
using var command = new OleDbCommand(
|
||||||
|
"SELECT UserName, Password FROM UserInfo WHERE UserName = ?", connection);
|
||||||
|
command.Parameters.AddWithValue("@UserName", "admin");
|
||||||
|
|
||||||
|
using var reader = command.ExecuteReader();
|
||||||
|
if (reader.Read())
|
||||||
|
{
|
||||||
|
var userName = reader["UserName"]?.ToString();
|
||||||
|
var password = reader["Password"]?.ToString();
|
||||||
|
Console.WriteLine($"找到用户: {userName}");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 统计数量
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using var command = new OleDbCommand(
|
||||||
|
"SELECT COUNT(*) FROM UserPermission WHERE UserInfo_UserName = ?", connection);
|
||||||
|
command.Parameters.AddWithValue("@UserName", "admin");
|
||||||
|
|
||||||
|
var count = Convert.ToInt32(command.ExecuteScalar());
|
||||||
|
Console.WriteLine($"该用户有 {count} 个权限");
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2️⃣ 添加(INSERT)
|
||||||
|
|
||||||
|
### 添加用户
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using var connection = new OleDbConnection(connectionString);
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
// 1. 先检查用户是否已存在
|
||||||
|
using (var checkCmd = new OleDbCommand(
|
||||||
|
"SELECT COUNT(*) FROM UserInfo WHERE UserName = ?", connection))
|
||||||
|
{
|
||||||
|
checkCmd.Parameters.AddWithValue("@UserName", "newuser");
|
||||||
|
var exists = Convert.ToInt32(checkCmd.ExecuteScalar()) > 0;
|
||||||
|
|
||||||
|
if (exists)
|
||||||
|
{
|
||||||
|
Console.WriteLine("用户已存在!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 插入新用户
|
||||||
|
using (var insertCmd = new OleDbCommand(
|
||||||
|
"INSERT INTO UserInfo (UserName, Password) VALUES (?, ?)", connection))
|
||||||
|
{
|
||||||
|
insertCmd.Parameters.AddWithValue("@UserName", "newuser");
|
||||||
|
insertCmd.Parameters.AddWithValue("@Password", "password123");
|
||||||
|
|
||||||
|
var rowsAffected = insertCmd.ExecuteNonQuery();
|
||||||
|
Console.WriteLine($"成功添加 {rowsAffected} 个用户");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 添加权限
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using var insertCmd = new OleDbCommand(
|
||||||
|
"INSERT INTO UserPermission (UserInfo_UserName, Permission_Code) VALUES (?, ?)", connection);
|
||||||
|
insertCmd.Parameters.AddWithValue("@UserName", "newuser");
|
||||||
|
insertCmd.Parameters.AddWithValue("@PermissionCode", "READ_DATA");
|
||||||
|
|
||||||
|
var rowsAffected = insertCmd.ExecuteNonQuery();
|
||||||
|
Console.WriteLine($"成功添加权限: {rowsAffected} 条");
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3️⃣ 修改(UPDATE)
|
||||||
|
|
||||||
|
### 修改用户密码
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using var connection = new OleDbConnection(connectionString);
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
using var updateCmd = new OleDbCommand(
|
||||||
|
"UPDATE UserInfo SET Password = ? WHERE UserName = ?", connection);
|
||||||
|
updateCmd.Parameters.AddWithValue("@Password", "newpassword");
|
||||||
|
updateCmd.Parameters.AddWithValue("@UserName", "admin");
|
||||||
|
|
||||||
|
var rowsAffected = updateCmd.ExecuteNonQuery();
|
||||||
|
|
||||||
|
if (rowsAffected > 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine("密码修改成功!");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine("用户不存在!");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 批量更新
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 将所有用户的密码更新为默认密码
|
||||||
|
using var updateCmd = new OleDbCommand(
|
||||||
|
"UPDATE UserInfo SET Password = ? WHERE Password IS NULL OR Password = ''", connection);
|
||||||
|
updateCmd.Parameters.AddWithValue("@Password", "default123");
|
||||||
|
|
||||||
|
var rowsAffected = updateCmd.ExecuteNonQuery();
|
||||||
|
Console.WriteLine($"更新了 {rowsAffected} 个用户的密码");
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4️⃣ 删除(DELETE)
|
||||||
|
|
||||||
|
### 删除用户(含事务)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using var connection = new OleDbConnection(connectionString);
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
// 使用事务确保数据一致性
|
||||||
|
using var transaction = connection.BeginTransaction();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 1. 先删除用户的所有权限
|
||||||
|
using (var deletePermCmd = new OleDbCommand(
|
||||||
|
"DELETE FROM UserPermission WHERE UserInfo_UserName = ?", connection, transaction))
|
||||||
|
{
|
||||||
|
deletePermCmd.Parameters.AddWithValue("@UserName", "testuser");
|
||||||
|
var permCount = deletePermCmd.ExecuteNonQuery();
|
||||||
|
Console.WriteLine($"删除了 {permCount} 个权限");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 再删除用户
|
||||||
|
using (var deleteUserCmd = new OleDbCommand(
|
||||||
|
"DELETE FROM UserInfo WHERE UserName = ?", connection, transaction))
|
||||||
|
{
|
||||||
|
deleteUserCmd.Parameters.AddWithValue("@UserName", "testuser");
|
||||||
|
var userCount = deleteUserCmd.ExecuteNonQuery();
|
||||||
|
|
||||||
|
if (userCount == 0)
|
||||||
|
{
|
||||||
|
transaction.Rollback();
|
||||||
|
Console.WriteLine("用户不存在!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"删除了 {userCount} 个用户");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 提交事务
|
||||||
|
transaction.Commit();
|
||||||
|
Console.WriteLine("删除成功!");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// 回滚事务
|
||||||
|
transaction.Rollback();
|
||||||
|
Console.WriteLine($"删除失败: {ex.Message}");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 删除权限
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using var deleteCmd = new OleDbCommand(
|
||||||
|
"DELETE FROM UserPermission WHERE UserInfo_UserName = ? AND Permission_Code = ?", connection);
|
||||||
|
deleteCmd.Parameters.AddWithValue("@UserName", "admin");
|
||||||
|
deleteCmd.Parameters.AddWithValue("@PermissionCode", "DELETE_DATA");
|
||||||
|
|
||||||
|
var rowsAffected = deleteCmd.ExecuteNonQuery();
|
||||||
|
Console.WriteLine($"删除了 {rowsAffected} 个权限");
|
||||||
|
```
|
||||||
|
|
||||||
|
### 批量删除
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 删除所有没有权限的用户
|
||||||
|
using var deleteCmd = new OleDbCommand(@"
|
||||||
|
DELETE FROM UserInfo
|
||||||
|
WHERE UserName NOT IN (
|
||||||
|
SELECT DISTINCT UserInfo_UserName FROM UserPermission
|
||||||
|
)", connection);
|
||||||
|
|
||||||
|
var rowsAffected = deleteCmd.ExecuteNonQuery();
|
||||||
|
Console.WriteLine($"删除了 {rowsAffected} 个无权限用户");
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔥 高级操作
|
||||||
|
|
||||||
|
### 1. JOIN 查询
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 查询用户及其所有权限
|
||||||
|
using var command = new OleDbCommand(@"
|
||||||
|
SELECT u.UserName, u.Password, p.Permission_Code
|
||||||
|
FROM UserInfo u
|
||||||
|
LEFT JOIN UserPermission p ON u.UserName = p.UserInfo_UserName
|
||||||
|
ORDER BY u.UserName", connection);
|
||||||
|
|
||||||
|
using var reader = command.ExecuteReader();
|
||||||
|
while (reader.Read())
|
||||||
|
{
|
||||||
|
var userName = reader["UserName"]?.ToString();
|
||||||
|
var permission = reader["Permission_Code"]?.ToString();
|
||||||
|
Console.WriteLine($"{userName}: {permission ?? "(无权限)"}");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 分组统计
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 统计每个用户的权限数量
|
||||||
|
using var command = new OleDbCommand(@"
|
||||||
|
SELECT u.UserName, COUNT(p.Permission_Code) AS PermCount
|
||||||
|
FROM UserInfo u
|
||||||
|
LEFT JOIN UserPermission p ON u.UserName = p.UserInfo_UserName
|
||||||
|
GROUP BY u.UserName
|
||||||
|
ORDER BY PermCount DESC", connection);
|
||||||
|
|
||||||
|
using var reader = command.ExecuteReader();
|
||||||
|
while (reader.Read())
|
||||||
|
{
|
||||||
|
var userName = reader["UserName"]?.ToString();
|
||||||
|
var count = Convert.ToInt32(reader["PermCount"]);
|
||||||
|
Console.WriteLine($"{userName}: {count} 个权限");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 参数化批量插入
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using var connection = new OleDbConnection(connectionString);
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
var users = new[]
|
||||||
|
{
|
||||||
|
("user1", "pass1"),
|
||||||
|
("user2", "pass2"),
|
||||||
|
("user3", "pass3")
|
||||||
|
};
|
||||||
|
|
||||||
|
using var transaction = connection.BeginTransaction();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var insertCmd = new OleDbCommand(
|
||||||
|
"INSERT INTO UserInfo (UserName, Password) VALUES (?, ?)", connection, transaction);
|
||||||
|
|
||||||
|
foreach (var (userName, password) in users)
|
||||||
|
{
|
||||||
|
insertCmd.Parameters.Clear();
|
||||||
|
insertCmd.Parameters.AddWithValue("@UserName", userName);
|
||||||
|
insertCmd.Parameters.AddWithValue("@Password", password);
|
||||||
|
insertCmd.ExecuteNonQuery();
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction.Commit();
|
||||||
|
Console.WriteLine($"成功添加 {users.Length} 个用户");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
transaction.Rollback();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 存在性检查
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 检查用户是否存在
|
||||||
|
public bool UserExists(string userName)
|
||||||
|
{
|
||||||
|
using var connection = new OleDbConnection(connectionString);
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
using var command = new OleDbCommand(
|
||||||
|
"SELECT COUNT(*) FROM UserInfo WHERE UserName = ?", connection);
|
||||||
|
command.Parameters.AddWithValue("@UserName", userName);
|
||||||
|
|
||||||
|
return Convert.ToInt32(command.ExecuteScalar()) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用
|
||||||
|
if (UserExists("admin"))
|
||||||
|
{
|
||||||
|
Console.WriteLine("用户存在");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 最佳实践
|
||||||
|
|
||||||
|
### 1. 使用 using 语句
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// ✅ 推荐:自动释放资源
|
||||||
|
using var connection = new OleDbConnection(connectionString);
|
||||||
|
using var command = new OleDbCommand(sql, connection);
|
||||||
|
using var reader = command.ExecuteReader();
|
||||||
|
|
||||||
|
// ❌ 不推荐:需要手动释放
|
||||||
|
var connection = new OleDbConnection(connectionString);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
connection.Dispose();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 使用参数化查询(防止 SQL 注入)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// ✅ 推荐:参数化查询
|
||||||
|
var sql = "SELECT * FROM UserInfo WHERE UserName = ?";
|
||||||
|
command.Parameters.AddWithValue("@UserName", userName);
|
||||||
|
|
||||||
|
// ❌ 危险:字符串拼接(SQL 注入风险)
|
||||||
|
var sql = $"SELECT * FROM UserInfo WHERE UserName = '{userName}'";
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 使用事务保证数据一致性
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// ✅ 推荐:使用事务
|
||||||
|
using var transaction = connection.BeginTransaction();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 多个操作
|
||||||
|
command1.ExecuteNonQuery();
|
||||||
|
command2.ExecuteNonQuery();
|
||||||
|
transaction.Commit();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
transaction.Rollback();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 异常处理
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var connection = new OleDbConnection(connectionString);
|
||||||
|
connection.Open();
|
||||||
|
// 数据库操作
|
||||||
|
}
|
||||||
|
catch (OleDbException ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"数据库错误: {ex.Message}");
|
||||||
|
// 记录日志
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"未知错误: {ex.Message}");
|
||||||
|
// 记录日志
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 完整示例项目
|
||||||
|
|
||||||
|
完整的 CRUD 操作已集成到 **Server 项目**中:
|
||||||
|
|
||||||
|
**文件位置**:
|
||||||
|
- UI: `Server/Views/AccessDatabaseWindowOleDb.xaml`
|
||||||
|
- 代码: `Server/Views/AccessDatabaseWindowOleDb.xaml.cs`
|
||||||
|
|
||||||
|
**功能列表**:
|
||||||
|
- ✅ 查询所有用户
|
||||||
|
- ✅ 添加新用户
|
||||||
|
- ✅ 修改用户密码
|
||||||
|
- ✅ 删除用户(含级联删除权限)
|
||||||
|
- ✅ 添加用户权限
|
||||||
|
- ✅ 删除用户权限
|
||||||
|
- ✅ 事务支持
|
||||||
|
- ✅ 异常处理
|
||||||
|
- ✅ 异步操作(不阻塞 UI)
|
||||||
|
|
||||||
|
**运行方式**:
|
||||||
|
1. 启动 Server 项目
|
||||||
|
2. 点击 "Access数据库" 按钮
|
||||||
|
3. 尽情测试增删改查功能!
|
||||||
|
|
||||||
|
数据库随意修改,不用担心!🎉
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
# Access 数据库访问方案对比
|
||||||
|
|
||||||
|
## 方案选择
|
||||||
|
|
||||||
|
### ✅ 方案 A:System.Data.OleDb(推荐)
|
||||||
|
|
||||||
|
**适用场景**:
|
||||||
|
- 简单的增删改查操作
|
||||||
|
- 性能要求高
|
||||||
|
- 需要稳定可靠的方案
|
||||||
|
- 不需要复杂的 LINQ 查询
|
||||||
|
|
||||||
|
**实现文件**:
|
||||||
|
- `Views/AccessDatabaseWindowOleDb.xaml`
|
||||||
|
- `Views/AccessDatabaseWindowOleDb.xaml.cs`
|
||||||
|
|
||||||
|
**示例代码**:
|
||||||
|
```csharp
|
||||||
|
// 1. 创建连接
|
||||||
|
var connectionString = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=dbDwDmsSetup.accdb";
|
||||||
|
using var connection = new OleDbConnection(connectionString);
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
// 2. 查询数据
|
||||||
|
using var command = new OleDbCommand("SELECT UserName, Password FROM UserInfo", connection);
|
||||||
|
using var reader = command.ExecuteReader();
|
||||||
|
while (reader.Read())
|
||||||
|
{
|
||||||
|
var userName = reader["UserName"]?.ToString();
|
||||||
|
var password = reader["Password"]?.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 插入数据
|
||||||
|
using var insertCmd = new OleDbCommand(
|
||||||
|
"INSERT INTO UserInfo (UserName, Password) VALUES (?, ?)", connection);
|
||||||
|
insertCmd.Parameters.AddWithValue("@UserName", "newuser");
|
||||||
|
insertCmd.Parameters.AddWithValue("@Password", "password");
|
||||||
|
insertCmd.ExecuteNonQuery();
|
||||||
|
|
||||||
|
// 4. 更新数据
|
||||||
|
using var updateCmd = new OleDbCommand(
|
||||||
|
"UPDATE UserInfo SET Password = ? WHERE UserName = ?", connection);
|
||||||
|
updateCmd.Parameters.AddWithValue("@Password", "newpassword");
|
||||||
|
updateCmd.Parameters.AddWithValue("@UserName", "username");
|
||||||
|
updateCmd.ExecuteNonQuery();
|
||||||
|
|
||||||
|
// 5. 删除数据
|
||||||
|
using var deleteCmd = new OleDbCommand(
|
||||||
|
"DELETE FROM UserInfo WHERE UserName = ?", connection);
|
||||||
|
deleteCmd.Parameters.AddWithValue("@UserName", "username");
|
||||||
|
deleteCmd.ExecuteNonQuery();
|
||||||
|
```
|
||||||
|
|
||||||
|
**优点**:
|
||||||
|
- ✅ 微软官方支持
|
||||||
|
- ✅ 无需额外 NuGet 包
|
||||||
|
- ✅ 性能最佳
|
||||||
|
- ✅ 稳定可靠
|
||||||
|
- ✅ 适用于所有 .NET 版本
|
||||||
|
|
||||||
|
**缺点**:
|
||||||
|
- ❌ 需要手写 SQL
|
||||||
|
- ❌ 没有 LINQ 支持
|
||||||
|
- ❌ 没有自动实体映射
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 方案 B:EntityFrameworkCore.Jet
|
||||||
|
|
||||||
|
**适用场景**:
|
||||||
|
- 需要复杂的 LINQ 查询
|
||||||
|
- 需要实体关系映射
|
||||||
|
- 代码优雅度要求高
|
||||||
|
- 可以接受第三方库
|
||||||
|
|
||||||
|
**实现文件**:
|
||||||
|
- `Views/AccessDatabaseWindow.xaml`
|
||||||
|
- `Views/AccessDatabaseWindow.xaml.cs`
|
||||||
|
- `Data/Access/AccessEntities.cs`
|
||||||
|
- `Data/Access/AccessDbContext.cs`
|
||||||
|
|
||||||
|
**示例代码**:
|
||||||
|
```csharp
|
||||||
|
// 1. 创建 DbContext
|
||||||
|
var optionsBuilder = new DbContextOptionsBuilder<AccessDbContext>();
|
||||||
|
optionsBuilder.UseJet("Provider=Microsoft.ACE.OLEDB.12.0;Data Source=dbDwDmsSetup.accdb");
|
||||||
|
using var dbContext = new AccessDbContext(optionsBuilder.Options);
|
||||||
|
|
||||||
|
// 2. LINQ 查询
|
||||||
|
var users = await dbContext.UserInfos
|
||||||
|
.Include(u => u.Permissions)
|
||||||
|
.Where(u => u.UserName.Contains("admin"))
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
// 3. 插入数据
|
||||||
|
var newUser = new UserInfo { UserName = "newuser", Password = "password" };
|
||||||
|
dbContext.UserInfos.Add(newUser);
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
// 4. 更新数据
|
||||||
|
var user = await dbContext.UserInfos.FindAsync("username");
|
||||||
|
if (user != null)
|
||||||
|
{
|
||||||
|
user.Password = "newpassword";
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 删除数据
|
||||||
|
var userToDelete = await dbContext.UserInfos.FindAsync("username");
|
||||||
|
if (userToDelete != null)
|
||||||
|
{
|
||||||
|
dbContext.UserInfos.Remove(userToDelete);
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**优点**:
|
||||||
|
- ✅ LINQ 查询支持
|
||||||
|
- ✅ 自动实体映射
|
||||||
|
- ✅ 代码更优雅
|
||||||
|
- ✅ 支持导航属性
|
||||||
|
|
||||||
|
**缺点**:
|
||||||
|
- ❌ 第三方库(EntityFrameworkCore.Jet)
|
||||||
|
- ❌ 版本兼容性问题
|
||||||
|
- ❌ 性能开销
|
||||||
|
- ❌ 可能与其他 EF Core 提供程序冲突
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 推荐方案
|
||||||
|
|
||||||
|
### 对于简单的增删改查:
|
||||||
|
|
||||||
|
**推荐使用方案 A(System.Data.OleDb)**
|
||||||
|
|
||||||
|
理由:
|
||||||
|
1. 稳定可靠,微软官方支持
|
||||||
|
2. 性能最佳
|
||||||
|
3. 无依赖问题
|
||||||
|
4. 代码简单直接
|
||||||
|
|
||||||
|
### 对于复杂的业务逻辑:
|
||||||
|
|
||||||
|
可以考虑方案 B(EntityFrameworkCore.Jet),但需要:
|
||||||
|
1. 接受第三方库的风险
|
||||||
|
2. 处理版本兼容性问题
|
||||||
|
3. 确保不与其他数据库提供程序冲突
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 当前 Server 项目使用的方案
|
||||||
|
|
||||||
|
当前使用:**方案 A(System.Data.OleDb)** ✅
|
||||||
|
|
||||||
|
主窗口按钮 "Access数据库" 打开的是 `AccessDatabaseWindowOleDb` 窗口。
|
||||||
|
|
||||||
|
如需切换到方案 B,修改 `MainWindow.xaml.cs` 中的 `AccessDbButton_Click` 方法:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 方案 A(当前)
|
||||||
|
var window = new AccessDatabaseWindowOleDb { Owner = this };
|
||||||
|
|
||||||
|
// 方案 B
|
||||||
|
var window = new AccessDatabaseWindow { Owner = this };
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 性能对比
|
||||||
|
|
||||||
|
| 操作 | OleDb | EF Core Jet |
|
||||||
|
|------|-------|-------------|
|
||||||
|
| 简单查询 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
|
||||||
|
| 复杂查询 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
|
||||||
|
| 插入操作 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
|
||||||
|
| 批量操作 | ⭐⭐⭐⭐⭐ | ⭐⭐ |
|
||||||
|
| 内存占用 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
对于你的需求(简单的增删改查),**强烈推荐使用 System.Data.OleDb(方案 A)**。
|
||||||
|
|
||||||
|
它是最稳定、性能最好、最简单的方案。
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Sharp.Data.Entities;
|
||||||
|
|
||||||
|
namespace Server.Data;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 示例用户实体 - 使用审计基类
|
||||||
|
/// </summary>
|
||||||
|
public class User : AuditableEntity
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 用户名
|
||||||
|
/// </summary>
|
||||||
|
public string UserName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 邮箱
|
||||||
|
/// </summary>
|
||||||
|
public string Email { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 年龄
|
||||||
|
/// </summary>
|
||||||
|
public int Age { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否激活
|
||||||
|
/// </summary>
|
||||||
|
public bool IsActive { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 备注
|
||||||
|
/// </summary>
|
||||||
|
public string? Remarks { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 应用数据库上下文
|
||||||
|
/// </summary>
|
||||||
|
public class AppDbContext : DbContext
|
||||||
|
{
|
||||||
|
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public DbSet<User> Users => Set<User>();
|
||||||
|
|
||||||
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
base.OnModelCreating(modelBuilder);
|
||||||
|
|
||||||
|
// 配置 User 实体
|
||||||
|
modelBuilder.Entity<User>(entity =>
|
||||||
|
{
|
||||||
|
entity.HasKey(e => e.Id);
|
||||||
|
entity.Property(e => e.UserName).IsRequired().HasMaxLength(50);
|
||||||
|
entity.Property(e => e.Email).IsRequired().HasMaxLength(100);
|
||||||
|
entity.Property(e => e.Remarks).HasMaxLength(500);
|
||||||
|
|
||||||
|
// 添加索引
|
||||||
|
entity.HasIndex(e => e.UserName);
|
||||||
|
entity.HasIndex(e => e.Email).IsUnique();
|
||||||
|
entity.HasIndex(e => e.IsActive);
|
||||||
|
entity.HasIndex(e => e.CreatedAt);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加示例数据
|
||||||
|
modelBuilder.Entity<User>().HasData(
|
||||||
|
new User
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
UserName = "admin",
|
||||||
|
Email = "admin@example.com",
|
||||||
|
Age = 30,
|
||||||
|
IsActive = true,
|
||||||
|
Remarks = "系统管理员",
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
CreatedBy = "system"
|
||||||
|
},
|
||||||
|
new User
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
UserName = "zhangsan",
|
||||||
|
Email = "zhangsan@example.com",
|
||||||
|
Age = 25,
|
||||||
|
IsActive = true,
|
||||||
|
Remarks = "测试用户1",
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
CreatedBy = "system"
|
||||||
|
},
|
||||||
|
new User
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
UserName = "lisi",
|
||||||
|
Email = "lisi@example.com",
|
||||||
|
Age = 28,
|
||||||
|
IsActive = true,
|
||||||
|
Remarks = "测试用户2",
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
CreatedBy = "system"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Sharp.NetServer;
|
||||||
|
|
||||||
|
namespace Server.Extensions;
|
||||||
|
|
||||||
|
internal static class HttpServerBuilderLoggingExtensions
|
||||||
|
{
|
||||||
|
public static HttpServerBuilder UseLoggerFactory(this HttpServerBuilder builder, ILoggerFactory loggerFactory)
|
||||||
|
{
|
||||||
|
builder.ConfigureLogging(services =>
|
||||||
|
{
|
||||||
|
services.AddSingleton(loggerFactory);
|
||||||
|
services.AddSingleton(typeof(ILogger<>), typeof(Logger<>));
|
||||||
|
});
|
||||||
|
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
<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="DataManagementButton" Content="数据管理" Width="100" Height="32" Margin="0,0,10,0" Click="DataManagementButton_Click"
|
||||||
|
Background="#673AB7" Foreground="White" BorderThickness="0" FontWeight="Bold" Cursor="Hand"/>
|
||||||
|
<Button x:Name="AccessDbButton" Content="Access数据库" Width="110" Height="32" Margin="0,0,10,0" Click="AccessDbButton_Click"
|
||||||
|
Background="#009688" Foreground="White" BorderThickness="0" FontWeight="Bold" Cursor="Hand"/>
|
||||||
|
<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: {"name":"test","value":"data"})"/>
|
||||||
|
<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>
|
||||||
|
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
using System.Windows;
|
||||||
|
using Server.Services;
|
||||||
|
using Server.Config;
|
||||||
|
using Server.Views;
|
||||||
|
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 DataManagementButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger?.LogInformation("打开数据管理窗口");
|
||||||
|
var window = new UserManagementWindow
|
||||||
|
{
|
||||||
|
Owner = this
|
||||||
|
};
|
||||||
|
window.ShowDialog();
|
||||||
|
_logger?.LogInformation("数据管理窗口已关闭");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger?.LogError(ex, "打开数据管理窗口失败");
|
||||||
|
AppendLog($"打开数据管理窗口失败: {ex.Message}");
|
||||||
|
MessageBox.Show($"打开数据管理窗口失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AccessDbButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger?.LogInformation("打开 Access 数据库管理窗口 (OleDb)");
|
||||||
|
var window = new AccessDatabaseWindowOleDb
|
||||||
|
{
|
||||||
|
Owner = this
|
||||||
|
};
|
||||||
|
window.ShowDialog();
|
||||||
|
_logger?.LogInformation("Access 数据库管理窗口已关闭");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger?.LogError(ex, "打开 Access 数据库管理窗口失败");
|
||||||
|
AppendLog($"打开 Access 数据库管理窗口失败: {ex.Message}");
|
||||||
|
MessageBox.Show($"打开 Access 数据库管理窗口失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>WinExe</OutputType>
|
||||||
|
<TargetFramework>net8.0-windows</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<UseWPF>true</UseWPF>
|
||||||
|
|
||||||
|
<!-- 强制使用 x64 架构,与已安装的 64 位 ACE 驱动匹配 -->
|
||||||
|
<PlatformTarget>x64</PlatformTarget>
|
||||||
|
|
||||||
|
<PublishSingleFile>true</PublishSingleFile>
|
||||||
|
<SelfContained>true</SelfContained>
|
||||||
|
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||||
|
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
|
||||||
|
<PublishTrimmed>false</PublishTrimmed>
|
||||||
|
|
||||||
|
</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" />
|
||||||
|
<ProjectReference Include="..\Data\Data.csproj" />
|
||||||
|
<ProjectReference Include="..\Data.Access\Data.Access.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>
|
||||||
|
<None Update="dbDwDmsSetup.accdb">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
Loaded 100 of 153 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user