A:增加数据库配置和使用

This commit is contained in:
jimmy committed 2025-11-18 11:14:56 +08:00
1 parent 53d1dd2dbf
commit f7660c49e8
86 files changed
+7401 -271

No files matched your search

+7 -4
View File
@@ -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>
+24 -5
View File
@@ -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)
+1
View File
@@ -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;
}
}
+69
View File
@@ -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>
+29
View File
@@ -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;
}
}
+99 -28
View File
@@ -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>
+7 -12
View File
@@ -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 管理员");
}
}
+31
View File
@@ -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;
}
}
}
+60 -17
View File
@@ -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
@@ -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;
+104
View File
@@ -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();
}
}
+25
View File
@@ -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; }
}
+31
View File
@@ -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>
+193
View File
@@ -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);
}
}
+37
View File
@@ -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>
{
}
+75
View File
@@ -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; }
}
+22
View File
@@ -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);
}
}
+452
View File
@@ -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
+90
View File
@@ -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);
}
+160
View File
@@ -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);
}
}
}
+498
View File
@@ -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/) - 示例代码
+1
View File
@@ -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
View File
@@ -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();
```
日志要简单可靠:一行接入,默认即用,可配更强。
```
+98
View File
@@ -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;
}
}
}
}
+23 -7
View File
@@ -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`。
-9
View File
@@ -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>
-14
View File
@@ -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
{
}
}
-10
View File
@@ -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);
}
-12
View File
@@ -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>
-24
View File
@@ -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
}
+90
View File
@@ -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 原本的扩展方法。
+14 -3
View File
@@ -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>
+45 -45
View File
@@ -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);
}
-21
View File
@@ -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();
}
}
+104
View File
@@ -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;
}
}
+2
View File
@@ -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>
+21
View File
@@ -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(() =>
+8
View File
@@ -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>
+6 -34
View File
@@ -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);
}
}
}
+14 -6
View File
@@ -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;
}
}
+12
View File
@@ -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
+19 -2
View File
@@ -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": []
}
}
}
}
+7 -1
View File
@@ -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 -1
View File
@@ -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
View File
@@ -1 +1 @@
17633497485167484
17633637013370903