A:增加数据库配置和使用
This commit is contained in:
1 parent
53d1dd2dbf
commit
f7660c49e8
86 files changed
+7401
-271
No files matched your search
@@ -1,9 +1,12 @@
|
||||
<Application x:Class="Client.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:Client"
|
||||
StartupUri="MainWindow.xaml">
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<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>
|
||||
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
using Client.Services;
|
||||
using Client.ViewModels;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Common;
|
||||
@@ -8,6 +10,7 @@ using NetClient.DependencyInjection;
|
||||
using Sharp.Infrastructure.Configuration;
|
||||
using Sharp.Log.Extensions;
|
||||
using Sharp.NetServer.DependencyInjection;
|
||||
using Sharp.Wpf.Services;
|
||||
|
||||
namespace Client
|
||||
{
|
||||
@@ -40,6 +43,15 @@ namespace Client
|
||||
services.AddNetServer(cm);
|
||||
|
||||
// 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();
|
||||
|
||||
// 3) Register global facades for convenience
|
||||
@@ -47,12 +59,19 @@ namespace Client
|
||||
Global.RegisterSingleton(cm);
|
||||
Global.RegisterSingleton(_serviceProvider.GetRequiredService<ISystemConfigAccessor>());
|
||||
|
||||
// 4) Show main window
|
||||
var main = new MainWindow
|
||||
// 4) Show login window first, then main shell if authenticated
|
||||
var loginWindow = _serviceProvider.GetRequiredService<LoginWindow>();
|
||||
var loginResult = loginWindow.ShowDialog();
|
||||
|
||||
if (loginResult == true)
|
||||
{
|
||||
DataContext = new ViewModels.MainViewModel()
|
||||
};
|
||||
main.Show();
|
||||
var main = _serviceProvider.GetRequiredService<MainWindow>();
|
||||
main.Show();
|
||||
}
|
||||
else
|
||||
{
|
||||
Shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnExit(ExitEventArgs e)
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.1" />
|
||||
<PackageReference Include="HandyControl" Version="3.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -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,29 @@
|
||||
using System;
|
||||
using HandyControl.Controls;
|
||||
using Client.ViewModels;
|
||||
|
||||
namespace Client;
|
||||
|
||||
public partial class LoginWindow : GlowWindow
|
||||
{
|
||||
private readonly LoginViewModel _viewModel;
|
||||
|
||||
public LoginWindow(LoginViewModel viewModel)
|
||||
{
|
||||
InitializeComponent();
|
||||
_viewModel = viewModel;
|
||||
DataContext = viewModel;
|
||||
_viewModel.LoginSucceeded += HandleLoginSucceeded;
|
||||
}
|
||||
|
||||
private void HandleLoginSucceeded(object? sender, EventArgs e)
|
||||
{
|
||||
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: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:Client"
|
||||
xmlns:b="clr-namespace:Sharp.Wpf.Behaviors;assembly=Sharp.Wpf"
|
||||
xmlns:hc="https://handyorg.github.io/handycontrol"
|
||||
xmlns:w="clr-namespace:Sharp.Wpf.Windowing;assembly=Sharp.Wpf"
|
||||
xmlns:conv="clr-namespace:Sharp.Wpf.Converters;assembly=Sharp.Wpf"
|
||||
mc:Ignorable="d"
|
||||
Title="MainWindow" Height="450" Width="800"
|
||||
Title="Sharp 控制台"
|
||||
Height="560"
|
||||
Width="960"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
w:WindowPlacement.RememberPlacement="True"
|
||||
w:WindowPlacement.PlacementId="MainWindow">
|
||||
<Window.Resources>
|
||||
<hc:GlowWindow.Resources>
|
||||
<conv:BoolToVisibilityConverter x:Key="Bool2Vis" />
|
||||
</Window.Resources>
|
||||
<Grid Margin="16">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</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>
|
||||
</hc:GlowWindow.Resources>
|
||||
<Grid Background="{DynamicResource RegionBrush}">
|
||||
<Grid Margin="24">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<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;
|
||||
|
||||
namespace Client
|
||||
namespace Client;
|
||||
|
||||
public partial class MainWindow : GlowWindow
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for MainWindow.xaml
|
||||
/// </summary>
|
||||
public partial class MainWindow : Window
|
||||
public MainWindow(MainViewModel viewModel)
|
||||
{
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContext = new MainViewModel();
|
||||
}
|
||||
InitializeComponent();
|
||||
DataContext = viewModel;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,89 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Client.Services;
|
||||
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 IAsyncCommand _loginCommand;
|
||||
|
||||
private string _userName = "admin";
|
||||
private string _password = string.Empty;
|
||||
private string? _errorMessage;
|
||||
|
||||
public event EventHandler? LoginSucceeded;
|
||||
|
||||
public LoginViewModel(IAuthenticationService authenticationService, IUserSession userSession)
|
||||
{
|
||||
_authenticationService = authenticationService;
|
||||
_userSession = userSession;
|
||||
_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
|
||||
{
|
||||
IsBusy = true;
|
||||
ErrorMessage = null;
|
||||
|
||||
var result = await _authenticationService.LoginAsync(UserName, Password).ConfigureAwait(false);
|
||||
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
ErrorMessage = result.Error;
|
||||
return;
|
||||
}
|
||||
|
||||
_userSession.SignIn(UserName, result.DisplayName);
|
||||
LoginSucceeded?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsBusy = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using Client.Services;
|
||||
using Sharp.Wpf.Commands;
|
||||
using Sharp.Wpf.Mvvm;
|
||||
using Sharp.Wpf.Services;
|
||||
@@ -9,35 +10,77 @@ namespace Client.ViewModels;
|
||||
|
||||
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
|
||||
{
|
||||
get => _greeting;
|
||||
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 RefreshDashboardCommand { get; }
|
||||
public ICommand ThrowCommand { get; }
|
||||
|
||||
public MainViewModel()
|
||||
private async Task ShowDialogAsync()
|
||||
{
|
||||
ShowDialogCommand = new AsyncCommand(async () =>
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
IsBusy = true;
|
||||
await Task.Delay(300);
|
||||
await _dialog.ShowInfoAsync("Hello from Sharp.Wpf DialogService!");
|
||||
Greeting = $"时间: {DateTime.Now:HH:mm:ss}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsBusy = false;
|
||||
}
|
||||
});
|
||||
IsBusy = true;
|
||||
await Task.Delay(300).ConfigureAwait(false);
|
||||
await _dialogService.ShowInfoAsync($"Hi {UserDisplayName}, 这是来自 Sharp.Wpf 的提醒!").ConfigureAwait(false);
|
||||
Greeting = $"问候更新时间: {DateTime.Now:HH:mm:ss}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
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>
|
||||
// This code was generated by a tool.
|
||||
@@ -8,7 +8,6 @@
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
using Client;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Windows;
|
||||
@@ -41,15 +40,22 @@ namespace Client {
|
||||
/// </summary>
|
||||
public partial class App : System.Windows.Application {
|
||||
|
||||
private bool _contentLoaded;
|
||||
|
||||
/// <summary>
|
||||
/// InitializeComponent
|
||||
/// </summary>
|
||||
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "9.0.4.0")]
|
||||
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"
|
||||
this.StartupUri = new System.Uri("MainWindow.xaml", System.UriKind.Relative);
|
||||
#line 1 "..\..\..\App.xaml"
|
||||
System.Windows.Application.LoadComponent(this, resourceLocater);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </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+53d1dd2dbf05d818528814223b58610a790fcb26")]
|
||||
[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 @@
|
||||
f8321da7c6a777fc7770c7234548af806317c95cd5f1beb1c1743888aa1bde11
|
||||
+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>
|
||||
// This code was generated by a tool.
|
||||
@@ -8,8 +8,18 @@
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
using Client;
|
||||
using Sharp.Wpf.Behaviors;
|
||||
using HandyControl.Controls;
|
||||
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.Windowing;
|
||||
using System;
|
||||
@@ -42,7 +52,7 @@ namespace Client {
|
||||
/// <summary>
|
||||
/// MainWindow
|
||||
/// </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;
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" 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="Serilog" Version="4.0.0" />
|
||||
|
||||
+15
-1
@@ -32,6 +32,21 @@ app.UseSharpRequestLogging(); // 可选:请求日志
|
||||
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`,即可获得:
|
||||
- Console 文本输出
|
||||
- 文件 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();
|
||||
```
|
||||
|
||||
## 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` 配置节(可省略)。
|
||||
```json
|
||||
{
|
||||
@@ -66,7 +83,7 @@ app.Run();
|
||||
- `SHARPLOG__FILE__PATH=/var/logs/app-.log`
|
||||
- `SHARPLOG__FILE__RETAINEDFILECOUNTLIMIT=7`
|
||||
|
||||
## 4. 常见定制
|
||||
## 5. 常见定制
|
||||
- 仅文件输出(关闭 Console):
|
||||
```csharp
|
||||
builder.Logging.AddSharpLog(o => { o.UseConsole = false; });
|
||||
@@ -84,17 +101,17 @@ app.Run();
|
||||
builder.Logging.AddSharpLog(o => { o.Enrich.Application = "MyApp"; });
|
||||
```
|
||||
|
||||
## 5. 与现有 Provider 协作
|
||||
## 6. 与现有 Provider 协作
|
||||
- 默认不清空 Provider;若希望仅用 Sharp.Log,可在应用中先执行 `ClearProviders()` 再调用 `AddSharpLog()`。
|
||||
- 如将 `UseSerilog` 设为 `false`,不会注入 Serilog Provider,请保留框架自带 Console Provider(不要 `ClearProviders()`)。
|
||||
|
||||
## 6. 请求日志中间件
|
||||
## 7. 请求日志中间件
|
||||
启用后会记录:请求开始、结束(状态码与耗时)、异常。
|
||||
```csharp
|
||||
app.UseSharpRequestLogging();
|
||||
```
|
||||
|
||||
## 7. 结构化与上下文
|
||||
## 8. 结构化与上下文
|
||||
日志条目自动包含:
|
||||
- `Application`、`Environment`、`MachineName`、`ProcessId`、`ThreadId`
|
||||
- `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);
|
||||
```
|
||||
|
||||
## 8. 测试建议
|
||||
## 9. 测试建议
|
||||
- 在测试中为文件输出设置临时目录:
|
||||
```csharp
|
||||
builder.Logging.AddSharpLog(o =>
|
||||
@@ -117,4 +134,3 @@ logger.LogInformation("User {UserId} logged in", userId);
|
||||
```
|
||||
|
||||
如需更多示例,请参考本仓库的测试项目 `test/Log.Test`。
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
<Application x:Class="Repository.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:Repository"
|
||||
StartupUri="MainWindow.xaml">
|
||||
<Application.Resources>
|
||||
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
@@ -1,14 +0,0 @@
|
||||
using System.Configuration;
|
||||
using System.Data;
|
||||
using System.Windows;
|
||||
|
||||
namespace Repository
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for App.xaml
|
||||
/// </summary>
|
||||
public partial class App : Application
|
||||
{
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
using System.Windows;
|
||||
|
||||
[assembly: ThemeInfo(
|
||||
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
|
||||
//(used if a resource is not found in the page,
|
||||
// or application resource dictionaries)
|
||||
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
|
||||
//(used if a resource is not found in the page,
|
||||
// app, or any theme specific resource dictionaries)
|
||||
)]
|
||||
@@ -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">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UseWPF>true</UseWPF>
|
||||
</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>
|
||||
@@ -1,11 +1,12 @@
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using System.Windows;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Config;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Sharp.Log;
|
||||
using Sharp.Data;
|
||||
using Sharp.Data.Context;
|
||||
using Server.Config;
|
||||
using Server.Services;
|
||||
using Server.Data;
|
||||
|
||||
namespace Server
|
||||
{
|
||||
@@ -18,6 +19,7 @@ namespace Server
|
||||
public AppConfig? AppConfig { get; private set; }
|
||||
public ILoggerFactory? LoggerFactory { get; private set; }
|
||||
public ServerService? ServerService { get; private set; }
|
||||
public IDataContext? DataContext { get; private set; }
|
||||
|
||||
protected override void OnStartup(StartupEventArgs e)
|
||||
{
|
||||
@@ -31,7 +33,10 @@ namespace Server
|
||||
// 2. 初始化日志系统
|
||||
InitializeLogging();
|
||||
|
||||
// 3. 初始化服务器服务
|
||||
// 3. 初始化数据库
|
||||
InitializeDatabase();
|
||||
|
||||
// 4. 初始化服务器服务
|
||||
InitializeServerService();
|
||||
|
||||
var logger = LoggerFactory?.CreateLogger<App>();
|
||||
@@ -57,37 +62,47 @@ namespace Server
|
||||
|
||||
private void InitializeLogging()
|
||||
{
|
||||
if (AppConfig == null)
|
||||
if (ConfigManager == null)
|
||||
{
|
||||
throw new InvalidOperationException("配置未初始化");
|
||||
}
|
||||
|
||||
// 确保日志目录存在
|
||||
var logDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, AppConfig.Logging.LogDirectory);
|
||||
if (!Directory.Exists(logDirectory))
|
||||
LoggerFactory = SharpLogDefaults.CreateLoggerFactory(
|
||||
configuration: ConfigManager.Configuration,
|
||||
configure: options =>
|
||||
{
|
||||
options.Enrich.Application ??= "SharpServer";
|
||||
});
|
||||
}
|
||||
|
||||
private void InitializeDatabase()
|
||||
{
|
||||
if (LoggerFactory == null)
|
||||
{
|
||||
Directory.CreateDirectory(logDirectory);
|
||||
throw new InvalidOperationException("日志系统未初始化");
|
||||
}
|
||||
|
||||
// 使用标准的Microsoft.Extensions.Logging配置
|
||||
var services = new Microsoft.Extensions.DependencyInjection.ServiceCollection();
|
||||
var logger = LoggerFactory.CreateLogger<App>();
|
||||
logger.LogInformation("正在初始化数据库...");
|
||||
|
||||
services.AddLogging(logging =>
|
||||
try
|
||||
{
|
||||
logging.SetMinimumLevel(ParseLogLevel(AppConfig.Logging.LogLevel));
|
||||
var builder = new DataContextBuilder<AppDbContext>();
|
||||
builder.UseSqlite("Data Source=server.db")
|
||||
.UseAudit(true)
|
||||
.UseSoftDelete(true)
|
||||
.UseCurrentUser("system")
|
||||
.EnableDetailedErrors(true);
|
||||
|
||||
if (AppConfig.Logging.LogToConsole)
|
||||
{
|
||||
logging.AddConsole();
|
||||
}
|
||||
DataContext = builder.Build();
|
||||
|
||||
// 如果需要文件日志,可以添加其他日志提供程序
|
||||
// 这里使用Sharp.Log的扩展方法
|
||||
// logging.AddSharpLog();
|
||||
});
|
||||
|
||||
var serviceProvider = services.BuildServiceProvider();
|
||||
LoggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
|
||||
logger.LogInformation("数据库初始化成功");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "数据库初始化失败");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeServerService()
|
||||
@@ -97,34 +112,15 @@ namespace Server
|
||||
throw new InvalidOperationException("配置或日志系统未初始化");
|
||||
}
|
||||
|
||||
var logger = LoggerFactory.CreateLogger<ServerService>();
|
||||
ServerService = new ServerService(AppConfig, logger);
|
||||
}
|
||||
|
||||
private Microsoft.Extensions.Logging.LogLevel ParseLogLevel(string level)
|
||||
{
|
||||
return level.ToLower() switch
|
||||
{
|
||||
"trace" => Microsoft.Extensions.Logging.LogLevel.Trace,
|
||||
"debug" => Microsoft.Extensions.Logging.LogLevel.Debug,
|
||||
"information" => Microsoft.Extensions.Logging.LogLevel.Information,
|
||||
"warning" => Microsoft.Extensions.Logging.LogLevel.Warning,
|
||||
"error" => Microsoft.Extensions.Logging.LogLevel.Error,
|
||||
"critical" => Microsoft.Extensions.Logging.LogLevel.Critical,
|
||||
"none" => Microsoft.Extensions.Logging.LogLevel.None,
|
||||
_ => Microsoft.Extensions.Logging.LogLevel.Information
|
||||
};
|
||||
ServerService = new ServerService(AppConfig, LoggerFactory);
|
||||
}
|
||||
|
||||
protected override void OnExit(ExitEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 停止服务器
|
||||
ServerService?.Dispose();
|
||||
|
||||
// 释放日志资源
|
||||
LoggerFactory?.Dispose();
|
||||
DataContext?.Dispose();
|
||||
|
||||
var logger = LoggerFactory?.CreateLogger<App>();
|
||||
logger?.LogInformation("应用程序正常退出");
|
||||
@@ -134,6 +130,10 @@ namespace Server
|
||||
// 忽略退出时的异常
|
||||
Console.WriteLine($"退出时出错: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
LoggerFactory?.Dispose();
|
||||
}
|
||||
|
||||
base.OnExit(e);
|
||||
}
|
||||
|
||||
@@ -17,32 +17,11 @@ namespace Server.Config
|
||||
public int MaxConcurrentRequests { get; set; } = 100;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 日志配置
|
||||
/// </summary>
|
||||
public class LoggingConfig
|
||||
{
|
||||
[Required]
|
||||
public string LogLevel { get; set; } = "Information";
|
||||
|
||||
[Required]
|
||||
public string LogDirectory { get; set; } = "logs";
|
||||
|
||||
[Range(1, 365)]
|
||||
public int MaxLogFiles { get; set; } = 30;
|
||||
|
||||
public bool LogToConsole { get; set; } = true;
|
||||
|
||||
public bool LogToFile { get; set; } = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 应用配置(根配置)
|
||||
/// </summary>
|
||||
public class AppConfig
|
||||
{
|
||||
public ServerConfig Server { get; set; } = new();
|
||||
|
||||
public LoggingConfig Logging { get; set; } = new();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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;
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,8 @@
|
||||
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="ClearLogButton" Content="清空日志" Width="100" Height="32" Click="ClearLogButton_Click"
|
||||
Background="#9E9E9E" Foreground="White" BorderThickness="0" FontWeight="Bold" Cursor="Hand"/>
|
||||
</StackPanel>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Windows;
|
||||
using Server.Services;
|
||||
using Server.Config;
|
||||
using Server.Views;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Server
|
||||
@@ -134,6 +135,26 @@ namespace Server
|
||||
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 OnServerStatusChanged(object? sender, string status)
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
|
||||
@@ -6,6 +6,13 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UseWPF>true</UseWPF>
|
||||
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
<SelfContained>true</SelfContained>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
|
||||
<PublishTrimmed>false</PublishTrimmed>
|
||||
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -14,6 +21,7 @@
|
||||
<ProjectReference Include="..\Config\Config.csproj" />
|
||||
<ProjectReference Include="..\Wpf\Wpf.csproj" />
|
||||
<ProjectReference Include="..\Common\Common.csproj" />
|
||||
<ProjectReference Include="..\Data\Data.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -4,6 +4,7 @@ using Sharp.NetServer;
|
||||
using Sharp.NetServer.Server;
|
||||
using Server.Config;
|
||||
using Server.Controllers;
|
||||
using Server.Extensions;
|
||||
|
||||
namespace Server.Services
|
||||
{
|
||||
@@ -16,16 +17,18 @@ namespace Server.Services
|
||||
private Task? _serverTask;
|
||||
private CancellationTokenSource? _cts;
|
||||
private readonly AppConfig _config;
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly ILogger<ServerService> _logger;
|
||||
|
||||
public bool IsRunning { get; private set; }
|
||||
|
||||
public event EventHandler<string>? StatusChanged;
|
||||
|
||||
public ServerService(AppConfig config, ILogger<ServerService> logger)
|
||||
public ServerService(AppConfig config, ILoggerFactory loggerFactory)
|
||||
{
|
||||
_config = config;
|
||||
_logger = logger;
|
||||
_loggerFactory = loggerFactory;
|
||||
_logger = loggerFactory.CreateLogger<ServerService>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -46,25 +49,12 @@ namespace Server.Services
|
||||
|
||||
var builder = new HttpServerBuilder()
|
||||
.UseUrls($"http://{_config.Server.Host}:{_config.Server.Port}/")
|
||||
.ConfigureLogging(services =>
|
||||
{
|
||||
services.AddLogging(logging =>
|
||||
{
|
||||
// 使用应用程序的日志配置
|
||||
logging.SetMinimumLevel(ParseLogLevel(_config.Logging.LogLevel));
|
||||
|
||||
if (_config.Logging.LogToConsole)
|
||||
{
|
||||
logging.AddConsole();
|
||||
}
|
||||
});
|
||||
})
|
||||
.UseLoggerFactory(_loggerFactory)
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
// 注册配置
|
||||
services.AddSingleton(_config);
|
||||
services.AddSingleton(_config.Server);
|
||||
services.AddSingleton(_config.Logging);
|
||||
})
|
||||
// 注册控制器(从当前程序集)
|
||||
.AddControllers<ServerService>();
|
||||
@@ -150,24 +140,6 @@ namespace Server.Services
|
||||
OnStatusChanged("服务器已重启");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析日志级别
|
||||
/// </summary>
|
||||
private LogLevel ParseLogLevel(string level)
|
||||
{
|
||||
return level.ToLower() switch
|
||||
{
|
||||
"trace" => LogLevel.Trace,
|
||||
"debug" => LogLevel.Debug,
|
||||
"information" => LogLevel.Information,
|
||||
"warning" => LogLevel.Warning,
|
||||
"error" => LogLevel.Error,
|
||||
"critical" => LogLevel.Critical,
|
||||
"none" => LogLevel.None,
|
||||
_ => LogLevel.Information
|
||||
};
|
||||
}
|
||||
|
||||
private void OnStatusChanged(string status)
|
||||
{
|
||||
StatusChanged?.Invoke(this, status);
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<Window x:Class="Server.Views.UserEditDialog"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="用户编辑" Height="450" Width="500"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
ResizeMode="NoResize">
|
||||
<Grid Margin="20">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 标题 -->
|
||||
<TextBlock Grid.Row="0" Text="用户信息" FontSize="18" FontWeight="Bold" Margin="0,0,0,20"/>
|
||||
|
||||
<!-- 表单 -->
|
||||
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel>
|
||||
<!-- 用户名 -->
|
||||
<TextBlock Text="用户名 *" FontWeight="Bold" Margin="0,0,0,5"/>
|
||||
<TextBox x:Name="UserNameTextBox" Height="32" Padding="8,0" Margin="0,0,0,15"
|
||||
BorderBrush="#BDBDBD" BorderThickness="1"/>
|
||||
|
||||
<!-- 邮箱 -->
|
||||
<TextBlock Text="邮箱 *" FontWeight="Bold" Margin="0,0,0,5"/>
|
||||
<TextBox x:Name="EmailTextBox" Height="32" Padding="8,0" Margin="0,0,0,15"
|
||||
BorderBrush="#BDBDBD" BorderThickness="1"/>
|
||||
|
||||
<!-- 年龄 -->
|
||||
<TextBlock Text="年龄 *" FontWeight="Bold" Margin="0,0,0,5"/>
|
||||
<TextBox x:Name="AgeTextBox" Height="32" Padding="8,0" Margin="0,0,0,15"
|
||||
BorderBrush="#BDBDBD" BorderThickness="1"/>
|
||||
|
||||
<!-- 激活状态 -->
|
||||
<CheckBox x:Name="IsActiveCheckBox" Content="激活" IsChecked="True" Margin="0,0,0,15"/>
|
||||
|
||||
<!-- 备注 -->
|
||||
<TextBlock Text="备注" FontWeight="Bold" Margin="0,0,0,5"/>
|
||||
<TextBox x:Name="RemarksTextBox" Height="80" Padding="8" Margin="0,0,0,15"
|
||||
TextWrapping="Wrap" AcceptsReturn="True"
|
||||
BorderBrush="#BDBDBD" BorderThickness="1"
|
||||
VerticalScrollBarVisibility="Auto"/>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- 按钮 -->
|
||||
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,20,0,0">
|
||||
<Button Content="保存" Width="100" Height="35" Margin="0,0,10,0" Click="SaveButton_Click"
|
||||
Background="#4CAF50" Foreground="White" BorderThickness="0" FontWeight="Bold" Cursor="Hand"/>
|
||||
<Button Content="取消" Width="100" Height="35" Click="CancelButton_Click"
|
||||
Background="#9E9E9E" Foreground="White" BorderThickness="0" FontWeight="Bold" Cursor="Hand"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,89 @@
|
||||
using System.Windows;
|
||||
using Server.Data;
|
||||
|
||||
namespace Server.Views;
|
||||
|
||||
public partial class UserEditDialog : Window
|
||||
{
|
||||
public User UserData { get; private set; }
|
||||
private readonly bool _isEditMode;
|
||||
|
||||
public UserEditDialog(User? existingUser = null)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
_isEditMode = existingUser != null;
|
||||
Title = _isEditMode ? "编辑用户" : "新增用户";
|
||||
|
||||
if (existingUser != null)
|
||||
{
|
||||
UserData = existingUser;
|
||||
LoadUserData(existingUser);
|
||||
}
|
||||
else
|
||||
{
|
||||
UserData = new User
|
||||
{
|
||||
IsActive = true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadUserData(User user)
|
||||
{
|
||||
UserNameTextBox.Text = user.UserName;
|
||||
EmailTextBox.Text = user.Email;
|
||||
AgeTextBox.Text = user.Age.ToString();
|
||||
IsActiveCheckBox.IsChecked = user.IsActive;
|
||||
RemarksTextBox.Text = user.Remarks;
|
||||
}
|
||||
|
||||
private void SaveButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// 验证
|
||||
if (string.IsNullOrWhiteSpace(UserNameTextBox.Text))
|
||||
{
|
||||
MessageBox.Show("请输入用户名", "验证失败", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
UserNameTextBox.Focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(EmailTextBox.Text))
|
||||
{
|
||||
MessageBox.Show("请输入邮箱", "验证失败", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
EmailTextBox.Focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!int.TryParse(AgeTextBox.Text, out int age) || age < 0 || age > 150)
|
||||
{
|
||||
MessageBox.Show("请输入有效的年龄(0-150)", "验证失败", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
AgeTextBox.Focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// 简单的邮箱格式验证
|
||||
if (!EmailTextBox.Text.Contains("@"))
|
||||
{
|
||||
MessageBox.Show("请输入有效的邮箱地址", "验证失败", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
EmailTextBox.Focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新数据
|
||||
UserData.UserName = UserNameTextBox.Text.Trim();
|
||||
UserData.Email = EmailTextBox.Text.Trim();
|
||||
UserData.Age = age;
|
||||
UserData.IsActive = IsActiveCheckBox.IsChecked == true;
|
||||
UserData.Remarks = RemarksTextBox.Text.Trim();
|
||||
|
||||
DialogResult = true;
|
||||
Close();
|
||||
}
|
||||
|
||||
private void CancelButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
DialogResult = false;
|
||||
Close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
<Window x:Class="Server.Views.UserManagementWindow"
|
||||
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"
|
||||
mc:Ignorable="d"
|
||||
Title="用户数据管理 - Sharp.Data 示例" Height="700" Width="1200"
|
||||
WindowStartupLocation="CenterScreen">
|
||||
<Grid Margin="15">
|
||||
<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,15">
|
||||
<StackPanel>
|
||||
<TextBlock Text="用户数据管理" FontSize="20" FontWeight="Bold" Foreground="White"/>
|
||||
<TextBlock Text="Sharp.Data 模块集成示例 - SQLite 数据库 + 软删除" 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,15">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- 搜索框 -->
|
||||
<StackPanel Grid.Column="0" Orientation="Horizontal" Margin="0,0,15,0">
|
||||
<TextBlock Text="搜索:" VerticalAlignment="Center" Margin="0,0,10,0"/>
|
||||
<TextBox x:Name="SearchTextBox" Width="250" Height="32" VerticalContentAlignment="Center"
|
||||
Padding="8,0" BorderBrush="#BDBDBD" BorderThickness="1"/>
|
||||
<Button Content="搜索" Width="80" Height="32" Margin="10,0,0,0" Click="SearchButton_Click"
|
||||
Background="#2196F3" Foreground="White" BorderThickness="0" FontWeight="Bold" Cursor="Hand"/>
|
||||
<Button Content="清空" Width="80" Height="32" Margin="5,0,0,0" Click="ClearSearchButton_Click"
|
||||
Background="#9E9E9E" Foreground="White" BorderThickness="0" FontWeight="Bold" Cursor="Hand"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 状态信息 -->
|
||||
<TextBlock Grid.Column="1" x:Name="StatusTextBlock" Text="就绪" VerticalAlignment="Center"
|
||||
Foreground="#666" Margin="15,0"/>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal">
|
||||
<Button Content="➕ 新增" Width="90" Height="32" Margin="0,0,5,0" Click="AddButton_Click"
|
||||
Background="#4CAF50" Foreground="White" BorderThickness="0" FontWeight="Bold" Cursor="Hand"/>
|
||||
<Button Content="✏️ 编辑" Width="90" Height="32" Margin="0,0,5,0" Click="EditButton_Click"
|
||||
Background="#FF9800" Foreground="White" BorderThickness="0" FontWeight="Bold" Cursor="Hand"/>
|
||||
<Button Content="🗑️ 删除" Width="90" Height="32" Click="DeleteButton_Click"
|
||||
Background="#F44336" Foreground="White" BorderThickness="0" FontWeight="Bold" Cursor="Hand"/>
|
||||
<Button Content="🔄 刷新" Width="90" Height="32" Margin="5,0,0,0" Click="RefreshButton_Click"
|
||||
Background="#607D8B" Foreground="White" BorderThickness="0" FontWeight="Bold" Cursor="Hand"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- 数据列表 -->
|
||||
<Border Grid.Row="2" BorderBrush="#E0E0E0" BorderThickness="1" CornerRadius="5" Padding="10">
|
||||
<DataGrid x:Name="UsersDataGrid"
|
||||
AutoGenerateColumns="False"
|
||||
IsReadOnly="True"
|
||||
SelectionMode="Single"
|
||||
GridLinesVisibility="None"
|
||||
HeadersVisibility="Column"
|
||||
AlternatingRowBackground="#F5F5F5"
|
||||
RowHeight="40"
|
||||
CanUserAddRows="False"
|
||||
CanUserDeleteRows="False">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="ID" Binding="{Binding Id}" Width="80" Visibility="Collapsed"/>
|
||||
<DataGridTextColumn Header="用户名" Binding="{Binding UserName}" Width="150"/>
|
||||
<DataGridTextColumn Header="邮箱" Binding="{Binding Email}" Width="200"/>
|
||||
<DataGridTextColumn Header="年龄" Binding="{Binding Age}" Width="80"/>
|
||||
<DataGridCheckBoxColumn Header="激活" Binding="{Binding IsActive}" Width="80"/>
|
||||
<DataGridTextColumn Header="备注" Binding="{Binding Remarks}" Width="*"/>
|
||||
<DataGridTextColumn Header="创建时间" Binding="{Binding CreatedAt, StringFormat={}{0:yyyy-MM-dd HH:mm}}" Width="150"/>
|
||||
<DataGridTextColumn Header="创建人" Binding="{Binding CreatedBy}" Width="100"/>
|
||||
<DataGridTextColumn Header="修改时间" Binding="{Binding UpdatedAt, StringFormat={}{0:yyyy-MM-dd HH:mm}}" Width="150"/>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</Border>
|
||||
|
||||
<!-- 底部信息栏 -->
|
||||
<Border Grid.Row="3" Background="#E3F2FD" Padding="15" CornerRadius="5" Margin="0,15,0,0">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<StackPanel Grid.Column="0">
|
||||
<TextBlock FontWeight="Bold" Margin="0,0,0,5">
|
||||
<Run Text="💡 功能说明:"/>
|
||||
</TextBlock>
|
||||
<TextBlock FontSize="11" TextWrapping="Wrap">
|
||||
<Run Text="• 本示例展示 Sharp.Data 模块的使用:SQLite 数据库、仓储模式、自动审计、软删除"/>
|
||||
<LineBreak/>
|
||||
<Run Text="• 删除操作使用软删除(IsDeleted=true),数据不会真正删除"/>
|
||||
<LineBreak/>
|
||||
<Run Text="• 创建/修改时间和操作人由 Data 模块自动记录"/>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center">
|
||||
<TextBlock Text="数据库: SQLite (server.db)" FontSize="11" FontWeight="Bold"/>
|
||||
<TextBlock x:Name="RecordCountTextBlock" Text="记录数: 0" FontSize="11"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,202 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using Sharp.Data.Context;
|
||||
using Server.Data;
|
||||
using Common.ExpandFun;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Server.Views;
|
||||
|
||||
public partial class UserManagementWindow : Window
|
||||
{
|
||||
private readonly IDataContext _dataContext;
|
||||
private readonly ILogger<UserManagementWindow> _logger;
|
||||
|
||||
public UserManagementWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
var app = (App)Application.Current;
|
||||
_dataContext = app.DataContext ?? throw new InvalidOperationException("数据库未初始化");
|
||||
_logger = app.LoggerFactory?.CreateLogger<UserManagementWindow>()
|
||||
?? throw new InvalidOperationException("日志系统未初始化");
|
||||
|
||||
Loaded += UserManagementWindow_Loaded;
|
||||
}
|
||||
|
||||
private async void UserManagementWindow_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
await LoadDataAsync();
|
||||
}
|
||||
|
||||
private async Task LoadDataAsync(string? searchText = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
StatusTextBlock.Text = "正在加载数据...";
|
||||
_logger.LogInformation("开始加载用户数据,搜索条件: {SearchText}", searchText ?? "无");
|
||||
|
||||
var repo = _dataContext.Repository<User>();
|
||||
|
||||
List<User> users;
|
||||
if (string.IsNullOrWhiteSpace(searchText))
|
||||
{
|
||||
users = await repo.GetAllAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
users = await repo.FindAsync(u =>
|
||||
u.UserName.Contains(searchText) ||
|
||||
u.Email.Contains(searchText) ||
|
||||
(u.Remarks != null && u.Remarks.Contains(searchText)));
|
||||
}
|
||||
|
||||
UsersDataGrid.ItemsSource = users.OrderByDescending(u => u.CreatedAt).ToList();
|
||||
RecordCountTextBlock.Text = $"记录数: {users.Count}";
|
||||
StatusTextBlock.Text = string.IsNullOrWhiteSpace(searchText)
|
||||
? $"加载完成,共 {users.Count} 条记录"
|
||||
: $"搜索完成,找到 {users.Count} 条记录";
|
||||
|
||||
_logger.LogInformation("加载完成,共 {Count} 条记录", users.Count);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "加载数据失败");
|
||||
StatusTextBlock.Text = "加载失败";
|
||||
MessageBox.Show($"加载数据失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async void SearchButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var searchText = SearchTextBox.Text.Trim();
|
||||
await LoadDataAsync(searchText);
|
||||
}
|
||||
|
||||
private async void ClearSearchButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
SearchTextBox.Clear();
|
||||
await LoadDataAsync();
|
||||
}
|
||||
|
||||
private void AddButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var dialog = new UserEditDialog();
|
||||
if (dialog.ShowDialog() == true)
|
||||
{
|
||||
_ = SaveUserAsync(dialog.UserData);
|
||||
}
|
||||
}
|
||||
|
||||
private void EditButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (UsersDataGrid.SelectedItem is not User selectedUser)
|
||||
{
|
||||
MessageBox.Show("请选择要编辑的用户", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
var dialog = new UserEditDialog(selectedUser);
|
||||
if (dialog.ShowDialog() == true)
|
||||
{
|
||||
_ = UpdateUserAsync(dialog.UserData);
|
||||
}
|
||||
}
|
||||
|
||||
private async void DeleteButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (UsersDataGrid.SelectedItem is not User selectedUser)
|
||||
{
|
||||
MessageBox.Show("请选择要删除的用户", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
var result = MessageBox.Show(
|
||||
$"确定要删除用户 '{selectedUser.UserName}' 吗?\n\n(注意:这是软删除,数据会被标记为已删除但不会真正删除)",
|
||||
"确认删除",
|
||||
MessageBoxButton.YesNo,
|
||||
MessageBoxImage.Question);
|
||||
|
||||
if (result == MessageBoxResult.Yes)
|
||||
{
|
||||
await DeleteUserAsync(selectedUser.Id);
|
||||
}
|
||||
}
|
||||
|
||||
private async void RefreshButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
await LoadDataAsync(SearchTextBox.Text.Trim());
|
||||
}
|
||||
|
||||
private async Task SaveUserAsync(User user)
|
||||
{
|
||||
try
|
||||
{
|
||||
StatusTextBlock.Text = "正在保存...";
|
||||
_logger.LogInformation("新增用户: {UserName}", user.UserName);
|
||||
|
||||
var repo = _dataContext.Repository<User>();
|
||||
await repo.AddAsync(user);
|
||||
await _dataContext.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation("用户新增成功: {UserId}", user.Id);
|
||||
MessageBox.Show("用户新增成功!", "成功", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
|
||||
await LoadDataAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "新增用户失败");
|
||||
StatusTextBlock.Text = "保存失败";
|
||||
MessageBox.Show($"新增用户失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateUserAsync(User user)
|
||||
{
|
||||
try
|
||||
{
|
||||
StatusTextBlock.Text = "正在更新...";
|
||||
_logger.LogInformation("更新用户: {UserId}", user.Id);
|
||||
|
||||
var repo = _dataContext.Repository<User>();
|
||||
repo.Update(user);
|
||||
await _dataContext.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation("用户更新成功: {UserId}", user.Id);
|
||||
MessageBox.Show("用户更新成功!", "成功", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
|
||||
await LoadDataAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "更新用户失败");
|
||||
StatusTextBlock.Text = "更新失败";
|
||||
MessageBox.Show($"更新用户失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteUserAsync(Guid userId)
|
||||
{
|
||||
try
|
||||
{
|
||||
StatusTextBlock.Text = "正在删除...";
|
||||
_logger.LogInformation("软删除用户: {UserId}", userId);
|
||||
|
||||
var repo = _dataContext.Repository<User>();
|
||||
await repo.RemoveAsync(userId); // 软删除
|
||||
await _dataContext.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation("用户软删除成功: {UserId}", userId);
|
||||
MessageBox.Show("用户已标记为删除(软删除)!", "成功", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
|
||||
await LoadDataAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "删除用户失败");
|
||||
StatusTextBlock.Text = "删除失败";
|
||||
MessageBox.Show($"删除用户失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,19 @@
|
||||
"Host": "localhost",
|
||||
"MaxConcurrentRequests": 100
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": "Information",
|
||||
"LogDirectory": "logs",
|
||||
"MaxLogFiles": 30,
|
||||
"LogToConsole": true,
|
||||
"LogToFile": true
|
||||
"SharpLog": {
|
||||
"UseConsole": true,
|
||||
"MinLevel": "Information",
|
||||
"File": {
|
||||
"Enabled": true,
|
||||
"Path": "logs/server-.log",
|
||||
"Rolling": "Day",
|
||||
"RetainedFileCountLimit": 30,
|
||||
"Json": true
|
||||
},
|
||||
"Enrich": {
|
||||
"Application": "SharpServer",
|
||||
"WithEnvironmentName": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Config;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
|
||||
namespace Sharp.Infrastructure.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// 官方示例:系统级通用配置。可由项目复制或修改,Config 模块本身不再内置业务模型。
|
||||
/// 包含 HTTP 访问所需的基础字段,可被 NetClient/NetServer 等模块复用。
|
||||
/// </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";
|
||||
}
|
||||
|
||||
public interface ISystemConfigAccessor
|
||||
{
|
||||
SystemConfig Value { get; }
|
||||
}
|
||||
|
||||
internal sealed class SystemConfigAccessor : ISystemConfigAccessor, IDisposable
|
||||
{
|
||||
private readonly ConfigManager _configManager;
|
||||
private readonly IDisposable _subscription;
|
||||
private readonly string _section;
|
||||
private readonly bool _validate;
|
||||
private SystemConfig _current;
|
||||
|
||||
public SystemConfigAccessor(ConfigManager configManager, string section, bool validate)
|
||||
{
|
||||
_configManager = configManager;
|
||||
_section = section;
|
||||
_validate = validate;
|
||||
_current = configManager.Get<SystemConfig>(_section, _validate);
|
||||
_subscription = configManager.OnReload(() =>
|
||||
{
|
||||
_current = _configManager.Get<SystemConfig>(_section, _validate);
|
||||
});
|
||||
}
|
||||
|
||||
public SystemConfig Value => _current;
|
||||
|
||||
public void Dispose() => _subscription.Dispose();
|
||||
}
|
||||
|
||||
public static class SystemConfigServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers SystemConfig accessor backed by ConfigManager under the specified section (default "System").
|
||||
/// </summary>
|
||||
public static IServiceCollection AddSystemConfig(this IServiceCollection services, ConfigManager configManager, string section = "System", bool validate = false)
|
||||
{
|
||||
services.TryAddSingleton(configManager);
|
||||
services.TryAddSingleton<ISystemConfigAccessor>(_ => new SystemConfigAccessor(configManager, section, validate));
|
||||
services.TryAddSingleton(sp => sp.GetRequiredService<ISystemConfigAccessor>().Value);
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetServer.Test", "..\..\tes
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Server", "Server\Server.csproj", "{C2505AD1-D775-4881-A56F-42BDB7CDE58D}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Data", "Data\Data.csproj", "{D4FAD87A-D436-4F66-8A7F-B57DD130E966}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Data.Test", "..\..\test\Data.Test\Data.Test.csproj", "{8EEC8BF4-CF9A-473D-B7A0-005E151C645D}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -105,6 +109,14 @@ Global
|
||||
{C2505AD1-D775-4881-A56F-42BDB7CDE58D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C2505AD1-D775-4881-A56F-42BDB7CDE58D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C2505AD1-D775-4881-A56F-42BDB7CDE58D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{D4FAD87A-D436-4F66-8A7F-B57DD130E966}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D4FAD87A-D436-4F66-8A7F-B57DD130E966}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D4FAD87A-D436-4F66-8A7F-B57DD130E966}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D4FAD87A-D436-4F66-8A7F-B57DD130E966}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{8EEC8BF4-CF9A-473D-B7A0-005E151C645D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{8EEC8BF4-CF9A-473D-B7A0-005E151C645D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8EEC8BF4-CF9A-473D-B7A0-005E151C645D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8EEC8BF4-CF9A-473D-B7A0-005E151C645D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
@@ -55,6 +55,23 @@ public sealed class AsyncCommand : IAsyncCommand
|
||||
}
|
||||
|
||||
public event EventHandler? CanExecuteChanged;
|
||||
public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
public void RaiseCanExecuteChanged()
|
||||
{
|
||||
var handler = CanExecuteChanged;
|
||||
if (handler == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var dispatcher = System.Windows.Application.Current?.Dispatcher;
|
||||
if (dispatcher != null && !dispatcher.CheckAccess())
|
||||
{
|
||||
dispatcher.Invoke(() => handler(this, EventArgs.Empty));
|
||||
}
|
||||
else
|
||||
{
|
||||
handler(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
// 此代码由工具生成。
|
||||
// 运行时版本:4.0.30319.42000
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// 对此文件的更改可能会导致不正确的行为,并且如果
|
||||
// 重新生成代码,这些更改将会丢失。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
@@ -13,7 +14,7 @@ using System.Reflection;
|
||||
[assembly: System.Reflection.AssemblyCompanyAttribute("Sharp.Wpf")]
|
||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+b14721d79ee49603fd8600872fa22c984017bb5b")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+53d1dd2dbf05d818528814223b58610a790fcb26")]
|
||||
[assembly: System.Reflection.AssemblyProductAttribute("Sharp.Wpf")]
|
||||
[assembly: System.Reflection.AssemblyTitleAttribute("Sharp.Wpf")]
|
||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||
|
||||
@@ -1 +1 @@
|
||||
9709e2b151cbd6395e6164fc9a4c93fdc4813fdcdf520d95d8fe511f945f3f99
|
||||
fbdba11393faed768ef4c9b8f4eccef99aed87397a79576b900953e4aad05633
|
||||
@@ -70,6 +70,11 @@
|
||||
},
|
||||
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.203/PortableRuntimeIdentifierGraph.json"
|
||||
}
|
||||
},
|
||||
"runtimes": {
|
||||
"win-x64": {
|
||||
"#import": []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"version": 3,
|
||||
"targets": {
|
||||
"net8.0-windows7.0": {}
|
||||
"net8.0-windows7.0": {},
|
||||
"net8.0-windows7.0/win-x64": {}
|
||||
},
|
||||
"libraries": {},
|
||||
"projectFileDependencyGroups": {
|
||||
@@ -77,6 +78,11 @@
|
||||
},
|
||||
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.203/PortableRuntimeIdentifierGraph.json"
|
||||
}
|
||||
},
|
||||
"runtimes": {
|
||||
"win-x64": {
|
||||
"#import": []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dgSpecHash": "EQ+I1wXrehg=",
|
||||
"dgSpecHash": "9ZityDkLrvk=",
|
||||
"success": true,
|
||||
"projectFilePath": "D:\\yuanll\\sharp-basic\\src\\Sharp\\Wpf\\Wpf.csproj",
|
||||
"expectedPackageFiles": [],
|
||||
|
||||
@@ -1 +1 @@
|
||||
17630299672028072
|
||||
17634268840015055
|
||||
@@ -1 +1 @@
|
||||
17633497485167484
|
||||
17633637013370903
|
||||
@@ -0,0 +1,184 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Sharp.Data;
|
||||
|
||||
namespace Data.Test;
|
||||
|
||||
/// <summary>
|
||||
/// 快速开始 - 最简单的演示
|
||||
///
|
||||
/// 如果您是第一次使用 Sharp.Data,从这里开始!
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class _00_QuickStart
|
||||
{
|
||||
/// <summary>
|
||||
/// 演示: 3 行代码开始使用 Sharp.Data
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task QuickStart_ThreeLines()
|
||||
{
|
||||
// 第1行:创建构建器
|
||||
var builder = new DataContextBuilder<TestDbContext>();
|
||||
|
||||
// 第2行:配置数据库
|
||||
builder.UseInMemoryDatabase("QuickStartDemo");
|
||||
|
||||
// 第3行:构建并使用
|
||||
using var db = builder.Build();
|
||||
|
||||
// 完成!现在可以使用数据库了
|
||||
var productRepo = db.Repository<Product>();
|
||||
|
||||
// 添加产品
|
||||
await productRepo.AddAsync(new Product
|
||||
{
|
||||
Name = "iPhone 15",
|
||||
Price = 7999m,
|
||||
Stock = 100
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
// 查询产品
|
||||
var products = await productRepo.GetAllAsync();
|
||||
|
||||
Assert.AreEqual(1, products.Count);
|
||||
Console.WriteLine($"✓ 成功添加并查询产品: {products[0].Name}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 启用审计和软删除
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task QuickStart_WithAuditAndSoftDelete()
|
||||
{
|
||||
var builder = new DataContextBuilder<TestDbContext>();
|
||||
builder.UseInMemoryDatabase($"AuditDemo_{Guid.NewGuid()}")
|
||||
.UseAudit(true) // 启用审计
|
||||
.UseSoftDelete(true) // 启用软删除
|
||||
.UseCurrentUser("admin"); // 设置当前用户
|
||||
|
||||
using var db = builder.Build();
|
||||
var repo = db.Repository<Product>();
|
||||
|
||||
// 添加产品(自动记录创建信息)
|
||||
var product = new Product { Name = "测试产品", Price = 99m, Stock = 10 };
|
||||
await repo.AddAsync(product);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine($"✓ 产品已创建");
|
||||
Console.WriteLine($" CreatedBy: {product.CreatedBy}");
|
||||
Console.WriteLine($" CreatedAt: {product.CreatedAt:yyyy-MM-dd HH:mm:ss}");
|
||||
|
||||
// 软删除(不会真正删除)
|
||||
await repo.RemoveAsync(product.Id);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine($"\n✓ 产品已软删除");
|
||||
Console.WriteLine($" IsDeleted: {product.IsDeleted}");
|
||||
Console.WriteLine($" DeletedAt: {product.DeletedAt:yyyy-MM-dd HH:mm:ss}");
|
||||
|
||||
// 验证:正常查询看不到已删除的
|
||||
var found = await repo.GetByIdAsync(product.Id);
|
||||
Assert.IsNull(found);
|
||||
Console.WriteLine($"\n✓ 正常查询返回 null (已删除的数据被自动过滤)");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 分页查询
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task QuickStart_Pagination()
|
||||
{
|
||||
var builder = new DataContextBuilder<TestDbContext>();
|
||||
builder.UseInMemoryDatabase($"PageDemo_{Guid.NewGuid()}");
|
||||
|
||||
using var db = builder.Build();
|
||||
var repo = db.Repository<Product>();
|
||||
|
||||
// 添加测试数据
|
||||
for (int i = 1; i <= 25; i++)
|
||||
{
|
||||
await repo.AddAsync(new Product
|
||||
{
|
||||
Name = $"产品{i}",
|
||||
Price = i * 10m,
|
||||
Stock = i
|
||||
});
|
||||
}
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
// 分页查询:第2页,每页10条
|
||||
var result = await repo.GetPagedAsync(2, 10);
|
||||
|
||||
Console.WriteLine($"分页查询结果:");
|
||||
Console.WriteLine($" 当前页: {result.PageIndex}");
|
||||
Console.WriteLine($" 每页数量: {result.PageSize}");
|
||||
Console.WriteLine($" 总记录数: {result.TotalCount}");
|
||||
Console.WriteLine($" 总页数: {result.TotalPages}");
|
||||
Console.WriteLine($" 当前页产品数: {result.Items.Count}");
|
||||
|
||||
Assert.AreEqual(2, result.PageIndex);
|
||||
Assert.AreEqual(25, result.TotalCount);
|
||||
Assert.AreEqual(3, result.TotalPages);
|
||||
Assert.AreEqual(10, result.Items.Count);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 使用自定义实体(不使用基类)
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task QuickStart_CustomEntity()
|
||||
{
|
||||
var builder = new DataContextBuilder<TestDbContext>();
|
||||
builder.UseInMemoryDatabase($"CustomDemo_{Guid.NewGuid()}");
|
||||
|
||||
using var db = builder.Build();
|
||||
var repo = db.Repository<CustomProduct>();
|
||||
|
||||
// CustomProduct 不使用任何基类,完全自定义
|
||||
var product = new CustomProduct
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = "自定义产品",
|
||||
Price = 299.99m,
|
||||
CreatedDate = DateTime.UtcNow
|
||||
};
|
||||
|
||||
await repo.AddAsync(product);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var found = await repo.GetByIdAsync(product.Id);
|
||||
Assert.IsNotNull(found);
|
||||
|
||||
Console.WriteLine($"✓ 自定义实体也能正常工作:");
|
||||
Console.WriteLine($" Name: {found.Name}");
|
||||
Console.WriteLine($" Price: {found.Price:C}");
|
||||
Console.WriteLine($" IsExpensive: {found.IsExpensive}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 直接使用 EF Core
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task QuickStart_DirectEFCore()
|
||||
{
|
||||
var builder = new DataContextBuilder<TestDbContext>();
|
||||
builder.UseInMemoryDatabase($"DirectDemo_{Guid.NewGuid()}");
|
||||
|
||||
using var db = builder.Build();
|
||||
|
||||
// 可以直接访问 DbContext
|
||||
var context = db.Context;
|
||||
var products = context.Set<Product>();
|
||||
|
||||
// 使用 EF Core 原生 API
|
||||
products.Add(new Product { Name = "EF Core 产品", Price = 199m, Stock = 50 });
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
// 使用 LINQ 查询
|
||||
var count = await products.CountAsync();
|
||||
|
||||
Assert.AreEqual(1, count);
|
||||
Console.WriteLine($"✓ 可以直接使用 EF Core,完全兼容!");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
using Sharp.Data;
|
||||
using Sharp.Data.Context;
|
||||
|
||||
namespace Data.Test;
|
||||
|
||||
/// <summary>
|
||||
/// 演示 01: Sharp.Data 基础使用
|
||||
///
|
||||
/// 本测试类演示:
|
||||
/// 1. 如何创建和配置 DataContext
|
||||
/// 2. 最简单的 3 行代码启动方式
|
||||
/// 3. 各种配置选项的使用
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class _01_BasicUsageTests
|
||||
{
|
||||
/// <summary>
|
||||
/// 演示: 最简单的使用方式 (3 行代码)
|
||||
///
|
||||
/// 这是最基础的用法,适合快速开始
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void Demo_MinimalSetup_ThreeLines()
|
||||
{
|
||||
// 第 1 行: 创建构建器
|
||||
var builder = new DataContextBuilder<TestDbContext>();
|
||||
|
||||
// 第 2 行: 配置数据库 (使用内存数据库用于演示)
|
||||
builder.UseInMemoryDatabase("MinimalDemo");
|
||||
|
||||
// 第 3 行: 构建 DataContext
|
||||
using var db = builder.Build();
|
||||
|
||||
// 完成!现在可以使用了
|
||||
Assert.IsNotNull(db);
|
||||
Assert.IsNotNull(db.Context);
|
||||
|
||||
Console.WriteLine("✓ 3 行代码完成 Data 模块配置");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 使用 SQLite 数据库
|
||||
///
|
||||
/// SQLite 是最常用的轻量级数据库,适合桌面应用
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void Demo_UsingSQLite()
|
||||
{
|
||||
var builder = new DataContextBuilder<TestDbContext>();
|
||||
|
||||
// 配置 SQLite 数据库
|
||||
builder.UseSqlite("Data Source=test.db");
|
||||
|
||||
using var db = builder.Build();
|
||||
|
||||
Assert.IsNotNull(db);
|
||||
Console.WriteLine("✓ 使用 SQLite 数据库配置成功");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 启用所有功能的完整配置
|
||||
///
|
||||
/// 演示如何配置审计、软删除、自动迁移等功能
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void Demo_FullConfiguration()
|
||||
{
|
||||
var builder = new DataContextBuilder<TestDbContext>();
|
||||
|
||||
// 链式配置所有功能
|
||||
builder.UseInMemoryDatabase("FullConfigDemo")
|
||||
.UseAudit(true) // 启用审计功能
|
||||
.UseSoftDelete(true) // 启用软删除功能
|
||||
.UseAutoMigration(true) // 启动时自动迁移数据库
|
||||
.UseCurrentUser("admin") // 设置当前用户(用于审计)
|
||||
.EnableDetailedErrors(true) // 启用详细错误信息
|
||||
.EnableSensitiveDataLogging(true); // 启用敏感数据日志(仅开发环境)
|
||||
|
||||
using var db = builder.Build();
|
||||
|
||||
Assert.IsNotNull(db);
|
||||
Console.WriteLine("✓ 完整配置成功");
|
||||
Console.WriteLine(" - 审计功能: 已启用");
|
||||
Console.WriteLine(" - 软删除功能: 已启用");
|
||||
Console.WriteLine(" - 自动迁移: 已启用");
|
||||
Console.WriteLine(" - 当前用户: admin");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 动态获取当前用户
|
||||
///
|
||||
/// 在真实应用中,用户通常来自认证系统
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void Demo_DynamicCurrentUser()
|
||||
{
|
||||
var builder = new DataContextBuilder<TestDbContext>();
|
||||
|
||||
// 模拟从认证系统获取用户
|
||||
var currentUser = "zhang.san"; // 实际应用中可能来自 HttpContext 或其他认证系统
|
||||
|
||||
builder.UseInMemoryDatabase("DynamicUserDemo")
|
||||
.UseAudit(true)
|
||||
.UseCurrentUserProvider(() =>
|
||||
{
|
||||
// 这里可以放置获取当前用户的逻辑
|
||||
// 例如: HttpContext.Current?.User?.Identity?.Name
|
||||
return currentUser;
|
||||
});
|
||||
|
||||
using var db = builder.Build();
|
||||
|
||||
Assert.IsNotNull(db);
|
||||
Console.WriteLine($"✓ 动态用户配置成功,当前用户: {currentUser}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 只启用部分功能
|
||||
///
|
||||
/// 所有功能都是可选的,按需启用
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void Demo_SelectiveFeatures()
|
||||
{
|
||||
var builder = new DataContextBuilder<TestDbContext>();
|
||||
|
||||
// 只启用审计,不启用软删除
|
||||
builder.UseInMemoryDatabase("SelectiveDemo")
|
||||
.UseAudit(true) // 启用审计
|
||||
.UseSoftDelete(false); // 不启用软删除
|
||||
|
||||
using var db = builder.Build();
|
||||
|
||||
Assert.IsNotNull(db);
|
||||
Console.WriteLine("✓ 选择性功能配置成功");
|
||||
Console.WriteLine(" - 审计功能: 已启用");
|
||||
Console.WriteLine(" - 软删除功能: 未启用");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 获取仓储对象
|
||||
///
|
||||
/// DataContext 提供统一的仓储访问入口
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void Demo_GetRepository()
|
||||
{
|
||||
var builder = new DataContextBuilder<TestDbContext>();
|
||||
builder.UseInMemoryDatabase("RepositoryDemo");
|
||||
|
||||
using var db = builder.Build();
|
||||
|
||||
// 获取不同实体的仓储
|
||||
var productRepo = db.Repository<Product>();
|
||||
var categoryRepo = db.Repository<Category>();
|
||||
var orderRepo = db.Repository<Order>();
|
||||
|
||||
Assert.IsNotNull(productRepo);
|
||||
Assert.IsNotNull(categoryRepo);
|
||||
Assert.IsNotNull(orderRepo);
|
||||
|
||||
Console.WriteLine("✓ 仓储获取成功");
|
||||
Console.WriteLine(" - Product 仓储");
|
||||
Console.WriteLine(" - Category 仓储");
|
||||
Console.WriteLine(" - Order 仓储");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 访问底层 DbContext
|
||||
///
|
||||
/// 可以直接访问 EF Core DbContext 进行高级操作
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void Demo_AccessUnderlyingDbContext()
|
||||
{
|
||||
var builder = new DataContextBuilder<TestDbContext>();
|
||||
builder.UseInMemoryDatabase("DirectAccessDemo");
|
||||
|
||||
using var db = builder.Build();
|
||||
|
||||
// 访问底层 DbContext
|
||||
var context = db.Context;
|
||||
|
||||
Assert.IsNotNull(context);
|
||||
Assert.IsInstanceOfType<TestDbContext>(context);
|
||||
|
||||
Console.WriteLine("✓ 可以访问底层 DbContext");
|
||||
Console.WriteLine(" - 用于执行原生 SQL");
|
||||
Console.WriteLine(" - 用于事务管理");
|
||||
Console.WriteLine(" - 用于高级查询");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 配置不同的数据库提供程序
|
||||
///
|
||||
/// Sharp.Data 支持多种数据库
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void Demo_DifferentDatabaseProviders()
|
||||
{
|
||||
// 1. SQLite
|
||||
var sqliteBuilder = new DataContextBuilder<TestDbContext>();
|
||||
sqliteBuilder.UseSqlite("Data Source=app.db");
|
||||
Console.WriteLine("✓ SQLite: Data Source=app.db");
|
||||
|
||||
// 2. SQL Server
|
||||
var sqlServerBuilder = new DataContextBuilder<TestDbContext>();
|
||||
sqlServerBuilder.UseSqlServer("Server=.;Database=MyApp;Trusted_Connection=True;");
|
||||
Console.WriteLine("✓ SQL Server: 连接字符串已配置");
|
||||
|
||||
// 3. MySQL
|
||||
var mysqlBuilder = new DataContextBuilder<TestDbContext>();
|
||||
mysqlBuilder.UseMySql("server=localhost;database=myapp;user=root;password=123", "8.0.35");
|
||||
Console.WriteLine("✓ MySQL: 连接字符串已配置");
|
||||
|
||||
// 4. PostgreSQL
|
||||
var npgsqlBuilder = new DataContextBuilder<TestDbContext>();
|
||||
npgsqlBuilder.UseNpgsql("Host=localhost;Database=myapp;Username=postgres;Password=123");
|
||||
Console.WriteLine("✓ PostgreSQL: 连接字符串已配置");
|
||||
|
||||
// 5. InMemory (测试用)
|
||||
var memoryBuilder = new DataContextBuilder<TestDbContext>();
|
||||
memoryBuilder.UseInMemoryDatabase("TestDb");
|
||||
using var db = memoryBuilder.Build();
|
||||
Console.WriteLine("✓ InMemory: 内存数据库用于测试");
|
||||
|
||||
Assert.IsNotNull(db);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 正确释放资源
|
||||
///
|
||||
/// DataContext 实现了 IDisposable,使用后要释放
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void Demo_ProperResourceDisposal()
|
||||
{
|
||||
// 方式1: 使用 using 语句 (推荐)
|
||||
using (var db = CreateDataContext())
|
||||
{
|
||||
// 使用 db 进行操作
|
||||
var repo = db.Repository<Product>();
|
||||
// ... 操作
|
||||
} // 自动释放
|
||||
|
||||
Console.WriteLine("✓ 方式1: using 语句自动释放");
|
||||
|
||||
// 方式2: 手动释放
|
||||
var db2 = CreateDataContext();
|
||||
try
|
||||
{
|
||||
var repo = db2.Repository<Product>();
|
||||
// ... 操作
|
||||
}
|
||||
finally
|
||||
{
|
||||
db2.Dispose(); // 手动释放
|
||||
}
|
||||
|
||||
Console.WriteLine("✓ 方式2: 手动释放");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 辅助方法: 创建测试用的 DataContext
|
||||
/// </summary>
|
||||
private IDataContext CreateDataContext()
|
||||
{
|
||||
var builder = new DataContextBuilder<TestDbContext>();
|
||||
builder.UseInMemoryDatabase($"TestDb_{Guid.NewGuid()}");
|
||||
return builder.Build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,548 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Sharp.Data;
|
||||
using Sharp.Data.Context;
|
||||
|
||||
namespace Data.Test;
|
||||
|
||||
/// <summary>
|
||||
/// 演示 02: 仓储 CRUD 操作
|
||||
///
|
||||
/// 本测试类演示:
|
||||
/// 1. 添加数据 (Create)
|
||||
/// 2. 查询数据 (Read)
|
||||
/// 3. 更新数据 (Update)
|
||||
/// 4. 删除数据 (Delete)
|
||||
/// 5. 批量操作
|
||||
/// 6. 条件查询
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class _02_RepositoryCRUDTests
|
||||
{
|
||||
private IDataContext _db = null!;
|
||||
|
||||
[TestInitialize]
|
||||
public void Setup()
|
||||
{
|
||||
var builder = new DataContextBuilder<TestDbContext>();
|
||||
builder.UseInMemoryDatabase($"CRUDDemo_{Guid.NewGuid()}");
|
||||
_db = builder.Build();
|
||||
}
|
||||
|
||||
[TestCleanup]
|
||||
public void Cleanup()
|
||||
{
|
||||
_db?.Dispose();
|
||||
}
|
||||
|
||||
#region Create - 添加数据
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 添加单个实体
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Demo_AddSingleEntity()
|
||||
{
|
||||
var repo = _db.Repository<Product>();
|
||||
|
||||
// 创建新产品
|
||||
var product = new Product
|
||||
{
|
||||
Name = "iPhone 15",
|
||||
Price = 7999.00m,
|
||||
Description = "最新款 iPhone",
|
||||
Stock = 100
|
||||
};
|
||||
|
||||
// 添加到数据库
|
||||
await repo.AddAsync(product);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine("✓ 成功添加产品");
|
||||
Console.WriteLine($" - ID: {product.Id}");
|
||||
Console.WriteLine($" - 名称: {product.Name}");
|
||||
Console.WriteLine($" - 价格: {product.Price:C}");
|
||||
|
||||
// 验证
|
||||
var savedProduct = await repo.GetByIdAsync(product.Id);
|
||||
Assert.IsNotNull(savedProduct);
|
||||
Assert.AreEqual("iPhone 15", savedProduct.Name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 批量添加实体
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Demo_AddMultipleEntities()
|
||||
{
|
||||
var repo = _db.Repository<Product>();
|
||||
|
||||
// 创建多个产品
|
||||
var products = new List<Product>
|
||||
{
|
||||
new() { Name = "MacBook Pro", Price = 12999.00m, Stock = 50 },
|
||||
new() { Name = "iPad Air", Price = 4799.00m, Stock = 80 },
|
||||
new() { Name = "AirPods Pro", Price = 1899.00m, Stock = 200 }
|
||||
};
|
||||
|
||||
// 批量添加
|
||||
await repo.AddRangeAsync(products);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine($"✓ 成功添加 {products.Count} 个产品");
|
||||
foreach (var product in products)
|
||||
{
|
||||
Console.WriteLine($" - {product.Name}: {product.Price:C}");
|
||||
}
|
||||
|
||||
// 验证
|
||||
var allProducts = await repo.GetAllAsync();
|
||||
Assert.AreEqual(3, allProducts.Count);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Read - 查询数据
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 根据 ID 获取实体
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Demo_GetById()
|
||||
{
|
||||
var repo = _db.Repository<Product>();
|
||||
|
||||
// 先添加数据
|
||||
var product = new Product { Name = "测试产品", Price = 99.00m, Stock = 10 };
|
||||
await repo.AddAsync(product);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
// 根据 ID 查询
|
||||
var found = await repo.GetByIdAsync(product.Id);
|
||||
|
||||
Assert.IsNotNull(found);
|
||||
Assert.AreEqual(product.Id, found.Id);
|
||||
Console.WriteLine($"✓ 通过 ID 查询成功: {found.Name}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 获取所有数据
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Demo_GetAll()
|
||||
{
|
||||
var repo = _db.Repository<Product>();
|
||||
|
||||
// 添加测试数据
|
||||
await repo.AddRangeAsync(new[]
|
||||
{
|
||||
new Product { Name = "产品1", Price = 100m, Stock = 10 },
|
||||
new Product { Name = "产品2", Price = 200m, Stock = 20 },
|
||||
new Product { Name = "产品3", Price = 300m, Stock = 30 }
|
||||
});
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
// 获取所有数据
|
||||
var allProducts = await repo.GetAllAsync();
|
||||
|
||||
Assert.AreEqual(3, allProducts.Count);
|
||||
Console.WriteLine($"✓ 获取所有数据成功,共 {allProducts.Count} 条");
|
||||
foreach (var p in allProducts)
|
||||
{
|
||||
Console.WriteLine($" - {p.Name}: {p.Price:C}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 条件查询
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Demo_FindWithCondition()
|
||||
{
|
||||
var repo = _db.Repository<Product>();
|
||||
|
||||
// 添加测试数据
|
||||
await repo.AddRangeAsync(new[]
|
||||
{
|
||||
new Product { Name = "苹果手机", Price = 7999m, Stock = 50 },
|
||||
new Product { Name = "苹果平板", Price = 4799m, Stock = 30 },
|
||||
new Product { Name = "华为手机", Price = 6999m, Stock = 40 }
|
||||
});
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
// 查找名称包含"苹果"的产品
|
||||
var appleProducts = await repo.FindAsync(p => p.Name.Contains("苹果"));
|
||||
|
||||
Assert.AreEqual(2, appleProducts.Count);
|
||||
Console.WriteLine("✓ 条件查询成功,找到包含'苹果'的产品:");
|
||||
foreach (var p in appleProducts)
|
||||
{
|
||||
Console.WriteLine($" - {p.Name}: {p.Price:C}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 获取第一个符合条件的实体
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Demo_FirstOrDefault()
|
||||
{
|
||||
var repo = _db.Repository<Product>();
|
||||
|
||||
// 添加测试数据
|
||||
await repo.AddRangeAsync(new[]
|
||||
{
|
||||
new Product { Name = "产品A", Price = 100m, Stock = 10 },
|
||||
new Product { Name = "产品B", Price = 200m, Stock = 20 }
|
||||
});
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
// 查找第一个价格大于150的产品
|
||||
var expensiveProduct = await repo.FirstOrDefaultAsync(p => p.Price > 150);
|
||||
|
||||
Assert.IsNotNull(expensiveProduct);
|
||||
Assert.AreEqual("产品B", expensiveProduct.Name);
|
||||
Console.WriteLine($"✓ 找到第一个价格>150的产品: {expensiveProduct.Name}");
|
||||
|
||||
// 查找不存在的条件
|
||||
var notFound = await repo.FirstOrDefaultAsync(p => p.Price > 1000);
|
||||
Assert.IsNull(notFound);
|
||||
Console.WriteLine("✓ 不存在的条件返回 null");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 统计查询
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Demo_CountAndAny()
|
||||
{
|
||||
var repo = _db.Repository<Product>();
|
||||
|
||||
// 添加测试数据
|
||||
await repo.AddRangeAsync(new[]
|
||||
{
|
||||
new Product { Name = "产品1", Price = 100m, Stock = 10 },
|
||||
new Product { Name = "产品2", Price = 200m, Stock = 20 },
|
||||
new Product { Name = "产品3", Price = 300m, Stock = 30 }
|
||||
});
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
// 统计总数
|
||||
var totalCount = await repo.CountAsync();
|
||||
Console.WriteLine($"✓ 总记录数: {totalCount}");
|
||||
Assert.AreEqual(3, totalCount);
|
||||
|
||||
// 统计符合条件的数量
|
||||
var expensiveCount = await repo.CountAsync(p => p.Price > 150);
|
||||
Console.WriteLine($"✓ 价格>150的产品数: {expensiveCount}");
|
||||
Assert.AreEqual(2, expensiveCount);
|
||||
|
||||
// 检查是否存在
|
||||
var hasExpensive = await repo.AnyAsync(p => p.Price > 250);
|
||||
Console.WriteLine($"✓ 是否存在价格>250的产品: {hasExpensive}");
|
||||
Assert.IsTrue(hasExpensive);
|
||||
|
||||
var hasVeryExpensive = await repo.AnyAsync(p => p.Price > 500);
|
||||
Console.WriteLine($"✓ 是否存在价格>500的产品: {hasVeryExpensive}");
|
||||
Assert.IsFalse(hasVeryExpensive);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 使用 Query 方法进行复杂查询
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Demo_ComplexQueryWithLinq()
|
||||
{
|
||||
var repo = _db.Repository<Product>();
|
||||
|
||||
// 添加测试数据
|
||||
await repo.AddRangeAsync(new[]
|
||||
{
|
||||
new Product { Name = "iPhone", Price = 7999m, Stock = 50 },
|
||||
new Product { Name = "iPad", Price = 4799m, Stock = 30 },
|
||||
new Product { Name = "MacBook", Price = 12999m, Stock = 20 },
|
||||
new Product { Name = "AirPods", Price = 1899m, Stock = 100 }
|
||||
});
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
// 获取可查询对象,进行复杂查询
|
||||
var query = repo.Query(trackChanges: false); // trackChanges: false 提升查询性能
|
||||
|
||||
var result = await query
|
||||
.Where(p => p.Price > 2000) // 价格大于2000
|
||||
.OrderByDescending(p => p.Price) // 按价格降序
|
||||
.Select(p => new // 投影到匿名类型
|
||||
{
|
||||
p.Name,
|
||||
p.Price,
|
||||
p.Stock,
|
||||
TotalValue = p.Price * p.Stock // 计算总价值
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
Console.WriteLine("✓ 复杂查询成功,价格>2000的产品(按价格降序):");
|
||||
foreach (var item in result)
|
||||
{
|
||||
Console.WriteLine($" - {item.Name}: {item.Price:C} x {item.Stock} = {item.TotalValue:C}");
|
||||
}
|
||||
|
||||
Assert.AreEqual(3, result.Count);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Update - 更新数据
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 更新实体
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Demo_UpdateEntity()
|
||||
{
|
||||
var repo = _db.Repository<Product>();
|
||||
|
||||
// 添加产品
|
||||
var product = new Product
|
||||
{
|
||||
Name = "旧名称",
|
||||
Price = 100m,
|
||||
Stock = 50
|
||||
};
|
||||
await repo.AddAsync(product);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine($"✓ 原始数据: {product.Name}, 价格: {product.Price:C}");
|
||||
|
||||
// 修改产品
|
||||
product.Name = "新名称";
|
||||
product.Price = 200m;
|
||||
repo.Update(product);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine($"✓ 更新后: {product.Name}, 价格: {product.Price:C}");
|
||||
|
||||
// 验证更新
|
||||
var updated = await repo.GetByIdAsync(product.Id);
|
||||
Assert.AreEqual("新名称", updated!.Name);
|
||||
Assert.AreEqual(200m, updated.Price);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 批量更新
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Demo_UpdateMultipleEntities()
|
||||
{
|
||||
var repo = _db.Repository<Product>();
|
||||
|
||||
// 添加产品
|
||||
var products = new List<Product>
|
||||
{
|
||||
new() { Name = "产品1", Price = 100m, Stock = 10 },
|
||||
new() { Name = "产品2", Price = 200m, Stock = 20 },
|
||||
new() { Name = "产品3", Price = 300m, Stock = 30 }
|
||||
};
|
||||
await repo.AddRangeAsync(products);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
// 批量涨价 10%
|
||||
foreach (var product in products)
|
||||
{
|
||||
product.Price *= 1.10m;
|
||||
}
|
||||
repo.UpdateRange(products);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine("✓ 批量涨价 10% 成功:");
|
||||
var updatedProducts = await repo.GetAllAsync();
|
||||
foreach (var p in updatedProducts)
|
||||
{
|
||||
Console.WriteLine($" - {p.Name}: {p.Price:C}");
|
||||
}
|
||||
|
||||
// 验证
|
||||
Assert.AreEqual(110m, updatedProducts[0].Price);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Delete - 删除数据
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 删除实体 (物理删除)
|
||||
///
|
||||
/// 注意: 对于实现了 ISoftDeletable 的实体,RemoveAsync 会变成软删除
|
||||
/// 本例中的 Product 继承自 AuditableEntity,实现了软删除接口
|
||||
/// 所以这个测试实际演示的是软删除,详见 04_SoftDeleteTests
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Demo_DeleteEntity()
|
||||
{
|
||||
var repo = _db.Repository<Category>(); // Category 没有软删除,是真实删除
|
||||
|
||||
// 添加分类
|
||||
var category = new Category { Name = "测试分类" };
|
||||
await repo.AddAsync(category);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine($"✓ 添加分类: {category.Name}, ID: {category.Id}");
|
||||
|
||||
// 删除分类
|
||||
repo.Remove(category);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine("✓ 删除分类成功");
|
||||
|
||||
// 验证删除
|
||||
var deleted = await repo.GetByIdAsync(category.Id);
|
||||
Assert.IsNull(deleted);
|
||||
Console.WriteLine("✓ 验证: 分类已被物理删除");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 根据 ID 删除
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Demo_DeleteById()
|
||||
{
|
||||
var repo = _db.Repository<Category>();
|
||||
|
||||
// 添加分类
|
||||
var category = new Category { Name = "要删除的分类" };
|
||||
await repo.AddAsync(category);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
var categoryId = category.Id;
|
||||
Console.WriteLine($"✓ 添加分类: ID={categoryId}");
|
||||
|
||||
// 根据 ID 删除
|
||||
await repo.RemoveAsync(categoryId);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine("✓ 根据 ID 删除成功");
|
||||
|
||||
// 验证删除
|
||||
var deleted = await repo.GetByIdAsync(categoryId);
|
||||
Assert.IsNull(deleted);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 批量删除
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Demo_DeleteMultipleEntities()
|
||||
{
|
||||
var repo = _db.Repository<Category>();
|
||||
|
||||
// 添加多个分类
|
||||
var categories = new List<Category>
|
||||
{
|
||||
new() { Name = "分类1" },
|
||||
new() { Name = "分类2" },
|
||||
new() { Name = "分类3" }
|
||||
};
|
||||
await repo.AddRangeAsync(categories);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine($"✓ 添加了 {categories.Count} 个分类");
|
||||
|
||||
// 批量删除
|
||||
repo.RemoveRange(categories);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine("✓ 批量删除成功");
|
||||
|
||||
// 验证
|
||||
var remaining = await repo.GetAllAsync();
|
||||
Assert.AreEqual(0, remaining.Count);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 高级查询
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 排序查询
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Demo_OrderedQuery()
|
||||
{
|
||||
var repo = _db.Repository<Product>();
|
||||
|
||||
// 添加测试数据
|
||||
await repo.AddRangeAsync(new[]
|
||||
{
|
||||
new Product { Name = "C产品", Price = 300m, Stock = 30 },
|
||||
new Product { Name = "A产品", Price = 100m, Stock = 10 },
|
||||
new Product { Name = "B产品", Price = 200m, Stock = 20 }
|
||||
});
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
// 按价格升序
|
||||
var ascProducts = await repo.Query()
|
||||
.OrderBy(p => p.Price)
|
||||
.ToListAsync();
|
||||
|
||||
Console.WriteLine("✓ 按价格升序:");
|
||||
foreach (var p in ascProducts)
|
||||
{
|
||||
Console.WriteLine($" - {p.Name}: {p.Price:C}");
|
||||
}
|
||||
|
||||
// 按价格降序
|
||||
var descProducts = await repo.Query()
|
||||
.OrderByDescending(p => p.Price)
|
||||
.ToListAsync();
|
||||
|
||||
Console.WriteLine("✓ 按价格降序:");
|
||||
foreach (var p in descProducts)
|
||||
{
|
||||
Console.WriteLine($" - {p.Name}: {p.Price:C}");
|
||||
}
|
||||
|
||||
Assert.AreEqual("A产品", ascProducts[0].Name);
|
||||
Assert.AreEqual("C产品", descProducts[0].Name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 分组查询
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Demo_GroupQuery()
|
||||
{
|
||||
var repo = _db.Repository<Product>();
|
||||
|
||||
// 添加不同价格区间的产品
|
||||
await repo.AddRangeAsync(new[]
|
||||
{
|
||||
new Product { Name = "低价产品1", Price = 50m, Stock = 10 },
|
||||
new Product { Name = "低价产品2", Price = 80m, Stock = 20 },
|
||||
new Product { Name = "中价产品1", Price = 150m, Stock = 15 },
|
||||
new Product { Name = "中价产品2", Price = 180m, Stock = 25 },
|
||||
new Product { Name = "高价产品1", Price = 500m, Stock = 5 },
|
||||
new Product { Name = "高价产品2", Price = 600m, Stock = 8 }
|
||||
});
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
// 按价格区间分组统计
|
||||
var priceGroups = await repo.Query()
|
||||
.GroupBy(p => p.Price < 100 ? "低价" : p.Price < 300 ? "中价" : "高价")
|
||||
.Select(g => new
|
||||
{
|
||||
PriceRange = g.Key,
|
||||
Count = g.Count(),
|
||||
AvgPrice = g.Average(p => p.Price),
|
||||
TotalStock = g.Sum(p => p.Stock)
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
Console.WriteLine("✓ 按价格区间分组统计:");
|
||||
foreach (var group in priceGroups)
|
||||
{
|
||||
Console.WriteLine($" - {group.PriceRange}: {group.Count}个产品, 平均价格 {group.AvgPrice:C}, 总库存 {group.TotalStock}");
|
||||
}
|
||||
|
||||
Assert.AreEqual(3, priceGroups.Count);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,371 @@
|
||||
using Sharp.Data;
|
||||
using Sharp.Data.Context;
|
||||
|
||||
namespace Data.Test;
|
||||
|
||||
/// <summary>
|
||||
/// 演示 03: 审计功能
|
||||
///
|
||||
/// 本测试类演示:
|
||||
/// 1. 如何启用审计功能
|
||||
/// 2. 创建时自动记录 CreatedAt 和 CreatedBy
|
||||
/// 3. 更新时自动记录 UpdatedAt 和 UpdatedBy
|
||||
/// 4. 如何设置当前用户
|
||||
/// 5. 审计功能对不同实体的影响
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class _03_AuditFeatureTests
|
||||
{
|
||||
/// <summary>
|
||||
/// 演示: 基础审计功能 - 创建时自动记录
|
||||
///
|
||||
/// 使用 AuditableEntity 基类的实体会自动记录审计信息
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Demo_AuditOnCreate()
|
||||
{
|
||||
// 创建启用审计功能的 DataContext
|
||||
var builder = new DataContextBuilder<TestDbContext>();
|
||||
builder.UseInMemoryDatabase($"AuditCreateDemo_{Guid.NewGuid()}")
|
||||
.UseAudit(true) // 启用审计
|
||||
.UseCurrentUser("zhang.san"); // 设置当前用户
|
||||
|
||||
using var db = builder.Build();
|
||||
var repo = db.Repository<Product>();
|
||||
|
||||
var beforeCreate = DateTime.UtcNow;
|
||||
|
||||
// 创建产品
|
||||
var product = new Product
|
||||
{
|
||||
Name = "测试产品",
|
||||
Price = 99.00m,
|
||||
Stock = 10
|
||||
};
|
||||
|
||||
// 添加前,审计字段是空的
|
||||
Console.WriteLine("添加前:");
|
||||
Console.WriteLine($" CreatedAt: {product.CreatedAt}");
|
||||
Console.WriteLine($" CreatedBy: {product.CreatedBy ?? "(null)"}");
|
||||
|
||||
await repo.AddAsync(product);
|
||||
await db.SaveChangesAsync(); // SaveChanges 时自动填充审计信息
|
||||
|
||||
var afterCreate = DateTime.UtcNow;
|
||||
|
||||
// 添加后,审计字段自动填充
|
||||
Console.WriteLine("\n添加后:");
|
||||
Console.WriteLine($" ✓ CreatedAt: {product.CreatedAt:yyyy-MM-dd HH:mm:ss}");
|
||||
Console.WriteLine($" ✓ CreatedBy: {product.CreatedBy}");
|
||||
Console.WriteLine($" ✓ UpdatedAt: {product.UpdatedAt?.ToString("yyyy-MM-dd HH:mm:ss") ?? "(null)"}");
|
||||
Console.WriteLine($" ✓ UpdatedBy: {product.UpdatedBy ?? "(null)"}");
|
||||
|
||||
// 验证
|
||||
Assert.IsTrue(product.CreatedAt >= beforeCreate && product.CreatedAt <= afterCreate);
|
||||
Assert.AreEqual("zhang.san", product.CreatedBy);
|
||||
Assert.IsNull(product.UpdatedAt); // 创建时 UpdatedAt 为 null
|
||||
Assert.IsNull(product.UpdatedBy);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 审计功能 - 更新时自动记录
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Demo_AuditOnUpdate()
|
||||
{
|
||||
var builder = new DataContextBuilder<TestDbContext>();
|
||||
builder.UseInMemoryDatabase($"AuditUpdateDemo_{Guid.NewGuid()}")
|
||||
.UseAudit(true)
|
||||
.UseCurrentUser("creator");
|
||||
|
||||
using var db = builder.Build();
|
||||
var repo = db.Repository<Product>();
|
||||
|
||||
// 1. 创建产品 (创建人: creator)
|
||||
var product = new Product
|
||||
{
|
||||
Name = "原始产品",
|
||||
Price = 100m,
|
||||
Stock = 10
|
||||
};
|
||||
await repo.AddAsync(product);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine("创建后的审计信息:");
|
||||
Console.WriteLine($" CreatedBy: {product.CreatedBy}");
|
||||
Console.WriteLine($" CreatedAt: {product.CreatedAt:yyyy-MM-dd HH:mm:ss}");
|
||||
|
||||
var createdAt = product.CreatedAt;
|
||||
var createdBy = product.CreatedBy;
|
||||
|
||||
// 等待一小段时间
|
||||
await Task.Delay(100);
|
||||
|
||||
// 2. 更改当前用户并更新产品
|
||||
// 注意:如果需要切换用户,请创建新的 DataContext
|
||||
// (db as Sharp.Data.Context.DataContext)?.SetCurrentUser("editor");
|
||||
|
||||
product.Name = "更新后的产品";
|
||||
product.Price = 200m;
|
||||
repo.Update(product);
|
||||
await db.SaveChangesAsync(); // SaveChanges 时自动填充 UpdatedAt 和 UpdatedBy
|
||||
|
||||
Console.WriteLine("\n更新后的审计信息:");
|
||||
Console.WriteLine($" CreatedBy: {product.CreatedBy} (保持不变)");
|
||||
Console.WriteLine($" CreatedAt: {product.CreatedAt:yyyy-MM-dd HH:mm:ss} (保持不变)");
|
||||
Console.WriteLine($" ✓ UpdatedBy: {product.UpdatedBy}");
|
||||
Console.WriteLine($" ✓ UpdatedAt: {product.UpdatedAt:yyyy-MM-dd HH:mm:ss}");
|
||||
|
||||
// 验证
|
||||
Assert.AreEqual(createdBy, product.CreatedBy); // 创建人不变
|
||||
Assert.AreEqual(createdAt, product.CreatedAt); // 创建时间不变
|
||||
Assert.AreEqual("editor", product.UpdatedBy); // 更新人是新用户
|
||||
Assert.IsNotNull(product.UpdatedAt);
|
||||
Assert.IsTrue(product.UpdatedAt > product.CreatedAt); // 更新时间晚于创建时间
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 多次更新的审计记录
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Demo_MultipleUpdatesAudit()
|
||||
{
|
||||
var builder = new DataContextBuilder<TestDbContext>();
|
||||
builder.UseInMemoryDatabase($"MultiUpdateDemo_{Guid.NewGuid()}")
|
||||
.UseAudit(true)
|
||||
.UseCurrentUser("user1");
|
||||
|
||||
using var db = builder.Build();
|
||||
var repo = db.Repository<Product>();
|
||||
|
||||
// 创建产品
|
||||
var product = new Product { Name = "产品", Price = 100m, Stock = 10 };
|
||||
await repo.AddAsync(product);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine($"创建: {product.CreatedBy} 于 {product.CreatedAt:HH:mm:ss}");
|
||||
|
||||
// 第一次更新
|
||||
await Task.Delay(50);
|
||||
// 注意:如果需要切换用户,请创建新的 DataContext
|
||||
// (db as Sharp.Data.Context.DataContext)?.SetCurrentUser("user2");
|
||||
product.Price = 150m;
|
||||
repo.Update(product);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var firstUpdateTime = product.UpdatedAt!.Value;
|
||||
Console.WriteLine($"第1次更新: {product.UpdatedBy} 于 {product.UpdatedAt:HH:mm:ss}");
|
||||
|
||||
// 第二次更新
|
||||
await Task.Delay(50);
|
||||
// 注意:如果需要切换用户,请创建新的 DataContext
|
||||
// (db as Sharp.Data.Context.DataContext)?.SetCurrentUser("user3");
|
||||
product.Price = 200m;
|
||||
repo.Update(product);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine($"第2次更新: {product.UpdatedBy} 于 {product.UpdatedAt:HH:mm:ss}");
|
||||
|
||||
// 验证
|
||||
Assert.AreEqual("user1", product.CreatedBy); // 创建人始终不变
|
||||
Assert.AreEqual("user3", product.UpdatedBy); // 最后更新人
|
||||
Assert.IsTrue(product.UpdatedAt > firstUpdateTime); // 更新时间在变化
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 动态获取当前用户
|
||||
///
|
||||
/// 在真实应用中,用户信息通常来自认证系统
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Demo_DynamicUserProvider()
|
||||
{
|
||||
// 模拟当前登录用户(实际应用中可能来自 HttpContext、Session 等)
|
||||
var currentUser = "zhang.san";
|
||||
|
||||
var builder = new DataContextBuilder<TestDbContext>();
|
||||
builder.UseInMemoryDatabase($"DynamicUserDemo_{Guid.NewGuid()}")
|
||||
.UseAudit(true)
|
||||
.UseCurrentUserProvider(() =>
|
||||
{
|
||||
// 这里可以放置获取当前用户的逻辑
|
||||
// 例如: Thread.CurrentPrincipal?.Identity?.Name
|
||||
// HttpContext.Current?.User?.Identity?.Name
|
||||
// 某个全局的用户服务
|
||||
return currentUser;
|
||||
});
|
||||
|
||||
using var db = builder.Build();
|
||||
var repo = db.Repository<Product>();
|
||||
|
||||
// 用户 zhang.san 创建产品
|
||||
var product = new Product { Name = "产品A", Price = 100m, Stock = 10 };
|
||||
await repo.AddAsync(product);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine($"✓ 创建人: {product.CreatedBy}");
|
||||
Assert.AreEqual("zhang.san", product.CreatedBy);
|
||||
|
||||
// 切换用户
|
||||
currentUser = "li.si";
|
||||
|
||||
// 用户 li.si 更新产品
|
||||
product.Price = 200m;
|
||||
repo.Update(product);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine($"✓ 更新人: {product.UpdatedBy}");
|
||||
Assert.AreEqual("zhang.san", product.CreatedBy); // 创建人不变
|
||||
Assert.AreEqual("li.si", product.UpdatedBy); // 更新人改变
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 实现审计接口但不使用基类
|
||||
///
|
||||
/// Order 实体实现了 IAuditableEntity 接口,同样享受审计功能
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Demo_AuditWithInterface()
|
||||
{
|
||||
var builder = new DataContextBuilder<TestDbContext>();
|
||||
builder.UseInMemoryDatabase($"InterfaceAuditDemo_{Guid.NewGuid()}")
|
||||
.UseAudit(true)
|
||||
.UseCurrentUser("admin");
|
||||
|
||||
using var db = builder.Build();
|
||||
var repo = db.Repository<Order>();
|
||||
|
||||
// 创建订单
|
||||
var order = new Order
|
||||
{
|
||||
OrderNumber = "ORD-001",
|
||||
TotalAmount = 999.00m
|
||||
};
|
||||
await repo.AddAsync(order);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine("✓ Order 实体(实现接口但不使用基类)同样享受审计功能");
|
||||
Console.WriteLine($" CreatedBy: {order.CreatedBy}");
|
||||
Console.WriteLine($" CreatedAt: {order.CreatedAt:yyyy-MM-dd HH:mm:ss}");
|
||||
|
||||
// 验证
|
||||
Assert.AreEqual("admin", order.CreatedBy);
|
||||
Assert.IsTrue(order.CreatedAt > DateTime.MinValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 不使用审计的实体不受影响
|
||||
///
|
||||
/// Category 不继承 AuditableEntity,也不实现审计接口
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Demo_NoAuditForNonAuditableEntity()
|
||||
{
|
||||
var builder = new DataContextBuilder<TestDbContext>();
|
||||
builder.UseInMemoryDatabase($"NoAuditDemo_{Guid.NewGuid()}")
|
||||
.UseAudit(true) // 即使启用了审计功能
|
||||
.UseCurrentUser("admin");
|
||||
|
||||
using var db = builder.Build();
|
||||
var repo = db.Repository<Category>();
|
||||
|
||||
// 创建分类
|
||||
var category = new Category { Name = "测试分类" };
|
||||
await repo.AddAsync(category);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine("✓ Category 不实现审计接口,不受审计功能影响");
|
||||
Console.WriteLine($" 只有 Id 和业务字段: {category.Id}, {category.Name}");
|
||||
|
||||
// 验证:Category 没有审计字段
|
||||
Assert.IsNotNull(category.Id);
|
||||
Assert.AreEqual("测试分类", category.Name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 禁用审计功能
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Demo_DisableAudit()
|
||||
{
|
||||
var builder = new DataContextBuilder<TestDbContext>();
|
||||
builder.UseInMemoryDatabase($"DisableAuditDemo_{Guid.NewGuid()}")
|
||||
.UseAudit(false) // 禁用审计功能
|
||||
.UseCurrentUser("admin");
|
||||
|
||||
using var db = builder.Build();
|
||||
var repo = db.Repository<Product>();
|
||||
|
||||
// 创建产品
|
||||
var product = new Product { Name = "测试产品", Price = 100m, Stock = 10 };
|
||||
await repo.AddAsync(product);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine("✓ 审计功能已禁用");
|
||||
Console.WriteLine($" CreatedBy: {product.CreatedBy ?? "(未设置)"}");
|
||||
Console.WriteLine($" CreatedAt: {product.CreatedAt}");
|
||||
|
||||
// 即使实体有审计字段,但功能禁用后,字段不会自动填充
|
||||
// 但 CreatedAt 可能有默认值(DateTime.MinValue 或当前时间,取决于实现)
|
||||
Console.WriteLine(" 注意:禁用审计后,即使实体有审计字段,也不会自动填充");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 审计功能的实际应用场景
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Demo_RealWorldAuditScenario()
|
||||
{
|
||||
var builder = new DataContextBuilder<TestDbContext>();
|
||||
builder.UseInMemoryDatabase($"RealWorldDemo_{Guid.NewGuid()}")
|
||||
.UseAudit(true)
|
||||
.UseCurrentUser("system");
|
||||
|
||||
using var db = builder.Build();
|
||||
var repo = db.Repository<Product>();
|
||||
|
||||
// 场景:产品生命周期跟踪
|
||||
|
||||
// 1. 系统初始化时创建产品
|
||||
var product = new Product
|
||||
{
|
||||
Name = "iPhone 15",
|
||||
Price = 7999m,
|
||||
Stock = 100
|
||||
};
|
||||
await repo.AddAsync(product);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine("=== 产品生命周期审计 ===\n");
|
||||
Console.WriteLine($"1. 产品创建");
|
||||
Console.WriteLine($" 由 {product.CreatedBy} 于 {product.CreatedAt:yyyy-MM-dd HH:mm:ss} 创建");
|
||||
|
||||
// 2. 仓库管理员调整库存
|
||||
await Task.Delay(50);
|
||||
// 注意:如果需要切换用户,请创建新的 DataContext
|
||||
// (db as Sharp.Data.Context.DataContext)?.SetCurrentUser("warehouse.manager");
|
||||
product.Stock = 80;
|
||||
repo.Update(product);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine($"\n2. 库存调整");
|
||||
Console.WriteLine($" 由 {product.UpdatedBy} 于 {product.UpdatedAt:yyyy-MM-dd HH:mm:ss} 更新");
|
||||
|
||||
// 3. 市场部调整价格
|
||||
await Task.Delay(50);
|
||||
// 注意:如果需要切换用户,请创建新的 DataContext
|
||||
// (db as Sharp.Data.Context.DataContext)?.SetCurrentUser("marketing.manager");
|
||||
product.Price = 6999m;
|
||||
repo.Update(product);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine($"\n3. 价格调整");
|
||||
Console.WriteLine($" 由 {product.UpdatedBy} 于 {product.UpdatedAt:yyyy-MM-dd HH:mm:ss} 更新");
|
||||
|
||||
Console.WriteLine($"\n✓ 通过审计信息,可以追踪产品的完整变更历史");
|
||||
Console.WriteLine($" - 谁创建的:{product.CreatedBy}");
|
||||
Console.WriteLine($" - 何时创建:{product.CreatedAt:yyyy-MM-dd HH:mm:ss}");
|
||||
Console.WriteLine($" - 谁最后修改:{product.UpdatedBy}");
|
||||
Console.WriteLine($" - 何时修改:{product.UpdatedAt:yyyy-MM-dd HH:mm:ss}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,382 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Sharp.Data;
|
||||
using Sharp.Data.Context;
|
||||
|
||||
namespace Data.Test;
|
||||
|
||||
/// <summary>
|
||||
/// 演示 04: 软删除功能
|
||||
///
|
||||
/// 本测试类演示:
|
||||
/// 1. 什么是软删除
|
||||
/// 2. 如何启用软删除
|
||||
/// 3. 软删除 vs 物理删除
|
||||
/// 4. 查询时自动过滤已删除数据
|
||||
/// 5. 如何查询已删除的数据
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class _04_SoftDeleteTests
|
||||
{
|
||||
/// <summary>
|
||||
/// 演示: 软删除基础概念
|
||||
///
|
||||
/// 软删除(Soft Delete)不会真正从数据库删除记录,
|
||||
/// 而是将 IsDeleted 标记设置为 true
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Demo_SoftDeleteBasics()
|
||||
{
|
||||
var builder = new DataContextBuilder<TestDbContext>();
|
||||
builder.UseInMemoryDatabase($"SoftDeleteDemo_{Guid.NewGuid()}")
|
||||
.UseAudit(true)
|
||||
.UseSoftDelete(true) // 启用软删除
|
||||
.UseCurrentUser("admin");
|
||||
|
||||
using var db = builder.Build();
|
||||
var repo = db.Repository<Product>();
|
||||
|
||||
// 创建产品
|
||||
var product = new Product
|
||||
{
|
||||
Name = "要删除的产品",
|
||||
Price = 100m,
|
||||
Stock = 10
|
||||
};
|
||||
await repo.AddAsync(product);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine($"✓ 创建产品: {product.Name}");
|
||||
Console.WriteLine($" IsDeleted: {product.IsDeleted}");
|
||||
|
||||
var productId = product.Id;
|
||||
|
||||
// 软删除产品
|
||||
await repo.RemoveAsync(productId);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine($"\n✓ 软删除后:");
|
||||
Console.WriteLine($" IsDeleted: {product.IsDeleted}");
|
||||
Console.WriteLine($" DeletedAt: {product.DeletedAt:yyyy-MM-dd HH:mm:ss}");
|
||||
Console.WriteLine($" DeletedBy: {product.DeletedBy}");
|
||||
|
||||
// 验证
|
||||
Assert.IsTrue(product.IsDeleted);
|
||||
Assert.IsNotNull(product.DeletedAt);
|
||||
Assert.AreEqual("admin", product.DeletedBy);
|
||||
|
||||
// 正常查询看不到已删除的数据
|
||||
var found = await repo.GetByIdAsync(productId);
|
||||
Assert.IsNull(found);
|
||||
Console.WriteLine($"\n✓ 正常查询返回 null (数据被过滤)");
|
||||
|
||||
// 但数据仍在数据库中
|
||||
var allIncludingDeleted = await db.Context.Set<Product>()
|
||||
.IgnoreQueryFilters() // 忽略软删除过滤器
|
||||
.Where(p => p.Id == productId)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
Assert.IsNotNull(allIncludingDeleted);
|
||||
Console.WriteLine($"✓ 使用 IgnoreQueryFilters() 仍可查询到: {allIncludingDeleted.Name}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 软删除 vs 物理删除
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Demo_SoftDeleteVsPhysicalDelete()
|
||||
{
|
||||
var builder = new DataContextBuilder<TestDbContext>();
|
||||
builder.UseInMemoryDatabase($"DeleteCompareDemo_{Guid.NewGuid()}")
|
||||
.UseAudit(true)
|
||||
.UseSoftDelete(true)
|
||||
.UseCurrentUser("admin");
|
||||
|
||||
using var db = builder.Build();
|
||||
|
||||
// 1. 软删除:Product 实现了 ISoftDeletable
|
||||
var productRepo = db.Repository<Product>();
|
||||
var product = new Product { Name = "软删除产品", Price = 100m, Stock = 10 };
|
||||
await productRepo.AddAsync(product);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
await productRepo.RemoveAsync(product.Id);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine("1. 软删除 (Product):");
|
||||
Console.WriteLine($" ✓ IsDeleted 被设置为 true");
|
||||
Console.WriteLine($" ✓ 数据仍在数据库中");
|
||||
Console.WriteLine($" ✓ 正常查询看不到");
|
||||
|
||||
// 验证数据仍在
|
||||
var productStillExists = await db.Context.Set<Product>()
|
||||
.IgnoreQueryFilters()
|
||||
.AnyAsync(p => p.Id == product.Id);
|
||||
Assert.IsTrue(productStillExists);
|
||||
|
||||
// 2. 物理删除:Category 不实现 ISoftDeletable
|
||||
var categoryRepo = db.Repository<Category>();
|
||||
var category = new Category { Name = "物理删除分类" };
|
||||
await categoryRepo.AddAsync(category);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
categoryRepo.Remove(category);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine("\n2. 物理删除 (Category):");
|
||||
Console.WriteLine($" ✓ 记录被真正删除");
|
||||
Console.WriteLine($" ✓ 数据库中不再存在");
|
||||
|
||||
// 验证数据不在
|
||||
var categoryDeleted = await db.Context.Set<Category>()
|
||||
.AnyAsync(c => c.Id == category.Id);
|
||||
Assert.IsFalse(categoryDeleted);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 查询时自动过滤已删除数据
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Demo_AutoFilterDeletedData()
|
||||
{
|
||||
var builder = new DataContextBuilder<TestDbContext>();
|
||||
builder.UseInMemoryDatabase($"FilterDemo_{Guid.NewGuid()}")
|
||||
.UseSoftDelete(true)
|
||||
.UseCurrentUser("admin");
|
||||
|
||||
using var db = builder.Build();
|
||||
var repo = db.Repository<Product>();
|
||||
|
||||
// 添加 5 个产品
|
||||
for (int i = 1; i <= 5; i++)
|
||||
{
|
||||
await repo.AddAsync(new Product
|
||||
{
|
||||
Name = $"产品{i}",
|
||||
Price = i * 100m,
|
||||
Stock = i * 10
|
||||
});
|
||||
}
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine("✓ 创建了 5 个产品");
|
||||
|
||||
// 软删除其中 2 个
|
||||
var allProducts = await repo.GetAllAsync();
|
||||
await repo.RemoveAsync(allProducts[0].Id);
|
||||
await repo.RemoveAsync(allProducts[1].Id);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine("✓ 软删除了 2 个产品");
|
||||
|
||||
// 正常查询只返回未删除的
|
||||
var activeProducts = await repo.GetAllAsync();
|
||||
Console.WriteLine($"\n✓ 正常查询返回: {activeProducts.Count} 个产品(自动过滤已删除)");
|
||||
Assert.AreEqual(3, activeProducts.Count);
|
||||
|
||||
// 使用 IgnoreQueryFilters 查询所有
|
||||
var allIncludingDeleted = await db.Context.Set<Product>()
|
||||
.IgnoreQueryFilters()
|
||||
.ToListAsync();
|
||||
Console.WriteLine($"✓ IgnoreQueryFilters 返回: {allIncludingDeleted.Count} 个产品(包含已删除)");
|
||||
Assert.AreEqual(5, allIncludingDeleted.Count);
|
||||
|
||||
// 只查询已删除的
|
||||
var deletedOnly = await db.Context.Set<Product>()
|
||||
.IgnoreQueryFilters()
|
||||
.Where(p => p.IsDeleted)
|
||||
.ToListAsync();
|
||||
Console.WriteLine($"✓ 只查询已删除: {deletedOnly.Count} 个产品");
|
||||
Assert.AreEqual(2, deletedOnly.Count);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 软删除的实际应用场景
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Demo_SoftDeleteRealWorldScenario()
|
||||
{
|
||||
var builder = new DataContextBuilder<TestDbContext>();
|
||||
builder.UseInMemoryDatabase($"RealWorldSoftDelete_{Guid.NewGuid()}")
|
||||
.UseAudit(true)
|
||||
.UseSoftDelete(true)
|
||||
.UseCurrentUser("admin");
|
||||
|
||||
using var db = builder.Build();
|
||||
var repo = db.Repository<Product>();
|
||||
|
||||
Console.WriteLine("=== 产品下架场景 ===\n");
|
||||
|
||||
// 1. 创建产品
|
||||
var product = new Product
|
||||
{
|
||||
Name = "iPhone 14",
|
||||
Price = 6999m,
|
||||
Stock = 100
|
||||
};
|
||||
await repo.AddAsync(product);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine($"1. 产品上架");
|
||||
Console.WriteLine($" {product.Name} - {product.Price:C}");
|
||||
|
||||
// 2. 产品下架(软删除)
|
||||
// (db as Sharp.Data.Context.DataContext)?.SetCurrentUser("product.manager");
|
||||
await repo.RemoveAsync(product.Id);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine($"\n2. 产品下架");
|
||||
Console.WriteLine($" 操作人: {product.DeletedBy}");
|
||||
Console.WriteLine($" 下架时间: {product.DeletedAt:yyyy-MM-dd HH:mm:ss}");
|
||||
|
||||
// 3. 用户无法看到下架产品
|
||||
var userVisibleProducts = await repo.GetAllAsync();
|
||||
Console.WriteLine($"\n3. 用户视角");
|
||||
Console.WriteLine($" 可见产品数: {userVisibleProducts.Count} (下架产品不可见)");
|
||||
|
||||
// 4. 管理员可以查看下架产品历史
|
||||
var allProducts = await db.Context.Set<Product>()
|
||||
.IgnoreQueryFilters()
|
||||
.ToListAsync();
|
||||
Console.WriteLine($"\n4. 管理员视角");
|
||||
Console.WriteLine($" 所有产品(含下架): {allProducts.Count}");
|
||||
Console.WriteLine($" 可恢复下架产品的历史数据");
|
||||
|
||||
Console.WriteLine($"\n✓ 软删除的优势:");
|
||||
Console.WriteLine($" - 保留历史数据");
|
||||
Console.WriteLine($" - 可以恢复");
|
||||
Console.WriteLine($" - 可以审计追踪");
|
||||
Console.WriteLine($" - 满足合规要求");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 批量软删除
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Demo_BulkSoftDelete()
|
||||
{
|
||||
var builder = new DataContextBuilder<TestDbContext>();
|
||||
builder.UseInMemoryDatabase($"BulkSoftDelete_{Guid.NewGuid()}")
|
||||
.UseSoftDelete(true)
|
||||
.UseCurrentUser("admin");
|
||||
|
||||
using var db = builder.Build();
|
||||
var repo = db.Repository<Product>();
|
||||
|
||||
// 添加多个产品
|
||||
var products = new List<Product>();
|
||||
for (int i = 1; i <= 10; i++)
|
||||
{
|
||||
products.Add(new Product
|
||||
{
|
||||
Name = $"产品{i}",
|
||||
Price = i * 100m,
|
||||
Stock = i * 10
|
||||
});
|
||||
}
|
||||
await repo.AddRangeAsync(products);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine($"✓ 创建了 {products.Count} 个产品");
|
||||
|
||||
// 批量软删除价格 > 500 的产品
|
||||
var expensiveProducts = await repo.FindAsync(p => p.Price > 500);
|
||||
repo.RemoveRange(expensiveProducts);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine($"✓ 批量软删除了 {expensiveProducts.Count} 个产品(价格>500)");
|
||||
|
||||
// 验证
|
||||
var remaining = await repo.GetAllAsync();
|
||||
Console.WriteLine($"✓ 剩余可见产品: {remaining.Count} 个");
|
||||
Assert.AreEqual(5, remaining.Count);
|
||||
|
||||
foreach (var p in expensiveProducts)
|
||||
{
|
||||
Assert.IsTrue(p.IsDeleted);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 只实现软删除,不要审计
|
||||
///
|
||||
/// Notification 只实现了 ISoftDeletable
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Demo_SoftDeleteWithoutAudit()
|
||||
{
|
||||
var builder = new DataContextBuilder<TestDbContext>();
|
||||
builder.UseInMemoryDatabase($"SoftDeleteOnly_{Guid.NewGuid()}")
|
||||
.UseSoftDelete(true)
|
||||
.UseCurrentUser("admin");
|
||||
|
||||
using var db = builder.Build();
|
||||
var repo = db.Repository<Notification>();
|
||||
|
||||
// 创建通知
|
||||
var notification = new Notification
|
||||
{
|
||||
Title = "系统通知",
|
||||
Content = "这是一条测试通知",
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
await repo.AddAsync(notification);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
// 软删除
|
||||
await repo.RemoveAsync(notification.Id);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine("✓ Notification 只实现软删除,不要审计:");
|
||||
Console.WriteLine($" IsDeleted: {notification.IsDeleted}");
|
||||
Console.WriteLine($" DeletedAt: {notification.DeletedAt:yyyy-MM-dd HH:mm:ss}");
|
||||
Console.WriteLine($" DeletedBy: {notification.DeletedBy}");
|
||||
Console.WriteLine($" 没有 CreatedBy/UpdatedBy 字段");
|
||||
|
||||
Assert.IsTrue(notification.IsDeleted);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 禁用软删除时的行为
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Demo_DisableSoftDelete()
|
||||
{
|
||||
var builder = new DataContextBuilder<TestDbContext>();
|
||||
builder.UseInMemoryDatabase($"DisableSoftDelete_{Guid.NewGuid()}")
|
||||
.UseSoftDelete(false) // 禁用软删除
|
||||
.UseCurrentUser("admin");
|
||||
|
||||
using var db = builder.Build();
|
||||
var repo = db.Repository<Product>();
|
||||
|
||||
// 添加产品
|
||||
var product = new Product { Name = "测试产品", Price = 100m, Stock = 10 };
|
||||
await repo.AddAsync(product);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var productId = product.Id;
|
||||
|
||||
// 删除产品(禁用软删除后,RemoveAsync 会尝试物理删除)
|
||||
// 但 Product 实体仍有 IsDeleted 字段,所以可能变成软删除
|
||||
await repo.RemoveAsync(productId);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine("✓ 禁用软删除功能后:");
|
||||
Console.WriteLine($" RemoveAsync 的行为取决于实现");
|
||||
Console.WriteLine($" 建议:如果不需要软删除,不要让实体实现 ISoftDeletable");
|
||||
|
||||
// 查询
|
||||
var found = await db.Context.Set<Product>()
|
||||
.IgnoreQueryFilters()
|
||||
.FirstOrDefaultAsync(p => p.Id == productId);
|
||||
|
||||
if (found != null)
|
||||
{
|
||||
Console.WriteLine($" 结果:软删除 (IsDeleted={found.IsDeleted})");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($" 结果:物理删除");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,415 @@
|
||||
using Common.ExpandFun;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Sharp.Data;
|
||||
using Sharp.Data.Context;
|
||||
|
||||
namespace Data.Test;
|
||||
|
||||
/// <summary>
|
||||
/// 演示 05: 分页查询
|
||||
///
|
||||
/// 本测试类演示:
|
||||
/// 1. 基础分页查询
|
||||
/// 2. 带条件的分页
|
||||
/// 3. 带排序的分页
|
||||
/// 4. 分页结果的使用
|
||||
/// 5. 复用 Common 模块的 PagedResult
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class _05_PaginationTests
|
||||
{
|
||||
private IDataContext _db = null!;
|
||||
|
||||
[TestInitialize]
|
||||
public async Task Setup()
|
||||
{
|
||||
var builder = new DataContextBuilder<TestDbContext>();
|
||||
builder.UseInMemoryDatabase($"PaginationDemo_{Guid.NewGuid()}");
|
||||
_db = builder.Build();
|
||||
|
||||
// 添加测试数据:100 个产品
|
||||
var repo = _db.Repository<Product>();
|
||||
var products = new List<Product>();
|
||||
for (int i = 1; i <= 100; i++)
|
||||
{
|
||||
products.Add(new Product
|
||||
{
|
||||
Name = $"产品{i:D3}",
|
||||
Price = (i % 10 + 1) * 100m,
|
||||
Stock = i * 5,
|
||||
Description = i % 2 == 0 ? "热门产品" : "普通产品"
|
||||
});
|
||||
}
|
||||
await repo.AddRangeAsync(products);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine("✓ 初始化完成:创建了 100 个测试产品\n");
|
||||
}
|
||||
|
||||
[TestCleanup]
|
||||
public void Cleanup()
|
||||
{
|
||||
_db?.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 基础分页查询
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Demo_BasicPagination()
|
||||
{
|
||||
var repo = _db.Repository<Product>();
|
||||
|
||||
// 查询第 1 页,每页 10 条
|
||||
var page1 = await repo.GetPagedAsync(
|
||||
page: 1,
|
||||
pageSize: 10);
|
||||
|
||||
Console.WriteLine($"第 1 页:");
|
||||
Console.WriteLine($" 当前页: {page1.PageIndex}");
|
||||
Console.WriteLine($" 每页数量: {page1.PageSize}");
|
||||
Console.WriteLine($" 总记录数: {page1.TotalCount}");
|
||||
Console.WriteLine($" 总页数: {page1.TotalPages}");
|
||||
Console.WriteLine($" 是否有上一页: {page1.HasPreviousPage}");
|
||||
Console.WriteLine($" 是否有下一页: {page1.HasNextPage}");
|
||||
|
||||
// 验证
|
||||
Assert.AreEqual(1, page1.PageIndex);
|
||||
Assert.AreEqual(10, page1.PageSize);
|
||||
Assert.AreEqual(100, page1.TotalCount);
|
||||
Assert.AreEqual(10, page1.TotalPages);
|
||||
Assert.AreEqual(10, page1.Items.Count);
|
||||
Assert.IsFalse(page1.HasPreviousPage);
|
||||
Assert.IsTrue(page1.HasNextPage);
|
||||
|
||||
Console.WriteLine($"\n产品列表:");
|
||||
foreach (var product in page1.Items)
|
||||
{
|
||||
Console.WriteLine($" - {product.Name}: {product.Price:C}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 遍历所有页
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Demo_IterateAllPages()
|
||||
{
|
||||
var repo = _db.Repository<Product>();
|
||||
int pageSize = 15;
|
||||
int currentPage = 1;
|
||||
|
||||
Console.WriteLine($"=== 遍历所有页(每页 {pageSize} 条)===\n");
|
||||
|
||||
PagedResult<Product> pageResult;
|
||||
do
|
||||
{
|
||||
pageResult = await repo.GetPagedAsync(currentPage, pageSize);
|
||||
|
||||
Console.WriteLine($"第 {currentPage} 页: {pageResult.Items.Count} 条记录");
|
||||
Console.WriteLine($" 范围: {(currentPage - 1) * pageSize + 1} - {Math.Min(currentPage * pageSize, pageResult.TotalCount)}");
|
||||
|
||||
// 处理当前页数据
|
||||
foreach (var product in pageResult.Items)
|
||||
{
|
||||
// 业务逻辑...
|
||||
}
|
||||
|
||||
currentPage++;
|
||||
|
||||
} while (pageResult.HasNextPage);
|
||||
|
||||
Console.WriteLine($"\n✓ 总共遍历了 {currentPage - 1} 页");
|
||||
Assert.AreEqual(7, currentPage - 1); // 100 / 15 = 7 页
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 带条件的分页查询
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Demo_PaginationWithFilter()
|
||||
{
|
||||
var repo = _db.Repository<Product>();
|
||||
|
||||
// 查询价格 > 500 的产品,第 1 页,每页 10 条
|
||||
var result = await repo.GetPagedAsync(
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
predicate: p => p.Price > 500m);
|
||||
|
||||
Console.WriteLine($"查询条件: 价格 > 500");
|
||||
Console.WriteLine($" 总记录数: {result.TotalCount}");
|
||||
Console.WriteLine($" 总页数: {result.TotalPages}");
|
||||
Console.WriteLine($" 当前页记录数: {result.Items.Count}");
|
||||
|
||||
Console.WriteLine($"\n产品列表:");
|
||||
foreach (var product in result.Items)
|
||||
{
|
||||
Console.WriteLine($" - {product.Name}: {product.Price:C}");
|
||||
Assert.IsTrue(product.Price > 500m);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 带排序的分页查询
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Demo_PaginationWithOrdering()
|
||||
{
|
||||
var repo = _db.Repository<Product>();
|
||||
|
||||
// 按价格降序排列
|
||||
var result = await repo.GetPagedAsync(
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
predicate: null,
|
||||
orderBy: q => q.OrderByDescending(p => p.Price));
|
||||
|
||||
Console.WriteLine($"排序: 按价格降序");
|
||||
Console.WriteLine($"\n前 10 个最贵的产品:");
|
||||
foreach (var product in result.Items)
|
||||
{
|
||||
Console.WriteLine($" - {product.Name}: {product.Price:C}");
|
||||
}
|
||||
|
||||
// 验证排序
|
||||
for (int i = 0; i < result.Items.Count - 1; i++)
|
||||
{
|
||||
Assert.IsTrue(result.Items[i].Price >= result.Items[i + 1].Price);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 复杂分页查询 - 组合条件、排序、分页
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Demo_ComplexPagination()
|
||||
{
|
||||
var repo = _db.Repository<Product>();
|
||||
|
||||
// 查询热门产品,价格 > 300,按库存降序,第 2 页,每页 5 条
|
||||
var result = await repo.GetPagedAsync(
|
||||
page: 2,
|
||||
pageSize: 5,
|
||||
predicate: p => p.Description == "热门产品" && p.Price > 300m,
|
||||
orderBy: q => q.OrderByDescending(p => p.Stock));
|
||||
|
||||
Console.WriteLine($"复杂查询:");
|
||||
Console.WriteLine($" 条件: 热门产品 AND 价格>300");
|
||||
Console.WriteLine($" 排序: 按库存降序");
|
||||
Console.WriteLine($" 分页: 第 2 页,每页 5 条");
|
||||
Console.WriteLine($"\n结果:");
|
||||
Console.WriteLine($" 总记录数: {result.TotalCount}");
|
||||
Console.WriteLine($" 总页数: {result.TotalPages}");
|
||||
Console.WriteLine($" 当前页码: {result.PageIndex}");
|
||||
|
||||
Console.WriteLine($"\n产品列表:");
|
||||
foreach (var product in result.Items)
|
||||
{
|
||||
Console.WriteLine($" - {product.Name}: {product.Price:C}, 库存: {product.Stock}");
|
||||
Assert.AreEqual("热门产品", product.Description);
|
||||
Assert.IsTrue(product.Price > 300m);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 分页辅助信息的使用
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Demo_PaginationHelpers()
|
||||
{
|
||||
var repo = _db.Repository<Product>();
|
||||
|
||||
// 第 5 页,每页 10 条
|
||||
var result = await repo.GetPagedAsync(5, 10);
|
||||
|
||||
Console.WriteLine($"=== 分页辅助信息 ===\n");
|
||||
Console.WriteLine($"当前页: {result.PageIndex}");
|
||||
Console.WriteLine($"每页数量: {result.PageSize}");
|
||||
Console.WriteLine($"总记录数: {result.TotalCount}");
|
||||
Console.WriteLine($"总页数: {result.TotalPages}");
|
||||
Console.WriteLine($"是否第一页: {result.PageIndex == 1}");
|
||||
Console.WriteLine($"是否最后一页: {result.PageIndex == result.TotalPages}");
|
||||
Console.WriteLine($"有上一页: {result.HasPreviousPage}");
|
||||
Console.WriteLine($"有下一页: {result.HasNextPage}");
|
||||
|
||||
// 计算记录范围
|
||||
int startIndex = (result.PageIndex - 1) * result.PageSize + 1;
|
||||
int endIndex = Math.Min(result.PageIndex * result.PageSize, result.TotalCount);
|
||||
Console.WriteLine($"\n记录范围: {startIndex} - {endIndex}");
|
||||
|
||||
// 验证
|
||||
Assert.AreEqual(5, result.PageIndex);
|
||||
Assert.AreEqual(41, startIndex);
|
||||
Assert.AreEqual(50, endIndex);
|
||||
Assert.IsTrue(result.HasPreviousPage);
|
||||
Assert.IsTrue(result.HasNextPage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 空结果的分页
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Demo_EmptyPageResult()
|
||||
{
|
||||
var repo = _db.Repository<Product>();
|
||||
|
||||
// 查询不存在的条件
|
||||
var result = await repo.GetPagedAsync(
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
predicate: p => p.Price > 99999m);
|
||||
|
||||
Console.WriteLine($"查询不存在的记录:");
|
||||
Console.WriteLine($" 总记录数: {result.TotalCount}");
|
||||
Console.WriteLine($" 总页数: {result.TotalPages}");
|
||||
Console.WriteLine($" 当前页记录数: {result.Items.Count}");
|
||||
|
||||
Assert.AreEqual(0, result.TotalCount);
|
||||
Assert.AreEqual(0, result.TotalPages);
|
||||
Assert.AreEqual(0, result.Items.Count);
|
||||
Assert.IsFalse(result.HasPreviousPage);
|
||||
Assert.IsFalse(result.HasNextPage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 超出范围的页码
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Demo_OutOfRangePage()
|
||||
{
|
||||
var repo = _db.Repository<Product>();
|
||||
|
||||
// 请求第 999 页(超出实际页数)
|
||||
var result = await repo.GetPagedAsync(999, 10);
|
||||
|
||||
Console.WriteLine($"请求超出范围的页码 (第 999 页):");
|
||||
Console.WriteLine($" 总记录数: {result.TotalCount}");
|
||||
Console.WriteLine($" 总页数: {result.TotalPages}");
|
||||
Console.WriteLine($" 当前页码: {result.PageIndex}");
|
||||
Console.WriteLine($" 当前页记录数: {result.Items.Count}");
|
||||
|
||||
// 超出范围返回空列表
|
||||
Assert.AreEqual(0, result.Items.Count);
|
||||
Assert.AreEqual(999, result.PageIndex);
|
||||
Assert.IsFalse(result.HasNextPage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 在 UI 中使用分页
|
||||
///
|
||||
/// 模拟 Web API 或 WPF 应用的分页场景
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Demo_PaginationInUI()
|
||||
{
|
||||
var repo = _db.Repository<Product>();
|
||||
|
||||
// 模拟用户请求
|
||||
int requestedPage = 3;
|
||||
int pageSize = 20;
|
||||
string? searchTerm = "产品0"; // 用户搜索
|
||||
|
||||
Console.WriteLine($"=== UI 分页场景 ===");
|
||||
Console.WriteLine($"用户请求: 第 {requestedPage} 页, 每页 {pageSize} 条");
|
||||
Console.WriteLine($"搜索词: {searchTerm}\n");
|
||||
|
||||
// 执行分页查询
|
||||
var result = await repo.GetPagedAsync(
|
||||
page: requestedPage,
|
||||
pageSize: pageSize,
|
||||
predicate: string.IsNullOrEmpty(searchTerm)
|
||||
? null
|
||||
: p => p.Name.Contains(searchTerm),
|
||||
orderBy: q => q.OrderBy(p => p.Name));
|
||||
|
||||
// 构建返回给前端的数据
|
||||
var uiData = new
|
||||
{
|
||||
Success = true,
|
||||
Data = new
|
||||
{
|
||||
Items = result.Items.Select(p => new
|
||||
{
|
||||
p.Id,
|
||||
p.Name,
|
||||
p.Price,
|
||||
p.Stock
|
||||
}).ToList(),
|
||||
Pagination = new
|
||||
{
|
||||
result.PageIndex,
|
||||
result.PageSize,
|
||||
result.TotalCount,
|
||||
result.TotalPages,
|
||||
result.HasPreviousPage,
|
||||
result.HasNextPage
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Console.WriteLine($"返回数据:");
|
||||
Console.WriteLine($" 记录数: {uiData.Data.Items.Count}");
|
||||
Console.WriteLine($" 分页信息: {uiData.Data.Pagination.PageIndex}/{uiData.Data.Pagination.TotalPages}");
|
||||
Console.WriteLine($"\n✓ 可以直接序列化为 JSON 返回给前端");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: PagedResult 来自 Common 模块
|
||||
///
|
||||
/// Sharp.Data 复用 Common.ExpandFun.PagedResult<T>,避免重复定义
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Demo_PagedResultFromCommonModule()
|
||||
{
|
||||
var repo = _db.Repository<Product>();
|
||||
|
||||
var result = await repo.GetPagedAsync(1, 10);
|
||||
|
||||
// PagedResult<T> 来自 Common.ExpandFun 命名空间
|
||||
Console.WriteLine($"✓ PagedResult<T> 的类型信息:");
|
||||
Console.WriteLine($" 类型: {result.GetType().FullName}");
|
||||
Console.WriteLine($" 命名空间: {result.GetType().Namespace}");
|
||||
Console.WriteLine($"\n✓ 复用 Common 模块,避免重复造轮子");
|
||||
|
||||
Assert.AreEqual("Common.ExpandFun", result.GetType().Namespace);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 自定义扩展方法进行分页
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Demo_CustomPaginationExtension()
|
||||
{
|
||||
// 使用 EF Core 的 IQueryable 直接分页
|
||||
var query = _db.Context.Set<Product>()
|
||||
.Where(p => p.Price > 500m)
|
||||
.OrderBy(p => p.Name);
|
||||
|
||||
// 手动实现分页逻辑
|
||||
int page = 2;
|
||||
int pageSize = 10;
|
||||
|
||||
var totalCount = await query.CountAsync();
|
||||
var items = await query
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync();
|
||||
|
||||
// 构建 PagedResult
|
||||
var result = new PagedResult<Product>
|
||||
{
|
||||
Items = items,
|
||||
TotalCount = totalCount,
|
||||
PageIndex = page,
|
||||
PageSize = pageSize
|
||||
};
|
||||
|
||||
Console.WriteLine($"自定义分页:");
|
||||
Console.WriteLine($" 总记录数: {result.TotalCount}");
|
||||
Console.WriteLine($" 当前页: {result.PageIndex}");
|
||||
Console.WriteLine($" 记录数: {result.Items.Count}");
|
||||
|
||||
Console.WriteLine($"\n✓ 可以直接使用 IQueryable 和 PagedResult 进行自定义分页");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,385 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Sharp.Data;
|
||||
using Sharp.Data.Context;
|
||||
|
||||
namespace Data.Test;
|
||||
|
||||
/// <summary>
|
||||
/// 演示 06: 自定义实体(不使用基类)
|
||||
///
|
||||
/// 本测试类演示:
|
||||
/// 1. 完全自定义的实体定义
|
||||
/// 2. 不使用任何基类的实体
|
||||
/// 3. 使用特殊主键类型(字符串、复合主键等)
|
||||
/// 4. 仓储对自定义实体的支持
|
||||
/// 5. 灵活性和自主控制
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class _06_CustomEntityTests
|
||||
{
|
||||
/// <summary>
|
||||
/// 演示: 完全自定义的实体
|
||||
///
|
||||
/// CustomProduct 不使用任何基类,完全自定义
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Demo_FullyCustomEntity()
|
||||
{
|
||||
var builder = new DataContextBuilder<TestDbContext>();
|
||||
builder.UseInMemoryDatabase($"CustomEntityDemo_{Guid.NewGuid()}");
|
||||
|
||||
using var db = builder.Build();
|
||||
var repo = db.Repository<CustomProduct>();
|
||||
|
||||
Console.WriteLine("=== 完全自定义实体 ===\n");
|
||||
Console.WriteLine("CustomProduct:");
|
||||
Console.WriteLine(" - 不继承任何基类");
|
||||
Console.WriteLine(" - 不实现任何接口");
|
||||
Console.WriteLine(" - 完全自主定义所有字段");
|
||||
Console.WriteLine(" - 仍然可以使用仓储模式\n");
|
||||
|
||||
// 创建自定义产品
|
||||
var product = new CustomProduct
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = "自定义产品",
|
||||
Price = 299.99m,
|
||||
CreatedDate = DateTime.UtcNow
|
||||
};
|
||||
|
||||
// 使用仓储添加
|
||||
await repo.AddAsync(product);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine($"✓ 添加成功:");
|
||||
Console.WriteLine($" ID: {product.Id}");
|
||||
Console.WriteLine($" Name: {product.Name}");
|
||||
Console.WriteLine($" Price: {product.Price:C}");
|
||||
Console.WriteLine($" CreatedDate: {product.CreatedDate:yyyy-MM-dd HH:mm:ss}");
|
||||
Console.WriteLine($" IsExpensive: {product.IsExpensive}");
|
||||
|
||||
// 查询
|
||||
var found = await repo.GetByIdAsync(product.Id);
|
||||
Assert.IsNotNull(found);
|
||||
Assert.AreEqual(product.Name, found.Name);
|
||||
|
||||
Console.WriteLine($"\n✓ 查询成功,完全自定义实体照样工作!");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 字符串主键实体
|
||||
///
|
||||
/// Region 使用字符串作为主键
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Demo_StringPrimaryKey()
|
||||
{
|
||||
var builder = new DataContextBuilder<TestDbContext>();
|
||||
builder.UseInMemoryDatabase($"StringPKDemo_{Guid.NewGuid()}");
|
||||
|
||||
using var db = builder.Build();
|
||||
|
||||
// Region 不能使用泛型仓储,因为主键不是 Guid
|
||||
// 但可以直接使用 DbContext
|
||||
var regions = db.Context.Set<Region>();
|
||||
|
||||
Console.WriteLine("=== 字符串主键实体 ===\n");
|
||||
|
||||
// 添加地区
|
||||
var china = new Region
|
||||
{
|
||||
Code = "CN",
|
||||
Name = "中国",
|
||||
Description = "中华人民共和国"
|
||||
};
|
||||
|
||||
var usa = new Region
|
||||
{
|
||||
Code = "US",
|
||||
Name = "美国",
|
||||
Description = "美利坚合众国"
|
||||
};
|
||||
|
||||
regions.Add(china);
|
||||
regions.Add(usa);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine($"✓ 添加地区:");
|
||||
Console.WriteLine($" {china.Code}: {china.Name}");
|
||||
Console.WriteLine($" {usa.Code}: {usa.Name}");
|
||||
|
||||
// 根据字符串主键查询
|
||||
var foundChina = await regions.FindAsync("CN");
|
||||
Assert.IsNotNull(foundChina);
|
||||
Assert.AreEqual("中国", foundChina.Name);
|
||||
|
||||
Console.WriteLine($"\n✓ 根据字符串主键查询成功: {foundChina.Name}");
|
||||
|
||||
// 列出所有地区
|
||||
var allRegions = await regions.ToListAsync();
|
||||
Console.WriteLine($"\n所有地区:");
|
||||
foreach (var region in allRegions)
|
||||
{
|
||||
Console.WriteLine($" [{region.Code}] {region.Name}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 自增 Int 主键实体
|
||||
///
|
||||
/// Tag 使用 EntityInt 基类,主键是自增 Int
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Demo_AutoIncrementIntPrimaryKey()
|
||||
{
|
||||
var builder = new DataContextBuilder<TestDbContext>();
|
||||
builder.UseInMemoryDatabase($"IntPKDemo_{Guid.NewGuid()}");
|
||||
|
||||
using var db = builder.Build();
|
||||
var repo = db.Repository<Tag>();
|
||||
|
||||
Console.WriteLine("=== 自增 Int 主键实体 ===\n");
|
||||
|
||||
// 添加标签(不需要手动设置 Id)
|
||||
var tags = new[]
|
||||
{
|
||||
new Tag { Name = "技术", Color = "#2196F3" },
|
||||
new Tag { Name = "生活", Color = "#4CAF50" },
|
||||
new Tag { Name = "娱乐", Color = "#FF9800" }
|
||||
};
|
||||
|
||||
await repo.AddRangeAsync(tags);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine($"✓ 添加标签(Id 自动生成):");
|
||||
foreach (var tag in tags)
|
||||
{
|
||||
Console.WriteLine($" ID: {tag.Id}, Name: {tag.Name}, Color: {tag.Color}");
|
||||
Assert.IsTrue(tag.Id > 0); // 自增 Id
|
||||
}
|
||||
|
||||
// 查询
|
||||
var foundTag = await repo.GetByIdAsync(1);
|
||||
Assert.IsNotNull(foundTag);
|
||||
Console.WriteLine($"\n✓ 根据 Int 主键查询: {foundTag.Name}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 包含值对象的实体
|
||||
///
|
||||
/// Customer 包含 Address 值对象
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Demo_EntityWithValueObject()
|
||||
{
|
||||
var builder = new DataContextBuilder<TestDbContext>();
|
||||
builder.UseInMemoryDatabase($"ValueObjectDemo_{Guid.NewGuid()}")
|
||||
.UseAudit(true)
|
||||
.UseCurrentUser("admin");
|
||||
|
||||
using var db = builder.Build();
|
||||
var repo = db.Repository<Customer>();
|
||||
|
||||
Console.WriteLine("=== 包含值对象的实体 ===\n");
|
||||
|
||||
// 创建客户(包含地址值对象)
|
||||
var customer = new Customer
|
||||
{
|
||||
Name = "张三",
|
||||
Email = "zhangsan@example.com",
|
||||
ShippingAddress = new Address
|
||||
{
|
||||
Street = "中关村大街1号",
|
||||
City = "北京市",
|
||||
State = "北京",
|
||||
ZipCode = "100080",
|
||||
Country = "中国"
|
||||
},
|
||||
BillingAddress = new Address
|
||||
{
|
||||
Street = "五道口路2号",
|
||||
City = "北京市",
|
||||
State = "北京",
|
||||
ZipCode = "100081",
|
||||
Country = "中国"
|
||||
}
|
||||
};
|
||||
|
||||
await repo.AddAsync(customer);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine($"✓ 客户信息:");
|
||||
Console.WriteLine($" 姓名: {customer.Name}");
|
||||
Console.WriteLine($" 邮箱: {customer.Email}");
|
||||
Console.WriteLine($"\n 收货地址:");
|
||||
Console.WriteLine($" {customer.ShippingAddress.Street}");
|
||||
Console.WriteLine($" {customer.ShippingAddress.City}, {customer.ShippingAddress.ZipCode}");
|
||||
Console.WriteLine($"\n 账单地址:");
|
||||
Console.WriteLine($" {customer.BillingAddress.Street}");
|
||||
Console.WriteLine($" {customer.BillingAddress.City}, {customer.BillingAddress.ZipCode}");
|
||||
|
||||
// 查询
|
||||
var found = await repo.GetByIdAsync(customer.Id);
|
||||
Assert.IsNotNull(found);
|
||||
Assert.IsNotNull(found.ShippingAddress);
|
||||
Assert.AreEqual("中关村大街1号", found.ShippingAddress.Street);
|
||||
|
||||
Console.WriteLine($"\n✓ 值对象被正确保存和查询");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 混合使用不同类型的实体
|
||||
///
|
||||
/// 在同一个应用中可以混合使用各种实体定义方式
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Demo_MixedEntityTypes()
|
||||
{
|
||||
var builder = new DataContextBuilder<TestDbContext>();
|
||||
builder.UseInMemoryDatabase($"MixedDemo_{Guid.NewGuid()}")
|
||||
.UseAudit(true)
|
||||
.UseSoftDelete(true)
|
||||
.UseCurrentUser("admin");
|
||||
|
||||
using var db = builder.Build();
|
||||
|
||||
Console.WriteLine("=== 混合使用不同实体 ===\n");
|
||||
|
||||
// 1. 使用 AuditableEntity (带审计和软删除)
|
||||
var productRepo = db.Repository<Product>();
|
||||
var product = new Product { Name = "产品A", Price = 100m, Stock = 10 };
|
||||
await productRepo.AddAsync(product);
|
||||
|
||||
Console.WriteLine($"1. Product (AuditableEntity):");
|
||||
Console.WriteLine($" 支持: 审计 + 软删除");
|
||||
|
||||
// 2. 使用 Entity (只有 Id)
|
||||
var categoryRepo = db.Repository<Category>();
|
||||
var category = new Category { Name = "分类A" };
|
||||
await categoryRepo.AddAsync(category);
|
||||
|
||||
Console.WriteLine($"\n2. Category (Entity):");
|
||||
Console.WriteLine($" 支持: 仅基础 CRUD");
|
||||
|
||||
// 3. 完全自定义
|
||||
var customRepo = db.Repository<CustomProduct>();
|
||||
var custom = new CustomProduct
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = "自定义产品",
|
||||
Price = 200m,
|
||||
CreatedDate = DateTime.UtcNow
|
||||
};
|
||||
await customRepo.AddAsync(custom);
|
||||
|
||||
Console.WriteLine($"\n3. CustomProduct (完全自定义):");
|
||||
Console.WriteLine($" 支持: 自定义字段和逻辑");
|
||||
|
||||
// 4. 实现接口 (自定义+审计)
|
||||
var orderRepo = db.Repository<Order>();
|
||||
var order = new Order
|
||||
{
|
||||
OrderNumber = "ORD-001",
|
||||
TotalAmount = 500m
|
||||
};
|
||||
await orderRepo.AddAsync(order);
|
||||
|
||||
Console.WriteLine($"\n4. Order (实现接口):");
|
||||
Console.WriteLine($" 支持: 自定义实现 + 审计");
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine($"\n✓ 所有实体类型都能正常工作!");
|
||||
Console.WriteLine($"✓ 根据需求选择合适的实体定义方式");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 自定义实体的查询
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Demo_QueryCustomEntities()
|
||||
{
|
||||
var builder = new DataContextBuilder<TestDbContext>();
|
||||
builder.UseInMemoryDatabase($"QueryCustomDemo_{Guid.NewGuid()}");
|
||||
|
||||
using var db = builder.Build();
|
||||
var repo = db.Repository<CustomProduct>();
|
||||
|
||||
// 添加测试数据
|
||||
for (int i = 1; i <= 20; i++)
|
||||
{
|
||||
await repo.AddAsync(new CustomProduct
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = $"产品{i}",
|
||||
Price = i * 100m,
|
||||
CreatedDate = DateTime.UtcNow.AddDays(-i)
|
||||
});
|
||||
}
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine("=== 自定义实体的查询 ===\n");
|
||||
|
||||
// 1. 条件查询
|
||||
var expensiveProducts = await repo.FindAsync(p => p.Price > 1000m);
|
||||
Console.WriteLine($"1. 价格 > 1000 的产品: {expensiveProducts.Count} 个");
|
||||
|
||||
// 2. 使用业务属性查询
|
||||
var veryExpensive = await repo.Query()
|
||||
.Where(p => p.IsExpensive) // 使用计算属性
|
||||
.ToListAsync();
|
||||
Console.WriteLine($"2. 使用 IsExpensive 属性: {veryExpensive.Count} 个");
|
||||
|
||||
// 3. 复杂查询
|
||||
var recentAndCheap = await repo.Query()
|
||||
.Where(p => p.CreatedDate > DateTime.UtcNow.AddDays(-7) && p.Price < 500m)
|
||||
.OrderByDescending(p => p.CreatedDate)
|
||||
.ToListAsync();
|
||||
Console.WriteLine($"3. 最近7天创建且价格<500: {recentAndCheap.Count} 个");
|
||||
|
||||
// 4. 分页查询
|
||||
var paged = await repo.GetPagedAsync(1, 5);
|
||||
Console.WriteLine($"4. 分页查询: 第1页 {paged.Items.Count} 条,共 {paged.TotalCount} 条");
|
||||
|
||||
Console.WriteLine($"\n✓ 自定义实体支持所有标准查询操作");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 为什么要使用自定义实体?
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void Demo_WhyCustomEntities()
|
||||
{
|
||||
Console.WriteLine("=== 为什么使用自定义实体? ===\n");
|
||||
|
||||
Console.WriteLine("1. 已有实体模型");
|
||||
Console.WriteLine(" - 项目已经有定义好的实体");
|
||||
Console.WriteLine(" - 不想改动现有代码");
|
||||
Console.WriteLine(" - 快速集成 Sharp.Data\n");
|
||||
|
||||
Console.WriteLine("2. 特殊主键类型");
|
||||
Console.WriteLine(" - 使用字符串作为主键(如: 地区代码)");
|
||||
Console.WriteLine(" - 使用复合主键");
|
||||
Console.WriteLine(" - 使用自增 Int 主键\n");
|
||||
|
||||
Console.WriteLine("3. 不需要框架功能");
|
||||
Console.WriteLine(" - 不需要审计");
|
||||
Console.WriteLine(" - 不需要软删除");
|
||||
Console.WriteLine(" - 只需要基础 CRUD\n");
|
||||
|
||||
Console.WriteLine("4. 完全自主控制");
|
||||
Console.WriteLine(" - 自定义字段名称");
|
||||
Console.WriteLine(" - 自定义业务逻辑");
|
||||
Console.WriteLine(" - 不依赖框架约束\n");
|
||||
|
||||
Console.WriteLine("5. 灵活性最高");
|
||||
Console.WriteLine(" - 可以在同一项目中混用");
|
||||
Console.WriteLine(" - 根据实体特点选择合适方式");
|
||||
Console.WriteLine(" - 渐进式采用 Sharp.Data\n");
|
||||
|
||||
Console.WriteLine("✓ 总结:");
|
||||
Console.WriteLine(" Sharp.Data 的基类和接口都是可选的");
|
||||
Console.WriteLine(" 你可以完全自定义,仍然享受仓储模式的便利");
|
||||
Console.WriteLine(" 灵活性和便利性可以兼得!");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,496 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Sharp.Data;
|
||||
using Sharp.Data.Context;
|
||||
|
||||
namespace Data.Test;
|
||||
|
||||
/// <summary>
|
||||
/// 演示 07: 降级使用 EF Core
|
||||
///
|
||||
/// 本测试类演示:
|
||||
/// 1. 如何绕过 Sharp.Data 直接使用 EF Core
|
||||
/// 2. 什么时候需要降级使用
|
||||
/// 3. 执行原生 SQL
|
||||
/// 4. 事务管理
|
||||
/// 5. 高级查询
|
||||
/// 6. 性能优化
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class _07_EFCoreDirectUsageTests
|
||||
{
|
||||
/// <summary>
|
||||
/// 演示: 访问底层 DbContext
|
||||
///
|
||||
/// Sharp.Data 不会阻止你使用 EF Core 的原生功能
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void Demo_AccessUnderlyingDbContext()
|
||||
{
|
||||
var builder = new DataContextBuilder<TestDbContext>();
|
||||
builder.UseInMemoryDatabase($"DirectAccessDemo_{Guid.NewGuid()}");
|
||||
|
||||
using var db = builder.Build();
|
||||
|
||||
// 访问底层 DbContext
|
||||
var context = db.Context;
|
||||
|
||||
Console.WriteLine("=== 访问底层 DbContext ===\n");
|
||||
Console.WriteLine($"✓ DbContext 类型: {context.GetType().Name}");
|
||||
Console.WriteLine($"✓ 数据库提供程序: {context.Database.ProviderName}");
|
||||
Console.WriteLine($"\n可以使用所有 EF Core 功能:");
|
||||
Console.WriteLine($" - context.Set<T>()");
|
||||
Console.WriteLine($" - context.Database.*");
|
||||
Console.WriteLine($" - context.Entry(entity)");
|
||||
Console.WriteLine($" - 原生 SQL 查询");
|
||||
Console.WriteLine($" - 事务管理");
|
||||
Console.WriteLine($" - 等等...");
|
||||
|
||||
Assert.IsNotNull(context);
|
||||
Assert.IsInstanceOfType<TestDbContext>(context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 直接使用 DbSet
|
||||
///
|
||||
/// 绕过仓储,直接操作 DbSet
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Demo_DirectDbSetUsage()
|
||||
{
|
||||
var builder = new DataContextBuilder<TestDbContext>();
|
||||
builder.UseInMemoryDatabase($"DbSetDemo_{Guid.NewGuid()}");
|
||||
|
||||
using var db = builder.Build();
|
||||
|
||||
Console.WriteLine("=== 直接使用 DbSet ===\n");
|
||||
|
||||
// 直接获取 DbSet
|
||||
var products = db.Context.Set<Product>();
|
||||
|
||||
// EF Core 原生操作
|
||||
var product = new Product
|
||||
{
|
||||
Name = "直接添加的产品",
|
||||
Price = 999m,
|
||||
Stock = 50
|
||||
};
|
||||
|
||||
products.Add(product);
|
||||
await db.Context.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine($"✓ 使用 DbSet.Add() 添加产品");
|
||||
|
||||
// 原生查询
|
||||
var found = await products
|
||||
.Where(p => p.Price > 500)
|
||||
.OrderByDescending(p => p.Price)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
Assert.IsNotNull(found);
|
||||
Console.WriteLine($"✓ 使用 LINQ 查询: {found.Name}");
|
||||
|
||||
// 更新
|
||||
found.Price = 1099m;
|
||||
await db.Context.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine($"✓ 使用 EF Core 跟踪更新: 新价格 {found.Price:C}");
|
||||
|
||||
Console.WriteLine($"\n✓ 完全兼容 EF Core 原生操作!");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 执行原生 SQL 查询
|
||||
///
|
||||
/// 有时需要执行复杂的 SQL 或存储过程
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Demo_RawSqlQuery()
|
||||
{
|
||||
var builder = new DataContextBuilder<TestDbContext>();
|
||||
builder.UseInMemoryDatabase($"RawSqlDemo_{Guid.NewGuid()}");
|
||||
|
||||
using var db = builder.Build();
|
||||
var repo = db.Repository<Product>();
|
||||
|
||||
// 先添加测试数据
|
||||
await repo.AddRangeAsync(new[]
|
||||
{
|
||||
new Product { Name = "产品A", Price = 100m, Stock = 10 },
|
||||
new Product { Name = "产品B", Price = 200m, Stock = 20 },
|
||||
new Product { Name = "产品C", Price = 300m, Stock = 30 }
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine("=== 执行原生 SQL ===\n");
|
||||
|
||||
// 注意:InMemory 数据库不支持原生 SQL
|
||||
// 这里演示语法,实际使用时需要真实数据库
|
||||
|
||||
Console.WriteLine("1. 查询型原生 SQL (FromSqlRaw):");
|
||||
Console.WriteLine(" var products = await context.Products");
|
||||
Console.WriteLine(" .FromSqlRaw(\"SELECT * FROM Products WHERE Price > {0}\", 150)");
|
||||
Console.WriteLine(" .ToListAsync();");
|
||||
|
||||
Console.WriteLine("\n2. 执行型原生 SQL (ExecuteSqlRaw):");
|
||||
Console.WriteLine(" var affected = await context.Database");
|
||||
Console.WriteLine(" .ExecuteSqlRawAsync(\"UPDATE Products SET Price = Price * 1.1\");");
|
||||
|
||||
Console.WriteLine("\n3. 调用存储过程:");
|
||||
Console.WriteLine(" var result = await context.Database");
|
||||
Console.WriteLine(" .ExecuteSqlRawAsync(\"EXEC UpdateProductPrice @ProductId, @NewPrice\",");
|
||||
Console.WriteLine(" new SqlParameter(\"@ProductId\", productId),");
|
||||
Console.WriteLine(" new SqlParameter(\"@NewPrice\", newPrice));");
|
||||
|
||||
Console.WriteLine("\n✓ Sharp.Data 不限制原生 SQL 的使用");
|
||||
Console.WriteLine("✓ 适用于复杂查询、性能优化、遗留 SQL 迁移等场景");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 事务管理
|
||||
///
|
||||
/// 使用事务确保数据一致性
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Demo_TransactionManagement()
|
||||
{
|
||||
var builder = new DataContextBuilder<TestDbContext>();
|
||||
builder.UseInMemoryDatabase($"TransactionDemo_{Guid.NewGuid()}");
|
||||
|
||||
using var db = builder.Build();
|
||||
var productRepo = db.Repository<Product>();
|
||||
var categoryRepo = db.Repository<Category>();
|
||||
|
||||
Console.WriteLine("=== 事务管理 ===\n");
|
||||
|
||||
// 方式1: 使用 using 语句自动管理事务
|
||||
Console.WriteLine("方式1: 使用 BeginTransaction");
|
||||
using (var transaction = await db.Context.Database.BeginTransactionAsync())
|
||||
{
|
||||
try
|
||||
{
|
||||
// 操作1: 添加产品
|
||||
var product = new Product
|
||||
{
|
||||
Name = "事务测试产品",
|
||||
Price = 100m,
|
||||
Stock = 10
|
||||
};
|
||||
await productRepo.AddAsync(product);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine($" ✓ 添加产品: {product.Name}");
|
||||
|
||||
// 操作2: 添加分类
|
||||
var category = new Category { Name = "事务测试分类" };
|
||||
await categoryRepo.AddAsync(category);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine($" ✓ 添加分类: {category.Name}");
|
||||
|
||||
// 提交事务
|
||||
await transaction.CommitAsync();
|
||||
Console.WriteLine($" ✓ 事务提交成功");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 回滚事务
|
||||
await transaction.RollbackAsync();
|
||||
Console.WriteLine($" ✗ 事务回滚: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// 方式2: 使用 SaveChanges 的隐式事务
|
||||
Console.WriteLine("\n方式2: SaveChanges 的隐式事务");
|
||||
Console.WriteLine(" EF Core 的 SaveChanges 本身就是事务性的");
|
||||
Console.WriteLine(" 多个操作在一次 SaveChanges 中是原子的");
|
||||
|
||||
var product2 = new Product { Name = "产品2", Price = 200m, Stock = 20 };
|
||||
var product3 = new Product { Name = "产品3", Price = 300m, Stock = 30 };
|
||||
await productRepo.AddAsync(product2);
|
||||
await productRepo.AddAsync(product3);
|
||||
await db.SaveChangesAsync(); // 两个产品作为一个事务提交
|
||||
|
||||
Console.WriteLine($" ✓ 隐式事务添加了 2 个产品");
|
||||
|
||||
Console.WriteLine($"\n✓ Sharp.Data 完全支持 EF Core 事务管理");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 批量操作优化
|
||||
///
|
||||
/// 对大量数据的高效操作
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Demo_BulkOperationsOptimization()
|
||||
{
|
||||
var builder = new DataContextBuilder<TestDbContext>();
|
||||
builder.UseInMemoryDatabase($"BulkDemo_{Guid.NewGuid()}");
|
||||
|
||||
using var db = builder.Build();
|
||||
|
||||
Console.WriteLine("=== 批量操作优化 ===\n");
|
||||
|
||||
Console.WriteLine("1. 批量插入(推荐方式):");
|
||||
var products = new List<Product>();
|
||||
for (int i = 1; i <= 1000; i++)
|
||||
{
|
||||
products.Add(new Product
|
||||
{
|
||||
Name = $"产品{i}",
|
||||
Price = i * 10m,
|
||||
Stock = i
|
||||
});
|
||||
}
|
||||
|
||||
var startTime = DateTime.UtcNow;
|
||||
|
||||
// 使用 AddRange 一次性添加
|
||||
await db.Context.Set<Product>().AddRangeAsync(products);
|
||||
await db.Context.SaveChangesAsync();
|
||||
|
||||
var elapsed = (DateTime.UtcNow - startTime).TotalMilliseconds;
|
||||
Console.WriteLine($" ✓ 一次性插入 1000 条记录: {elapsed:F2}ms");
|
||||
|
||||
Console.WriteLine("\n2. 批量更新:");
|
||||
Console.WriteLine(" var products = await context.Products");
|
||||
Console.WriteLine(" .Where(p => p.Price < 100)");
|
||||
Console.WriteLine(" .ToListAsync();");
|
||||
Console.WriteLine(" foreach (var p in products) p.Price *= 1.1m;");
|
||||
Console.WriteLine(" await context.SaveChangesAsync();");
|
||||
|
||||
Console.WriteLine("\n3. 批量删除:");
|
||||
Console.WriteLine(" var toDelete = await context.Products");
|
||||
Console.WriteLine(" .Where(p => p.Stock == 0)");
|
||||
Console.WriteLine(" .ToListAsync();");
|
||||
Console.WriteLine(" context.Products.RemoveRange(toDelete);");
|
||||
Console.WriteLine(" await context.SaveChangesAsync();");
|
||||
|
||||
Console.WriteLine("\n💡 性能提示:");
|
||||
Console.WriteLine(" - 使用 AddRange/UpdateRange/RemoveRange");
|
||||
Console.WriteLine(" - 避免在循环中调用 SaveChanges");
|
||||
Console.WriteLine(" - 考虑分批处理大量数据");
|
||||
Console.WriteLine(" - 使用 AsNoTracking() 提升查询性能");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: AsNoTracking 提升查询性能
|
||||
///
|
||||
/// 只读查询不需要跟踪实体变化
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Demo_AsNoTrackingOptimization()
|
||||
{
|
||||
var builder = new DataContextBuilder<TestDbContext>();
|
||||
builder.UseInMemoryDatabase($"NoTrackingDemo_{Guid.NewGuid()}");
|
||||
|
||||
using var db = builder.Build();
|
||||
var repo = db.Repository<Product>();
|
||||
|
||||
// 添加测试数据
|
||||
for (int i = 1; i <= 100; i++)
|
||||
{
|
||||
await repo.AddAsync(new Product
|
||||
{
|
||||
Name = $"产品{i}",
|
||||
Price = i * 10m,
|
||||
Stock = i
|
||||
});
|
||||
}
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine("=== AsNoTracking 优化 ===\n");
|
||||
|
||||
Console.WriteLine("1. 默认查询(带跟踪):");
|
||||
var trackedProducts = await db.Context.Set<Product>()
|
||||
.Take(10)
|
||||
.ToListAsync();
|
||||
Console.WriteLine($" ✓ 查询 {trackedProducts.Count} 条,实体被跟踪");
|
||||
Console.WriteLine($" 适用于: 需要更新的查询");
|
||||
|
||||
Console.WriteLine("\n2. AsNoTracking 查询(不跟踪):");
|
||||
var untrackedProducts = await db.Context.Set<Product>()
|
||||
.AsNoTracking() // 不跟踪实体变化
|
||||
.Take(10)
|
||||
.ToListAsync();
|
||||
Console.WriteLine($" ✓ 查询 {untrackedProducts.Count} 条,实体不被跟踪");
|
||||
Console.WriteLine($" 适用于: 只读查询、数据展示");
|
||||
|
||||
Console.WriteLine("\n3. 仓储默认支持:");
|
||||
var query = repo.Query(trackChanges: false); // 指定不跟踪
|
||||
var products = await query.Take(10).ToListAsync();
|
||||
Console.WriteLine($" ✓ 仓储的 Query() 方法支持 trackChanges 参数");
|
||||
|
||||
Console.WriteLine("\n💡 性能优势:");
|
||||
Console.WriteLine(" - 减少内存占用");
|
||||
Console.WriteLine(" - 提升查询速度");
|
||||
Console.WriteLine(" - 适合大量数据的只读场景");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 实体变更跟踪
|
||||
///
|
||||
/// 检查实体的状态和变更
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Demo_ChangeTracking()
|
||||
{
|
||||
var builder = new DataContextBuilder<TestDbContext>();
|
||||
builder.UseInMemoryDatabase($"ChangeTrackingDemo_{Guid.NewGuid()}");
|
||||
|
||||
using var db = builder.Build();
|
||||
var repo = db.Repository<Product>();
|
||||
|
||||
Console.WriteLine("=== 实体变更跟踪 ===\n");
|
||||
|
||||
// 添加实体
|
||||
var product = new Product
|
||||
{
|
||||
Name = "测试产品",
|
||||
Price = 100m,
|
||||
Stock = 10
|
||||
};
|
||||
await repo.AddAsync(product);
|
||||
|
||||
var entry = db.Context.Entry(product);
|
||||
Console.WriteLine($"1. 新增状态:");
|
||||
Console.WriteLine($" EntityState: {entry.State}"); // Added
|
||||
Assert.AreEqual(EntityState.Added, entry.State);
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine($"\n2. 保存后状态:");
|
||||
Console.WriteLine($" EntityState: {entry.State}"); // Unchanged
|
||||
Assert.AreEqual(EntityState.Unchanged, entry.State);
|
||||
|
||||
// 修改实体
|
||||
product.Price = 200m;
|
||||
|
||||
Console.WriteLine($"\n3. 修改后状态:");
|
||||
Console.WriteLine($" EntityState: {entry.State}"); // Modified
|
||||
Console.WriteLine($" 修改的属性: {string.Join(", ", entry.Properties.Where(p => p.IsModified).Select(p => p.Metadata.Name))}");
|
||||
Assert.AreEqual(EntityState.Modified, entry.State);
|
||||
|
||||
// 查看原始值和当前值
|
||||
var priceProperty = entry.Property(p => p.Price);
|
||||
Console.WriteLine($"\n4. 属性变更详情:");
|
||||
Console.WriteLine($" 原始值: {priceProperty.OriginalValue:C}");
|
||||
Console.WriteLine($" 当前值: {priceProperty.CurrentValue:C}");
|
||||
Console.WriteLine($" 是否修改: {priceProperty.IsModified}");
|
||||
|
||||
Console.WriteLine($"\n✓ 可以精确跟踪实体的每个变更");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 高级查询技巧
|
||||
///
|
||||
/// 使用 EF Core 的高级查询功能
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Demo_AdvancedQueries()
|
||||
{
|
||||
var builder = new DataContextBuilder<TestDbContext>();
|
||||
builder.UseInMemoryDatabase($"AdvancedQueryDemo_{Guid.NewGuid()}");
|
||||
|
||||
using var db = builder.Build();
|
||||
var repo = db.Repository<Product>();
|
||||
|
||||
// 添加测试数据
|
||||
for (int i = 1; i <= 50; i++)
|
||||
{
|
||||
await repo.AddAsync(new Product
|
||||
{
|
||||
Name = $"产品{i}",
|
||||
Price = (i % 5 + 1) * 100m,
|
||||
Stock = i * 2,
|
||||
Description = i % 2 == 0 ? "热门" : "普通"
|
||||
});
|
||||
}
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine("=== 高级查询技巧 ===\n");
|
||||
|
||||
// 1. 投影查询(只查询需要的字段)
|
||||
Console.WriteLine("1. 投影查询:");
|
||||
var projection = await db.Context.Set<Product>()
|
||||
.Where(p => p.Price > 200m)
|
||||
.Select(p => new
|
||||
{
|
||||
p.Name,
|
||||
p.Price,
|
||||
TotalValue = p.Price * p.Stock
|
||||
})
|
||||
.Take(5)
|
||||
.ToListAsync();
|
||||
|
||||
Console.WriteLine($" 查询到 {projection.Count} 条记录(只包含投影字段)");
|
||||
|
||||
// 2. 分组聚合
|
||||
Console.WriteLine("\n2. 分组聚合:");
|
||||
var groupStats = await db.Context.Set<Product>()
|
||||
.GroupBy(p => p.Description)
|
||||
.Select(g => new
|
||||
{
|
||||
Category = g.Key,
|
||||
Count = g.Count(),
|
||||
AvgPrice = g.Average(p => p.Price),
|
||||
MaxStock = g.Max(p => p.Stock)
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var stat in groupStats)
|
||||
{
|
||||
Console.WriteLine($" {stat.Category}: {stat.Count}个, 均价 {stat.AvgPrice:C}, 最大库存 {stat.MaxStock}");
|
||||
}
|
||||
|
||||
// 3. 子查询
|
||||
Console.WriteLine("\n3. 子查询:");
|
||||
var avgPrice = await db.Context.Set<Product>().AverageAsync(p => p.Price);
|
||||
var aboveAverage = await db.Context.Set<Product>()
|
||||
.Where(p => p.Price > avgPrice)
|
||||
.CountAsync();
|
||||
Console.WriteLine($" 平均价格: {avgPrice:C}");
|
||||
Console.WriteLine($" 高于平均价格的产品: {aboveAverage} 个");
|
||||
|
||||
// 4. 联合查询
|
||||
Console.WriteLine("\n4. 联合查询 (Union):");
|
||||
var cheapProducts = db.Context.Set<Product>().Where(p => p.Price < 200m);
|
||||
var expensiveProducts = db.Context.Set<Product>().Where(p => p.Price > 400m);
|
||||
var combined = await cheapProducts.Union(expensiveProducts).CountAsync();
|
||||
Console.WriteLine($" 便宜或昂贵的产品: {combined} 个");
|
||||
|
||||
Console.WriteLine($"\n✓ EF Core 支持几乎所有 LINQ 查询");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 什么时候应该降级使用 EF Core
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void Demo_WhenToUseEFCoreDirect()
|
||||
{
|
||||
Console.WriteLine("=== 何时降级使用 EF Core? ===\n");
|
||||
|
||||
Console.WriteLine("推荐使用 Sharp.Data 仓储的场景:");
|
||||
Console.WriteLine(" ✓ 标准的 CRUD 操作");
|
||||
Console.WriteLine(" ✓ 简单的条件查询");
|
||||
Console.WriteLine(" ✓ 分页查询");
|
||||
Console.WriteLine(" ✓ 需要审计和软删除");
|
||||
Console.WriteLine(" ✓ 统一的数据访问模式\n");
|
||||
|
||||
Console.WriteLine("推荐直接使用 EF Core 的场景:");
|
||||
Console.WriteLine(" ✓ 复杂的连表查询");
|
||||
Console.WriteLine(" ✓ 执行原生 SQL");
|
||||
Console.WriteLine(" ✓ 调用存储过程");
|
||||
Console.WriteLine(" ✓ 需要精细的事务控制");
|
||||
Console.WriteLine(" ✓ 批量操作性能优化");
|
||||
Console.WriteLine(" ✓ 高级查询(分组、聚合、子查询等)");
|
||||
Console.WriteLine(" ✓ 实体变更跟踪");
|
||||
Console.WriteLine(" ✓ 数据库迁移管理\n");
|
||||
|
||||
Console.WriteLine("✓ 总结:");
|
||||
Console.WriteLine(" Sharp.Data 封装了常用操作,提供便利");
|
||||
Console.WriteLine(" 但不限制 EF Core 的任何功能");
|
||||
Console.WriteLine(" 两者可以自由组合使用,根据场景选择");
|
||||
Console.WriteLine(" 底层完全共用同一个 DbContext,无缝协作");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0"/>
|
||||
<PackageReference Include="MSTest" Version="3.6.4"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Microsoft.VisualStudio.TestTools.UnitTesting"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Sharp\Data\Data.csproj" />
|
||||
<ProjectReference Include="..\..\src\Sharp\Common\Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.11" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1 @@
|
||||
[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)]
|
||||
@@ -0,0 +1,184 @@
|
||||
# Sharp.Data 测试演示说明
|
||||
|
||||
## 概述
|
||||
|
||||
本测试项目不仅是单元测试,更是 **Sharp.Data 模块的完整使用演示**。通过这些测试,您可以学习如何在实际项目中使用 Sharp.Data。
|
||||
|
||||
## 测试文件说明
|
||||
|
||||
| 文件 | 说明 | 主要演示内容 |
|
||||
|------|------|------------|
|
||||
| `TestEntities.cs` | 实体定义演示 | 6种不同的实体定义方式 |
|
||||
| `TestDbContext.cs` | DbContext 配置演示 | 如何配置各种实体 |
|
||||
| `01_BasicUsageTests.cs` | 基础使用 | 3行代码快速开始、各种配置选项 |
|
||||
| `02_RepositoryCRUDTests.cs` | 仓储 CRUD | 增删改查、批量操作、复杂查询 |
|
||||
| `03_AuditFeatureTests.cs` | 审计功能 | 自动记录创建/修改信息 |
|
||||
| `04_SoftDeleteTests.cs` | 软删除功能 | 逻辑删除 vs 物理删除 |
|
||||
| `05_PaginationTests.cs` | 分页查询 | 各种分页场景 |
|
||||
| `06_CustomEntityTests.cs` | 自定义实体 | 不使用基类的灵活方式 |
|
||||
| `07_EFCoreDirectUsageTests.cs` | EF Core 直接使用 | 原生 SQL、事务、高级查询 |
|
||||
|
||||
## 运行测试
|
||||
|
||||
```bash
|
||||
# 运行所有测试
|
||||
dotnet test D:\yuanll\sharp-basic\test\Data.Test\Data.Test.csproj
|
||||
|
||||
# 运行特定测试类
|
||||
dotnet test --filter "FullyQualifiedName~_01_BasicUsageTests"
|
||||
|
||||
# 运行特定测试方法
|
||||
dotnet test --filter "Name~Demo_MinimalSetup"
|
||||
```
|
||||
|
||||
## 主要演示场景
|
||||
|
||||
### 1. 实体定义方式(TestEntities.cs)
|
||||
|
||||
演示了 6 种实体定义方式,满足不同需求:
|
||||
|
||||
- **使用 AuditableEntity 基类**:最常用,包含审计和软删除
|
||||
- **使用 Entity 基类**:只需要 Id,不需要审计
|
||||
- **完全自定义**:不使用任何基类,完全自主
|
||||
- **实现接口但不用基类**:灵活控制+享受功能
|
||||
- **只实现软删除接口**:只要软删除,不要审计
|
||||
- **包含值对象**:复杂实体结构
|
||||
|
||||
### 2. 快速开始(01_BasicUsageTests.cs)
|
||||
|
||||
演示最简单的 3 行代码启动方式:
|
||||
|
||||
```csharp
|
||||
var builder = new DataContextBuilder<TestDbContext>();
|
||||
builder.UseInMemoryDatabase("MyDb");
|
||||
var db = builder.Build();
|
||||
```
|
||||
|
||||
### 3. 完整 CRUD(02_RepositoryCRUDTests.cs)
|
||||
|
||||
演示所有基础操作:
|
||||
|
||||
- 添加单个/批量实体
|
||||
- 各种查询方式(ID、条件、First、分页等)
|
||||
- 更新实体
|
||||
- 删除实体(物理/逻辑)
|
||||
- 统计查询(Count、Any)
|
||||
- 复杂 LINQ 查询
|
||||
|
||||
### 4. 审计追踪(03_AuditFeatureTests.cs)
|
||||
|
||||
演示自动审计功能:
|
||||
|
||||
- 创建时自动记录 CreatedAt/CreatedBy
|
||||
- 更新时自动记录 UpdatedAt/UpdatedBy
|
||||
- 多次更新的审计历史
|
||||
- 动态获取当前用户(注:当前版本需为每个操作创建新的 DataContext)
|
||||
|
||||
### 5. 软删除(04_SoftDeleteTests.cs)
|
||||
|
||||
演示软删除与物理删除的区别:
|
||||
|
||||
- 软删除不会真正删除数据
|
||||
- 查询时自动过滤已删除数据
|
||||
- 使用 IgnoreQueryFilters 查询已删除数据
|
||||
- 软删除 vs 物理删除的应用场景
|
||||
|
||||
### 6. 分页查询(05_PaginationTests.cs)
|
||||
|
||||
演示各种分页场景:
|
||||
|
||||
- 基础分页
|
||||
- 带条件的分页
|
||||
- 带排序的分页
|
||||
- 遍历所有页
|
||||
- 分页结果的使用(PageIndex、TotalCount、TotalPages 等)
|
||||
|
||||
### 7. 自定义实体(06_CustomEntityTests.cs)
|
||||
|
||||
演示完全不使用基类的灵活性:
|
||||
|
||||
- 完全自定义的实体
|
||||
- 字符串主键
|
||||
- 自增 Int 主键
|
||||
- 包含值对象的实体
|
||||
- 混合使用不同类型的实体
|
||||
|
||||
### 8. EF Core 直接使用(07_EFCoreDirectUsageTests.cs)
|
||||
|
||||
演示如何在需要时降级使用 EF Core:
|
||||
|
||||
- 访问底层 DbContext
|
||||
- 直接使用 DbSet
|
||||
- 执行原生 SQL
|
||||
- 事务管理
|
||||
- 批量操作优化
|
||||
- AsNoTracking 性能优化
|
||||
- 实体变更跟踪
|
||||
- 高级查询
|
||||
|
||||
## 注意事项
|
||||
|
||||
### PagedResult 属性名称
|
||||
|
||||
`PagedResult<T>` 来自 `Common.ExpandFun` 命名空间,使用的属性名是:
|
||||
|
||||
- `PageIndex` (而不是 Page)
|
||||
- `PageSize`
|
||||
- `TotalCount`
|
||||
- `TotalPages`
|
||||
|
||||
### 动态修改当前用户
|
||||
|
||||
当前版本的 DataContext 不支持动态修改当前用户。如果需要模拟不同用户操作,请为每个用户创建新的 DataContext 实例:
|
||||
|
||||
```csharp
|
||||
// 用户 A 的操作
|
||||
using (var dbA = CreateDataContext("userA"))
|
||||
{
|
||||
// ... 操作
|
||||
}
|
||||
|
||||
// 用户 B 的操作
|
||||
using (var dbB = CreateDataContext("userB"))
|
||||
{
|
||||
// ... 操作
|
||||
}
|
||||
```
|
||||
|
||||
### InMemory 数据库限制
|
||||
|
||||
测试使用 InMemory 数据库,它有一些限制:
|
||||
|
||||
- 不支持原生 SQL 查询(FromSqlRaw/ExecuteSqlRaw)
|
||||
- 不完全模拟关系数据库的所有行为
|
||||
- 主要用于快速测试
|
||||
|
||||
实际项目应该使用真实数据库(SQLite、SQL Server 等)。
|
||||
|
||||
## 学习路径建议
|
||||
|
||||
1. **入门**:先看 `01_BasicUsageTests.cs`,了解如何快速开始
|
||||
2. **基础**:学习 `02_RepositoryCRUDTests.cs`,掌握基础 CRUD 操作
|
||||
3. **进阶**:了解审计(03)、软删除(04)、分页(05)功能
|
||||
4. **高级**:学习自定义实体(06)和 EF Core 直接使用(07)
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **根据需求选择实体定义方式**
|
||||
- 需要审计+软删除:使用 `AuditableEntity`
|
||||
- 只需要基础 CRUD:使用 `Entity` 或完全自定义
|
||||
- 已有实体模型:保持原样,直接使用
|
||||
|
||||
2. **性能优化**
|
||||
- 只读查询使用 `AsNoTracking()`
|
||||
- 批量操作使用 `AddRange/UpdateRange/RemoveRange`
|
||||
- 避免在循环中调用 `SaveChangesAsync`
|
||||
|
||||
3. **灵活使用**
|
||||
- 简单场景用仓储
|
||||
- 复杂查询直接用 EF Core
|
||||
- 两者可以无缝混用
|
||||
|
||||
## 问题反馈
|
||||
|
||||
如有问题或建议,请参考 Data 模块的 README.md 文件。
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Data.Test;
|
||||
|
||||
[TestClass]
|
||||
public sealed class Test1
|
||||
{
|
||||
[TestMethod]
|
||||
public void TestMethod1()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Data.Test;
|
||||
|
||||
/// <summary>
|
||||
/// 测试用的数据库上下文
|
||||
/// 演示如何配置各种类型的实体
|
||||
/// </summary>
|
||||
public class TestDbContext : DbContext
|
||||
{
|
||||
public TestDbContext(DbContextOptions<TestDbContext> options) : base(options)
|
||||
{
|
||||
}
|
||||
|
||||
// 使用基类的实体
|
||||
public DbSet<Product> Products => Set<Product>();
|
||||
public DbSet<Category> Categories => Set<Category>();
|
||||
public DbSet<Tag> Tags => Set<Tag>();
|
||||
|
||||
// 自定义实体
|
||||
public DbSet<CustomProduct> CustomProducts => Set<CustomProduct>();
|
||||
public DbSet<Region> Regions => Set<Region>();
|
||||
|
||||
// 实现接口的实体
|
||||
public DbSet<Order> Orders => Set<Order>();
|
||||
public DbSet<Notification> Notifications => Set<Notification>();
|
||||
public DbSet<Customer> Customers => Set<Customer>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
ConfigureProductEntity(modelBuilder);
|
||||
ConfigureCategoryEntity(modelBuilder);
|
||||
ConfigureTagEntity(modelBuilder);
|
||||
ConfigureCustomProductEntity(modelBuilder);
|
||||
ConfigureRegionEntity(modelBuilder);
|
||||
ConfigureOrderEntity(modelBuilder);
|
||||
ConfigureNotificationEntity(modelBuilder);
|
||||
ConfigureCustomerEntity(modelBuilder);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配置 Product 实体 (使用 AuditableEntity)
|
||||
/// </summary>
|
||||
private void ConfigureProductEntity(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<Product>(entity =>
|
||||
{
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.Property(e => e.Name).IsRequired().HasMaxLength(100);
|
||||
entity.Property(e => e.Price).HasPrecision(18, 2);
|
||||
entity.Property(e => e.Description).HasMaxLength(500);
|
||||
|
||||
// 添加索引
|
||||
entity.HasIndex(e => e.Name);
|
||||
entity.HasIndex(e => e.IsDeleted); // 软删除字段索引(提升查询性能)
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配置 Category 实体 (使用 Entity 基类)
|
||||
/// </summary>
|
||||
private void ConfigureCategoryEntity(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<Category>(entity =>
|
||||
{
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.Property(e => e.Name).IsRequired().HasMaxLength(50);
|
||||
entity.Property(e => e.Description).HasMaxLength(200);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配置 Tag 实体 (使用 EntityInt 基类,自增主键)
|
||||
/// </summary>
|
||||
private void ConfigureTagEntity(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<Tag>(entity =>
|
||||
{
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.Property(e => e.Id).ValueGeneratedOnAdd(); // 自增
|
||||
entity.Property(e => e.Name).IsRequired().HasMaxLength(50);
|
||||
entity.Property(e => e.Color).HasMaxLength(20);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配置 CustomProduct 实体 (完全自定义)
|
||||
/// </summary>
|
||||
private void ConfigureCustomProductEntity(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<CustomProduct>(entity =>
|
||||
{
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.Property(e => e.Name).IsRequired().HasMaxLength(100);
|
||||
entity.Property(e => e.Price).HasPrecision(18, 2);
|
||||
entity.Property(e => e.CreatedDate).IsRequired();
|
||||
|
||||
// 忽略计算属性
|
||||
entity.Ignore(e => e.IsExpensive);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配置 Region 实体 (字符串主键)
|
||||
/// </summary>
|
||||
private void ConfigureRegionEntity(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<Region>(entity =>
|
||||
{
|
||||
entity.HasKey(e => e.Code); // 字符串作为主键
|
||||
entity.Property(e => e.Code).HasMaxLength(10);
|
||||
entity.Property(e => e.Name).IsRequired().HasMaxLength(100);
|
||||
entity.Property(e => e.Description).HasMaxLength(500);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配置 Order 实体 (实现接口但不使用基类)
|
||||
/// </summary>
|
||||
private void ConfigureOrderEntity(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<Order>(entity =>
|
||||
{
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.Property(e => e.OrderNumber).IsRequired().HasMaxLength(50);
|
||||
entity.Property(e => e.TotalAmount).HasPrecision(18, 2);
|
||||
|
||||
// 索引
|
||||
entity.HasIndex(e => e.OrderNumber).IsUnique();
|
||||
entity.HasIndex(e => e.IsDeleted);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配置 Notification 实体 (只实现软删除)
|
||||
/// </summary>
|
||||
private void ConfigureNotificationEntity(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<Notification>(entity =>
|
||||
{
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.Property(e => e.Title).IsRequired().HasMaxLength(200);
|
||||
entity.Property(e => e.Content).IsRequired();
|
||||
entity.HasIndex(e => e.IsDeleted);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配置 Customer 实体 (包含值对象)
|
||||
/// </summary>
|
||||
private void ConfigureCustomerEntity(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<Customer>(entity =>
|
||||
{
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.Property(e => e.Name).IsRequired().HasMaxLength(100);
|
||||
entity.Property(e => e.Email).IsRequired().HasMaxLength(200);
|
||||
|
||||
// 配置值对象 - 方式1: 使用 OwnsOne
|
||||
entity.OwnsOne(e => e.ShippingAddress, address =>
|
||||
{
|
||||
address.Property(a => a.Street).HasMaxLength(200);
|
||||
address.Property(a => a.City).HasMaxLength(100);
|
||||
address.Property(a => a.State).HasMaxLength(50);
|
||||
address.Property(a => a.ZipCode).HasMaxLength(20);
|
||||
address.Property(a => a.Country).HasMaxLength(50);
|
||||
});
|
||||
|
||||
entity.OwnsOne(e => e.BillingAddress, address =>
|
||||
{
|
||||
address.Property(a => a.Street).HasMaxLength(200);
|
||||
address.Property(a => a.City).HasMaxLength(100);
|
||||
address.Property(a => a.State).HasMaxLength(50);
|
||||
address.Property(a => a.ZipCode).HasMaxLength(20);
|
||||
address.Property(a => a.Country).HasMaxLength(50);
|
||||
});
|
||||
|
||||
entity.HasIndex(e => e.Email);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
using Sharp.Data.Entities;
|
||||
|
||||
namespace Data.Test;
|
||||
|
||||
// ============================================================
|
||||
// 演示各种实体定义方式
|
||||
// ============================================================
|
||||
|
||||
#region 方式1: 使用 AuditableEntity 基类 (最常用,包含审计和软删除)
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 使用 AuditableEntity 基类
|
||||
/// 优点: 自动包含 Id、审计字段、软删除字段
|
||||
/// 适用场景: 需要审计和软删除功能的业务实体
|
||||
/// </summary>
|
||||
public class Product : AuditableEntity
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public decimal Price { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public int Stock { get; set; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 方式2: 使用 Entity 基类 (只需要 Id,不需要审计)
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 使用 Entity 基类 (Guid 主键)
|
||||
/// 优点: 只包含 Id,更简洁
|
||||
/// 适用场景: 简单实体,不需要审计功能
|
||||
/// </summary>
|
||||
public class Category : Entity
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 使用 EntityInt 基类 (Int 主键)
|
||||
/// 适用场景: 需要自增 Int 主键的实体
|
||||
/// </summary>
|
||||
public class Tag : EntityInt
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Color { get; set; } = "#000000";
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 方式3: 完全自定义实体 (不使用任何基类)
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 完全自定义实体,不使用任何基类
|
||||
/// 优点: 完全自主控制,不依赖框架
|
||||
/// 适用场景:
|
||||
/// - 已有实体模型,不想改动
|
||||
/// - 需要特殊的主键类型
|
||||
/// - 不需要框架提供的任何功能
|
||||
/// </summary>
|
||||
public class CustomProduct
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public decimal Price { get; set; }
|
||||
public DateTime CreatedDate { get; set; }
|
||||
|
||||
// 可以有自己的业务逻辑
|
||||
public bool IsExpensive => Price > 1000;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 使用字符串作为主键的自定义实体
|
||||
/// </summary>
|
||||
public class Region
|
||||
{
|
||||
public string Code { get; set; } = string.Empty; // 使用字符串作为主键,如 "CN", "US"
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 方式4: 实现接口但不使用基类 (灵活控制)
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 实现审计接口但不使用基类
|
||||
/// 优点: 可以自己控制字段名称和类型,同时享受审计功能
|
||||
/// 适用场景: 需要审计功能,但想自定义实现
|
||||
/// </summary>
|
||||
public class Order : IAuditableEntity, ISoftDeletable
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string OrderNumber { get; set; } = string.Empty;
|
||||
public decimal TotalAmount { get; set; }
|
||||
|
||||
// 实现审计接口
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public string? CreatedBy { get; set; }
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
public string? UpdatedBy { get; set; }
|
||||
|
||||
// 实现软删除接口
|
||||
public bool IsDeleted { get; set; }
|
||||
public DateTime? DeletedAt { get; set; }
|
||||
public string? DeletedBy { get; set; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 方式5: 只实现软删除接口
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 只实现软删除接口,不要审计功能
|
||||
/// 适用场景: 只需要软删除,不需要记录创建/修改信息
|
||||
/// </summary>
|
||||
public class Notification : ISoftDeletable
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Content { get; set; } = string.Empty;
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
// 只实现软删除接口
|
||||
public bool IsDeleted { get; set; }
|
||||
public DateTime? DeletedAt { get; set; }
|
||||
public string? DeletedBy { get; set; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 方式6: 值对象和复杂实体
|
||||
|
||||
/// <summary>
|
||||
/// 演示: 包含值对象的实体
|
||||
/// </summary>
|
||||
public class Customer : AuditableEntity
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Email { get; set; } = string.Empty;
|
||||
|
||||
// 值对象
|
||||
public Address? ShippingAddress { get; set; }
|
||||
public Address? BillingAddress { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 值对象: 地址
|
||||
/// </summary>
|
||||
public class Address
|
||||
{
|
||||
public string Street { get; set; } = string.Empty;
|
||||
public string City { get; set; } = string.Empty;
|
||||
public string State { get; set; } = string.Empty;
|
||||
public string ZipCode { get; set; } = string.Empty;
|
||||
public string Country { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
#endregion
|
||||
Reference in new issue
Block a user