Compare commits

..
5 Commits
Author SHA1 Message Date
jimmy e1d059ed29 A:调整部分日志;增加Client对于依赖注入的支持 2025-11-20 16:35:48 +08:00
jimmy 2739a71134 A:Access接口增加可加密选项 2025-11-19 18:12:11 +08:00
jimmy dd993e5b45 A:增加Access数据库支持 2025-11-19 14:04:25 +08:00
jimmy e334e65c2d A:增加Access数据库 2025-11-19 08:43:25 +08:00
jimmy 1d412bba33 A:Access数据库功能 2025-11-19 08:42:28 +08:00
102 changed files with 4422 additions and 1601 deletions

No files matched your search

+4
View File
@@ -10,3 +10,7 @@
</ResourceDictionary>
</Application.Resources>
</Application>
+21 -79
View File
@@ -2,7 +2,9 @@ using System;
using System.Windows;
using Client.Services;
using Client.ViewModels;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using Common;
using Config;
@@ -10,10 +12,8 @@ using NetClient.DependencyInjection;
using Sharp.Infrastructure.Configuration;
using Sharp.Log.Extensions;
using Sharp.NetServer.DependencyInjection;
using Sharp.Wpf.Exceptions;
using Sharp.Wpf.Services;
using Sharp.Wpf.Abstractions;
using Plc.Siemens.DependencyInjection;
using Microsoft.Extensions.Configuration;
namespace Client
{
@@ -27,58 +27,7 @@ namespace Client
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
// Global Exception Handling
DispatcherUnhandledException += (s, args) =>
{
// Safe logging: handle case where DI failed
if (_serviceProvider != null)
{
try
{
var logger = _serviceProvider.GetService<ILogger<App>>();
logger?.LogError(args.Exception, "UI Thread Unhandled Exception");
}
catch { /* Ignore logging failure during crash */ }
}
MessageBox.Show($"发生未处理异常: {args.Exception.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
args.Handled = true; // Prevent crash
};
TaskScheduler.UnobservedTaskException += (s, args) =>
{
if (_serviceProvider != null)
{
try
{
var logger = _serviceProvider.GetService<ILogger<App>>();
logger?.LogError(args.Exception, "Task Unobserved Exception");
}
catch { /* Ignore */ }
}
args.SetObserved();
};
AppDomain.CurrentDomain.UnhandledException += (s, args) =>
{
var ex = args.ExceptionObject as Exception;
if (_serviceProvider != null)
{
try
{
var logger = _serviceProvider.GetService<ILogger<App>>();
logger?.LogCritical(ex, "AppDomain Unhandled Exception (IsTerminating: {IsTerminating})", args.IsTerminating);
}
catch { /* Ignore */ }
}
if (args.IsTerminating)
{
MessageBox.Show($"发生严重错误,程序即将退出: {ex?.Message}", "严重错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
};
// 1) Build configuration manager from appsettings.json (+ reload on change + env vars)
var cm = ConfigBuilder.BuildFromFiles(new[] { "appsettings.json" }, reloadOnChange: true, includeEnvironment: true, basePath: AppContext.BaseDirectory);
@@ -87,8 +36,7 @@ namespace Client
// ConfigManager and typed SystemConfig accessor
services.AddSingleton(cm);
// Register standard IConfiguration so that .BindConfiguration() works
services.AddSingleton<IConfiguration>(cm.Configuration);
services.TryAddSingleton<IConfiguration>(cm.Configuration); // Register IConfiguration for BindConfiguration
services.AddSystemConfig(cm);
// Logging (binds from section "SharpLog")
@@ -98,53 +46,47 @@ namespace Client
services.AddNetClient(cm);
services.AddNetServer(cm);
// PLC
services.AddSiemensPlc(options =>
{
options.IpAddress = "127.0.0.1";
options.CpuType = S7.Net.CpuType.S71200;
});
// WPF exception handler
services.AddSingleton<IUiExceptionHandler, UiExceptionHandler>();
// WPF app services
services.AddSingleton<IDialogService, DialogService>();
services.AddSingleton<IAuthenticationService, DemoAuthenticationService>();
services.AddSingleton<IUserSession, UserSession>();
// Register DispatcherService with the current UI dispatcher
services.AddSingleton<IDispatcher>(new DispatcherService(Dispatcher));
services.AddTransient<LoginViewModel>();
services.AddTransient<MainViewModel>();
services.AddTransient<PlcViewModel>();
services.AddTransient<LoginWindow>();
services.AddTransient<MainWindow>();
services.AddTransient<PlcWindow>();
_serviceProvider = services.BuildServiceProvider();
// 3) Register global facades for convenience
Global.RegisterSingleton<IServiceProvider>(_serviceProvider);
// 3) Activate global WPF exception handler
var exceptionHandler = _serviceProvider.GetRequiredService<IUiExceptionHandler>();
exceptionHandler.Register();
// 4) Register global facades for convenience
Global.RegisterSingleton(_serviceProvider);
Global.RegisterSingleton(cm);
Global.RegisterSingleton(_serviceProvider.GetRequiredService<ISystemConfigAccessor>());
// Also register IDispatcher in Global if it's needed by static contexts (though injection is preferred)
Global.RegisterSingleton(_serviceProvider.GetRequiredService<IDispatcher>());
// 4) Show login window first, then main shell if authenticated
// Explicitly set ShutdownMode to prevent app from closing when LoginWindow closes
ShutdownMode = ShutdownMode.OnExplicitShutdown;
// 5) Show login window first, then main shell if authenticated
var logger = _serviceProvider.GetRequiredService<ILogger<App>>();
logger.LogInformation("Showing login window");
var loginWindow = _serviceProvider.GetRequiredService<LoginWindow>();
var loginResult = loginWindow.ShowDialog();
logger.LogInformation("Login window closed with result: {LoginResult}", loginResult);
if (loginResult == true)
{
logger.LogInformation("Login successful, showing main window");
var main = _serviceProvider.GetRequiredService<MainWindow>();
Current.MainWindow = main;
ShutdownMode = ShutdownMode.OnMainWindowClose;
main.Show();
}
else
{
logger.LogInformation("Login cancelled or failed, shutting down application");
Shutdown();
}
}
@@ -155,4 +97,4 @@ namespace Client
base.OnExit(e);
}
}
}
}
-1
View File
@@ -21,7 +21,6 @@
<ProjectReference Include="..\NetServer\NetServer.csproj" />
<ProjectReference Include="..\Log\Log.csproj" />
<ProjectReference Include="..\Sharp.Infrastructure\Sharp.Infrastructure.csproj" />
<ProjectReference Include="..\Plc.Siemens\Plc.Siemens.csproj" />
</ItemGroup>
</Project>
-18
View File
@@ -1,18 +0,0 @@
using System.ComponentModel;
using Plc.Attributes;
namespace Client.Hardware;
// 必须继承 INotifyPropertyChanged 才能支持自动 UI 刷新
public interface IPackMachine : INotifyPropertyChanged
{
[PlcAddress("DB1.DBW0")]
short RunningSpeed { get; set; }
[PlcAddress("DB1.DBW2")]
short Temperature { get; set; }
// 支持 bool (比如 M 区)
[PlcAddress("M100.0")]
bool IsStartButtonPushed { get; set; }
}
+5 -1
View File
@@ -1,23 +1,27 @@
using System;
using HandyControl.Controls;
using Client.ViewModels;
using Microsoft.Extensions.Logging;
namespace Client;
public partial class LoginWindow : GlowWindow
{
private readonly LoginViewModel _viewModel;
private readonly ILogger<LoginWindow>? _logger;
public LoginWindow(LoginViewModel viewModel)
public LoginWindow(LoginViewModel viewModel, ILogger<LoginWindow>? logger = null)
{
InitializeComponent();
_viewModel = viewModel;
_logger = logger;
DataContext = viewModel;
_viewModel.LoginSucceeded += HandleLoginSucceeded;
}
private void HandleLoginSucceeded(object? sender, EventArgs e)
{
_logger?.LogInformation("HandleLoginSucceeded called, setting DialogResult = true");
DialogResult = true;
}
-44
View File
@@ -47,12 +47,7 @@
Command="{Binding ShowDialogCommand}" />
<Button Content="触发异常"
Padding="18,6"
Margin="0,0,12,0"
Command="{Binding ThrowCommand}" />
<Button Content="PLC 调试"
Padding="18,6"
Style="{StaticResource ButtonPrimary}"
Command="{Binding OpenPlcToolCommand}" />
</StackPanel>
</Grid>
</Border>
@@ -87,45 +82,6 @@
<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="包装机状态 (PLC)" FontWeight="SemiBold" />
<StackPanel Orientation="Horizontal" Margin="0,8,0,0">
<TextBlock Text="速度: " Foreground="{DynamicResource SecondaryTextBrush}"/>
<TextBlock Text="{Binding Machine.RunningSpeed}" FontWeight="Bold" />
<TextBlock Text=" rpm" Foreground="{DynamicResource SecondaryTextBrush}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,4,0,0">
<TextBlock Text="温度: " Foreground="{DynamicResource SecondaryTextBrush}"/>
<TextBlock Text="{Binding Machine.Temperature}" FontWeight="Bold" />
<TextBlock Text=" ℃" Foreground="{DynamicResource SecondaryTextBrush}"/>
</StackPanel>
<!-- 交互控制演示 -->
<Separator Margin="0,10" Background="{DynamicResource BorderBrush}"/>
<TextBlock Text="控制面板" FontWeight="SemiBold" Margin="0,0,0,5"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Text="设定速度:" VerticalAlignment="Center"/>
<Slider Grid.Column="1" Minimum="0" Maximum="2000" Value="{Binding Machine.RunningSpeed}" Margin="10,0" IsMoveToPointEnabled="True"/>
<TextBlock Grid.Column="2" Text="{Binding Machine.RunningSpeed}" Width="40" TextAlignment="Right"/>
</Grid>
<StackPanel Orientation="Horizontal" Margin="0,10,0,0">
<Button Content="启动机器" Command="{Binding StartMachineCommand}" Margin="0,0,10,0" Style="{StaticResource ButtonSuccess}"/>
<Button Content="停止机器" Command="{Binding StopMachineCommand}" Style="{StaticResource ButtonDanger}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,10,0,0">
<CheckBox IsChecked="{Binding Machine.IsStartButtonPushed}" Content="启动按钮状态 (双向绑定)" IsHitTestVisible="False"/>
</StackPanel>
</StackPanel>
</Border>
<Border Background="{DynamicResource PopupBrush}" Margin="0,0,16,16" Padding="20" CornerRadius="12">
<StackPanel>
<TextBlock Text="提示" FontWeight="SemiBold" />
-90
View File
@@ -1,90 +0,0 @@
<hc:GlowWindow x:Class="Client.PlcWindow"
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"
mc:Ignorable="d"
Title="PLC 调试工具" Height="500" Width="800"
WindowStartupLocation="CenterOwner">
<hc:GlowWindow.Resources>
<conv:BoolToVisibilityConverter x:Key="Bool2Vis" />
</hc:GlowWindow.Resources>
<Grid Margin="20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- 连接控制 -->
<Border Background="{DynamicResource RegionBrush}" Padding="15" CornerRadius="8" Margin="0,0,0,15">
<StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,0,0,10">
<TextBlock Text="品牌:" VerticalAlignment="Center" Margin="0,0,5,0"/>
<ComboBox ItemsSource="{Binding Brands}" SelectedItem="{Binding SelectedBrand}" Width="120" Margin="0,0,15,0"/>
<StackPanel Orientation="Horizontal" Visibility="{Binding IsSiemensOptionsVisible, Converter={StaticResource Bool2Vis}}">
<TextBlock Text="型号:" VerticalAlignment="Center" Margin="0,0,5,0"/>
<ComboBox ItemsSource="{Binding CpuTypes}" SelectedItem="{Binding SelectedCpuType}" Width="120" Margin="0,0,15,0"/>
</StackPanel>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBox hc:InfoElement.Placeholder="IP 地址" hc:InfoElement.Title="IP" Text="{Binding IpAddress}" Width="200" Margin="0,0,10,0"/>
<Button Content="连接 / 启动服务" Command="{Binding ConnectCommand}" Margin="0,0,10,0" Style="{StaticResource ButtonPrimary}"/>
<Button Content="断开" Command="{Binding DisconnectCommand}"/>
<TextBlock Text="{Binding Status}" VerticalAlignment="Center" Margin="20,0,0,0" FontWeight="Bold" Foreground="{DynamicResource PrimaryTextBrush}"/>
</StackPanel>
</StackPanel>
</Border>
<!-- 单次读写 -->
<GroupBox Header="单次读写测试 (默认 Short)" Grid.Row="1" Margin="0,0,0,15">
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Text="读取地址:" VerticalAlignment="Center"/>
<TextBox Grid.Column="1" Text="{Binding ReadAddress}" Margin="5,0"/>
<Button Grid.Column="2" Content="读取" Command="{Binding ReadCommand}" Margin="5,0"/>
<TextBlock Grid.Column="3" Text="{Binding ReadValue}" VerticalAlignment="Center" Margin="5,0" FontWeight="Bold"/>
<TextBlock Grid.Row="1" Text="写入地址:" VerticalAlignment="Center" Margin="0,10,0,0"/>
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding WriteAddress}" Margin="5,10,0,0"/>
<TextBox Grid.Row="1" Grid.Column="3" Text="{Binding WriteValue}" Margin="5,10,0,0"/>
<Button Grid.Row="1" Grid.Column="4" Content="写入" Command="{Binding WriteCommand}" Margin="5,10,0,0"/>
</Grid>
</GroupBox>
<!-- 监控 -->
<GroupBox Header="实时监控 (Add Watch)" Grid.Row="2" Margin="0,0,0,15">
<StackPanel Orientation="Horizontal" Margin="10">
<TextBox hc:InfoElement.Placeholder="地址 (如 DB1.DBW0)" Text="{Binding MonitorAddress}" Width="200" Margin="0,0,10,0"/>
<Button Content="添加监控" Command="{Binding AddMonitorCommand}"/>
</StackPanel>
</GroupBox>
<!-- 监控列表 -->
<DataGrid Grid.Row="3" ItemsSource="{Binding MonitoredTags}" AutoGenerateColumns="False" IsReadOnly="True" HeadersVisibility="Column">
<DataGrid.Columns>
<DataGridTextColumn Header="ID" Binding="{Binding Id}" Width="100"/>
<DataGridTextColumn Header="地址" Binding="{Binding Address}" Width="150"/>
<DataGridTextColumn Header="类型" Binding="{Binding DataType.Name}" Width="80"/>
<DataGridTextColumn Header="当前值" Binding="{Binding Value}" Width="*"/>
</DataGrid.Columns>
</DataGrid>
</Grid>
</hc:GlowWindow>
-14
View File
@@ -1,14 +0,0 @@
using System.Windows;
using Client.ViewModels;
using HandyControl.Controls;
namespace Client;
public partial class PlcWindow : GlowWindow
{
public PlcWindow(PlcViewModel viewModel)
{
InitializeComponent();
DataContext = viewModel;
}
}
+13 -1
View File
@@ -1,6 +1,7 @@
using System;
using System.Threading.Tasks;
using Client.Services;
using Microsoft.Extensions.Logging;
using Sharp.Wpf.Commands;
using Sharp.Wpf.Mvvm;
@@ -10,6 +11,7 @@ public sealed class LoginViewModel : ViewModelBase
{
private readonly IAuthenticationService _authenticationService;
private readonly IUserSession _userSession;
private readonly ILogger<LoginViewModel> _logger;
private readonly IAsyncCommand _loginCommand;
private string _userName = "admin";
@@ -18,10 +20,11 @@ public sealed class LoginViewModel : ViewModelBase
public event EventHandler? LoginSucceeded;
public LoginViewModel(IAuthenticationService authenticationService, IUserSession userSession)
public LoginViewModel(IAuthenticationService authenticationService, IUserSession userSession, ILogger<LoginViewModel> logger)
{
_authenticationService = authenticationService;
_userSession = userSession;
_logger = logger;
_loginCommand = new AsyncCommand(LoginAsync);
}
@@ -67,6 +70,7 @@ public sealed class LoginViewModel : ViewModelBase
{
try
{
_logger.LogInformation("Login attempt for user: {UserName}", UserName);
IsBusy = true;
ErrorMessage = null;
@@ -74,13 +78,21 @@ public sealed class LoginViewModel : ViewModelBase
if (!result.Succeeded)
{
_logger.LogWarning("Login failed for user: {UserName}, Error: {Error}", UserName, result.Error);
ErrorMessage = result.Error;
return;
}
_logger.LogInformation("Login succeeded for user: {UserName}", UserName);
_userSession.SignIn(UserName, result.DisplayName);
_logger.LogInformation("Invoking LoginSucceeded event");
LoginSucceeded?.Invoke(this, EventArgs.Empty);
}
catch (Exception ex)
{
_logger.LogError(ex, "Login failed for user: {UserName}", UserName);
ErrorMessage = "An unexpected error occurred.";
}
finally
{
IsBusy = false;
+2 -54
View File
@@ -5,14 +5,6 @@ using Client.Services;
using Sharp.Wpf.Commands;
using Sharp.Wpf.Mvvm;
using Sharp.Wpf.Services;
using Microsoft.Extensions.DependencyInjection;
using Common;
using Client.Hardware;
using Plc.Proxy;
using Plc.Abstractions;
using System.ComponentModel;
using Sharp.Wpf.Abstractions;
namespace Client.ViewModels;
@@ -20,64 +12,23 @@ public sealed class MainViewModel : ViewModelBase
{
private readonly IDialogService _dialogService;
private readonly IUserSession _userSession;
private readonly IDispatcher _dispatcher;
private string _greeting = "欢迎使用 Sharp";
private string _statusMessage = "系统就绪";
private DateTime _lastSyncTime = DateTime.Now;
// 魔法设备对象:直接绑定到界面,无需手动写 Read/Write/Subscribe
public IPackMachine Machine { get; }
public MainViewModel(IDialogService dialogService, IUserSession userSession, IDispatcher dispatcher)
public MainViewModel(IDialogService dialogService, IUserSession userSession)
{
_dialogService = dialogService;
_userSession = userSession;
_dispatcher = dispatcher;
_greeting = $"欢迎回来,{UserDisplayName}";
_statusMessage = "准备就绪";
ShowDialogCommand = new AsyncCommand(ShowDialogAsync);
RefreshDashboardCommand = new AsyncCommand(RefreshDashboardAsync);
ThrowCommand = new RelayCommand(() => throw new InvalidOperationException("演示:未处理异常将被全局处理"));
OpenPlcToolCommand = new RelayCommand(() =>
{
var sp = Global.Get<IServiceProvider>();
var win = sp.GetRequiredService<PlcWindow>();
win.Show();
});
// 1. 获取 PLC 服务
var plcService = Global.Get<IServiceProvider>().GetRequiredService<IPlcService>();
// 2. 创建代理
Machine = PlcInterfaceProxy.Create<IPackMachine>(plcService);
// 3. 代码监听演示:监听属性变化
Machine.PropertyChanged += OnMachinePropertyChanged;
// 4. 控制命令
StartMachineCommand = new RelayCommand(() => Machine.IsStartButtonPushed = true);
StopMachineCommand = new RelayCommand(() => Machine.IsStartButtonPushed = false);
}
private void OnMachinePropertyChanged(object? sender, PropertyChangedEventArgs e)
{
// 演示:当速度变化时,在日志或状态栏显示
if (e.PropertyName == nameof(IPackMachine.RunningSpeed))
{
_dispatcher.InvokeAsync(() =>
{
if (Machine.RunningSpeed > 1000)
{
StatusMessage = $"警告:速度过高 ({Machine.RunningSpeed})";
}
});
}
}
//...
public string UserDisplayName => _userSession.DisplayName ?? _userSession.UserName ?? "访客";
public string Greeting
@@ -101,9 +52,6 @@ public sealed class MainViewModel : ViewModelBase
public IAsyncCommand ShowDialogCommand { get; }
public IAsyncCommand RefreshDashboardCommand { get; }
public ICommand ThrowCommand { get; }
public ICommand OpenPlcToolCommand { get; }
public ICommand StartMachineCommand { get; }
public ICommand StopMachineCommand { get; }
private async Task ShowDialogAsync()
{
@@ -135,4 +83,4 @@ public sealed class MainViewModel : ViewModelBase
IsBusy = false;
}
}
}
}
-212
View File
@@ -1,212 +0,0 @@
using System.Collections.ObjectModel;
using System.Windows.Input;
using Sharp.Wpf.Mvvm;
using Sharp.Wpf.Commands;
using Plc.Abstractions;
using Plc.Models;
using Plc.Siemens.Options;
using S7.Net;
using System.Windows;
namespace Client.ViewModels;
public class PlcViewModel : ViewModelBase
{
private readonly IPlcService _plcService;
private readonly SiemensPlcOptions _siemensOptions;
// 连接配置
private PlcBrand _selectedBrand = PlcBrand.Siemens;
private CpuType _selectedCpuType = CpuType.S71200;
private string _ipAddress = "127.0.0.1";
private string _status = "未连接";
// 读写测试
private string _readAddress = "DB1.DBW0";
private string _readValue = "-";
private string _writeAddress = "DB1.DBW0";
private string _writeValue = "0";
// 监控
private string _monitorAddress = "DB1.DBW0";
public PlcViewModel(IPlcService plcService, SiemensPlcOptions siemensOptions)
{
_plcService = plcService;
_siemensOptions = siemensOptions;
// 初始化界面值为当前配置值
_ipAddress = _siemensOptions.IpAddress;
_selectedCpuType = _siemensOptions.CpuType;
ConnectCommand = new AsyncCommand(ConnectAsync);
DisconnectCommand = new AsyncCommand(DisconnectAsync);
ReadCommand = new AsyncCommand(ReadAsync);
WriteCommand = new AsyncCommand(WriteAsync);
AddMonitorCommand = new RelayCommand(AddMonitor);
}
#region Properties
public PlcBrand SelectedBrand
{
get => _selectedBrand;
set
{
if (SetProperty(ref _selectedBrand, value))
{
OnPropertyChanged(nameof(IsSiemensOptionsVisible));
}
}
}
public CpuType SelectedCpuType
{
get => _selectedCpuType;
set => SetProperty(ref _selectedCpuType, value);
}
public bool IsSiemensOptionsVisible => SelectedBrand == PlcBrand.Siemens;
public string IpAddress
{
get => _ipAddress;
set => SetProperty(ref _ipAddress, value);
}
public string Status
{
get => _status;
set => SetProperty(ref _status, value);
}
public string ReadAddress
{
get => _readAddress;
set => SetProperty(ref _readAddress, value);
}
public string ReadValue
{
get => _readValue;
set => SetProperty(ref _readValue, value);
}
public string WriteAddress
{
get => _writeAddress;
set => SetProperty(ref _writeAddress, value);
}
public string WriteValue
{
get => _writeValue;
set => SetProperty(ref _writeValue, value);
}
public string MonitorAddress
{
get => _monitorAddress;
set => SetProperty(ref _monitorAddress, value);
}
// 数据源
public ObservableCollection<PlcBrand> Brands { get; } = new(Enum.GetValues<PlcBrand>());
public ObservableCollection<CpuType> CpuTypes { get; } = new(Enum.GetValues<CpuType>());
public ObservableCollection<PlcTag> MonitoredTags { get; } = new();
#endregion
#region Commands
public IAsyncCommand ConnectCommand { get; }
public IAsyncCommand DisconnectCommand { get; }
public IAsyncCommand ReadCommand { get; }
public IAsyncCommand WriteCommand { get; }
public ICommand AddMonitorCommand { get; }
#endregion
private async Task ConnectAsync()
{
if (SelectedBrand != PlcBrand.Siemens)
{
Status = $"暂不支持 {SelectedBrand},请等待后续更新";
return;
}
Status = "正在连接...";
try
{
// 1. 更新配置 (因为 SiemensDriver 注册为单例,且引用了 SiemensPlcOptions 单例)
// 修改这里的 Options 会直接影响 Driver 下一次 ConnectAsync 的行为
_siemensOptions.IpAddress = IpAddress;
_siemensOptions.CpuType = SelectedCpuType;
// 端口、机架号等暂时保持默认,也可以暴露在界面上
// 2. 启动服务 (内部会调用 Driver.ConnectAsync)
await _plcService.StartAsync();
Status = $"已连接 ({SelectedBrand} - {SelectedCpuType})";
}
catch (Exception ex)
{
Status = $"连接失败: {ex.Message}";
}
}
private async Task DisconnectAsync()
{
await _plcService.StopAsync();
Status = "已断开";
}
private async Task ReadAsync()
{
var result = await _plcService.ReadAsync<short>(ReadAddress);
if (result.IsSuccess)
{
ReadValue = result.Value.ToString();
}
else
{
ReadValue = $"Error: {result.ErrorMessage}";
}
}
private async Task WriteAsync()
{
if (short.TryParse(WriteValue, out var val))
{
var result = await _plcService.WriteAsync(WriteAddress, val);
if (result.IsFailure)
{
Status = $"写入失败: {result.ErrorMessage}";
}
else
{
Status = "写入成功";
}
}
else
{
Status = "输入值无效 (仅支持 short)";
}
}
private void AddMonitor()
{
var tag = new PlcTag
{
Address = MonitorAddress,
DataType = typeof(short),
OnChanged = (t, v) =>
{
// 注意:UI绑定更新需要在UI线程,这里简化处理,假设WPF做了同步或者是后台线程安全集合
t.Value = v;
}
};
_plcService.Subscribe(tag);
MonitoredTags.Add(tag);
}
}
@@ -1,4 +1,4 @@
#pragma checksum "..\..\..\App.xaml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "0048F04EA9C102506BBFAC3803E4711F700CF3ED"
#pragma checksum "..\..\..\App.xaml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "A01D87CBC408FE1E9934F8938867D92F17E3A2CF"
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
@@ -46,7 +46,7 @@ namespace Client {
/// InitializeComponent
/// </summary>
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "10.0.0.0")]
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "9.0.4.0")]
public void InitializeComponent() {
if (_contentLoaded) {
return;
@@ -66,7 +66,7 @@ namespace Client {
/// </summary>
[System.STAThreadAttribute()]
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "10.0.0.0")]
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "9.0.4.0")]
public static void Main() {
Client.App app = new Client.App();
app.InitializeComponent();
@@ -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>
//------------------------------------------------------------------------------
@@ -1,7 +1,5 @@
is_global = true
build_property.TargetFramework = net8.0-windows
build_property.TargetFrameworkIdentifier = .NETCoreApp
build_property.TargetFrameworkVersion = v8.0
build_property.TargetPlatformMinVersion = 7.0
build_property.UsingMicrosoftNETSdkWeb =
build_property.ProjectTypeGuids =
@@ -10,7 +8,7 @@ build_property.PlatformNeutralAssembly =
build_property.EnforceExtendedAnalyzerRules =
build_property._SupportedPlatformList = Linux,macOS,Windows
build_property.RootNamespace = Client
build_property.ProjectDir = D:\code\sharp-basic\src\Sharp\Client\
build_property.ProjectDir = D:\yuanll\sharp-basic\src\Sharp\Client\
build_property.EnableComHosting =
build_property.EnableGeneratedComInterfaceComImportInterop =
build_property.CsWinRTUseWindowsUIXamlProjections = false
@@ -0,0 +1,89 @@
#pragma checksum "..\..\..\MainWindow.xaml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "9E813BA3492F961B4CE0E3DE4E78408D972934B6"
//------------------------------------------------------------------------------
// <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 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;
using System.Diagnostics;
using System.Windows;
using System.Windows.Automation;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Controls.Ribbon;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Forms.Integration;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Markup;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Effects;
using System.Windows.Media.Imaging;
using System.Windows.Media.Media3D;
using System.Windows.Media.TextFormatting;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Shell;
namespace Client {
/// <summary>
/// MainWindow
/// </summary>
public partial class MainWindow : HandyControl.Controls.GlowWindow, System.Windows.Markup.IComponentConnector {
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/mainwindow.xaml", System.UriKind.Relative);
#line 1 "..\..\..\MainWindow.xaml"
System.Windows.Application.LoadComponent(this, resourceLocater);
#line default
#line hidden
}
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "9.0.4.0")]
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes")]
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")]
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily")]
void System.Windows.Markup.IComponentConnector.Connect(int connectionId, object target) {
this._contentLoaded = true;
}
}
}
+503
View File
@@ -0,0 +1,503 @@
using System.Data;
using System.Data.OleDb;
using System.Linq;
using System.Reflection;
using Microsoft.Extensions.Logging;
using Sharp.Data.Access.Connections;
using Sharp.Data.Access.Initialization;
using Sharp.Data.Access.Internal;
using Sharp.Data.Access.Options;
using Sharp.Data.Access.Security;
namespace Sharp.Data.Access;
/// <summary>
/// Access 数据库事务实现
/// </summary>
internal class AccessTransaction : IAccessTransaction
{
private readonly OleDbConnection _connection;
private readonly OleDbTransaction _transaction;
private readonly ILogger? _logger;
private readonly bool _ownsConnection;
private bool _disposed;
public AccessTransaction(OleDbConnection connection, bool ownsConnection, ILogger? logger)
{
_connection = connection;
_ownsConnection = ownsConnection;
_transaction = connection.BeginTransaction();
_logger = logger;
}
public int Execute(string sql, params OleDbParameter[] parameters)
{
using var command = new OleDbCommand(sql, _connection, _transaction);
if (parameters.Length > 0)
command.Parameters.AddRange(parameters);
return command.ExecuteNonQuery();
}
public List<T> Query<T>(string sql, params OleDbParameter[] parameters) where T : new()
{
using var command = new OleDbCommand(sql, _connection, _transaction);
if (parameters.Length > 0)
command.Parameters.AddRange(parameters);
var results = new List<T>();
using var reader = command.ExecuteReader();
while (reader.Read())
{
var item = MapReaderToObject<T>(reader);
results.Add(item);
}
return results;
}
public object? ExecuteScalar(string sql, params OleDbParameter[] parameters)
{
using var command = new OleDbCommand(sql, _connection, _transaction);
if (parameters.Length > 0)
command.Parameters.AddRange(parameters);
return command.ExecuteScalar();
}
public void Commit()
{
_transaction.Commit();
_logger?.LogDebug("事务已提交");
}
public void Rollback()
{
_transaction.Rollback();
_logger?.LogDebug("事务已回滚");
}
public void Dispose()
{
if (_disposed) return;
try
{
_transaction.Dispose();
}
finally
{
if (_ownsConnection)
{
_connection.Dispose();
}
}
_disposed = true;
}
private static T MapReaderToObject<T>(IDataReader reader) where T : new()
{
var obj = new T();
var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
// 创建字段名到索引的映射(大小写不敏感)
var fieldMap = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
for (int i = 0; i < reader.FieldCount; i++)
{
var fieldName = reader.GetName(i);
fieldMap[fieldName] = i;
// Access 列名有时包含末尾空格,提前做一次修剪以便映射
var trimmed = fieldName.Trim();
if (!fieldMap.ContainsKey(trimmed))
{
fieldMap[trimmed] = i;
}
}
foreach (var property in properties)
{
if (!property.CanWrite) continue;
try
{
// 尝试查找匹配的字段(大小写不敏感)
if (fieldMap.TryGetValue(property.Name, out int ordinal))
{
var value = reader.GetValue(ordinal);
if (value != DBNull.Value && value != null)
{
// 尝试类型转换
if (property.PropertyType == value.GetType())
{
property.SetValue(obj, value);
}
else if (property.PropertyType == typeof(string))
{
property.SetValue(obj, value.ToString());
}
else
{
// 尝试使用 Convert.ChangeType
var convertedValue = Convert.ChangeType(value, property.PropertyType);
property.SetValue(obj, convertedValue);
}
}
}
}
catch
{
// 字段不存在或类型转换失败,保持默认值
}
}
return obj;
}
}
/// <summary>
/// Access 数据服务实现(基于 System.Data.OleDb)
/// </summary>
public class AccessDataService : IAccessDataService
{
private readonly AccessDatabaseOptions _options;
private readonly IAccessConnectionStringFactory _connectionFactory;
private readonly IAccessDatabaseInitializer _initializer;
private readonly IAccessPasswordDecryptor _decryptor;
private readonly ILogger<AccessDataService>? _logger;
private readonly string _connectionString;
public AccessDataService(
AccessDatabaseOptions options,
IAccessConnectionStringFactory? connectionFactory = null,
IAccessDatabaseInitializer? initializer = null,
IAccessPasswordDecryptor? decryptor = null,
ILogger<AccessDataService>? logger = null)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
_options.Validate();
_connectionFactory = connectionFactory ?? new AccessConnectionStringFactory(decryptor ?? new PassthroughAccessPasswordDecryptor());
_initializer = initializer ?? new AccessDatabaseInitializer();
_decryptor = decryptor ?? new PassthroughAccessPasswordDecryptor();
_logger = logger;
try
{
// 确保数据库存在(如果配置了模板复制)
_initializer.EnsureDatabase(_options, NullServiceProvider.Instance);
// 构建连接字符串
_connectionString = _connectionFactory.Create(_options, NullServiceProvider.Instance);
_logger?.LogInformation("AccessDataService 已初始化,数据库: {DatabaseFile}, Provider: {Provider}",
_options.DatabaseFile, _options.ProviderName);
_logger?.LogDebug("实际连接字符串: {ConnectionString}", _connectionString);
}
catch (Exception ex)
{
_logger?.LogError(ex, "AccessDataService 初始化失败");
// 提供详细的错误诊断信息
if (ex.Message.Contains("找不到") || ex is FileNotFoundException)
{
throw new InvalidOperationException(
$"找不到 Access 数据库文件: {_options.DatabaseFile}。" +
$"请确保文件存在或配置 TemplateFile 以自动创建。", ex);
}
throw;
}
}
public List<T> Query<T>(string sql, params OleDbParameter[] parameters) where T : new()
{
_logger?.LogDebug("执行查询: {Sql}", sql);
return ExecuteWithRetry(() =>
{
using var connection = CreateConnection();
connection.Open();
using var command = new OleDbCommand(sql, connection);
if (parameters.Length > 0)
{
// 克隆参数以避免重试时的"已包含在另一个集合中"错误
var clonedParams = parameters.Select(p => CloneParameter(p)).ToArray();
command.Parameters.AddRange(clonedParams);
}
var results = new List<T>();
using var reader = command.ExecuteReader();
while (reader.Read())
{
var item = MapReaderToObject<T>(reader);
results.Add(item);
}
_logger?.LogDebug("查询返回 {Count} 条记录", results.Count);
return results;
});
}
public T? QueryFirstOrDefault<T>(string sql, params OleDbParameter[] parameters) where T : new()
{
_logger?.LogDebug("执行单条查询: {Sql}", sql);
return ExecuteWithRetry(() =>
{
using var connection = CreateConnection();
connection.Open();
using var command = new OleDbCommand(sql, connection);
if (parameters.Length > 0)
{
// 克隆参数以避免重试时的"已包含在另一个集合中"错误
var clonedParams = parameters.Select(p => CloneParameter(p)).ToArray();
command.Parameters.AddRange(clonedParams);
}
using var reader = command.ExecuteReader();
if (reader.Read())
{
return MapReaderToObject<T>(reader);
}
return default;
});
}
public object? ExecuteScalar(string sql, params OleDbParameter[] parameters)
{
_logger?.LogDebug("执行标量查询: {Sql}", sql);
return ExecuteWithRetry(() =>
{
using var connection = CreateConnection();
connection.Open();
using var command = new OleDbCommand(sql, connection);
if (parameters.Length > 0)
{
// 克隆参数以避免重试时的"已包含在另一个集合中"错误
var clonedParams = parameters.Select(p => CloneParameter(p)).ToArray();
command.Parameters.AddRange(clonedParams);
}
var result = command.ExecuteScalar();
_logger?.LogDebug("标量查询返回: {Result}", result);
return result;
});
}
public int Execute(string sql, params OleDbParameter[] parameters)
{
_logger?.LogDebug("执行命令: {Sql}", sql);
return ExecuteWithRetry(() =>
{
using var connection = CreateConnection();
connection.Open();
using var command = new OleDbCommand(sql, connection);
if (parameters.Length > 0)
{
// 克隆参数以避免重试时的"已包含在另一个集合中"错误
var clonedParams = parameters.Select(p => CloneParameter(p)).ToArray();
command.Parameters.AddRange(clonedParams);
}
var rowsAffected = command.ExecuteNonQuery();
_logger?.LogDebug("命令影响了 {RowsAffected} 行", rowsAffected);
return rowsAffected;
});
}
public IAccessTransaction BeginTransaction()
{
_logger?.LogDebug("开始事务");
var connection = CreateConnection();
connection.Open();
return new AccessTransaction(connection, ownsConnection: true, _logger);
}
public void ExecuteInTransaction(Action<IAccessTransaction> action)
{
_logger?.LogDebug("在事务中执行操作");
using var transaction = BeginTransaction();
try
{
action(transaction);
transaction.Commit();
_logger?.LogDebug("事务执行成功");
}
catch (Exception ex)
{
_logger?.LogError(ex, "事务执行失败,正在回滚");
transaction.Rollback();
throw;
}
}
public bool TestConnection()
{
try
{
_logger?.LogDebug("测试数据库连接");
using var connection = CreateConnection();
connection.Open();
_logger?.LogDebug("数据库连接成功");
return true;
}
catch (Exception ex)
{
_logger?.LogError(ex, "数据库连接失败");
return false;
}
}
private OleDbConnection CreateConnection()
{
return new OleDbConnection(_connectionString);
}
private T ExecuteWithRetry<T>(Func<T> operation)
{
var maxRetries = _options.MaxRetryCount;
var retryDelay = _options.RetryDelayMilliseconds;
for (int attempt = 0; attempt <= maxRetries; attempt++)
{
try
{
return operation();
}
catch (OleDbException ex) when (attempt < maxRetries && IsRetriableError(ex))
{
_logger?.LogWarning(ex, "数据库操作失败,正在重试 ({Attempt}/{MaxRetries})...", attempt + 1, maxRetries);
Thread.Sleep(retryDelay);
}
catch (OleDbException ex)
{
// 提供详细的错误诊断信息
var errorMessage = $"Access 数据库操作失败: {ex.Message}";
// 检查是否是 Provider 相关的错误
if (ex.Message.Contains("找不到") || ex.Message.Contains("未注册") ||
ex.Message.Contains("OLE DB") || ex.Message.Contains("多步"))
{
errorMessage += $"\n\n可能的原因:" +
$"\n1. 未安装 Access Database Engine:当前使用 Provider = {_options.ProviderName}" +
$"\n - 如果使用 16.0,需要安装 Access Database Engine 2016 或 Office 2016+" +
$"\n - 如果使用 12.0,需要安装 Access Database Engine 2010 或 Office 2010+" +
$"\n - 建议:优先使用 12.0 版本,兼容性最好" +
$"\n2. 32位/64位不匹配:进程架构必须与 Access Driver 架构一致" +
$"\n3. 数据库文件损坏或被其他程序锁定" +
$"\n4. 数据库文件路径不正确:{_options.DatabaseFile}";
_logger?.LogError(ex, "数据库连接失败,Provider: {Provider}", _options.ProviderName);
}
throw new InvalidOperationException(errorMessage, ex);
}
}
// 不会到这里,因为最后一次尝试会抛出异常
throw new InvalidOperationException("重试逻辑错误");
}
private static bool IsRetriableError(OleDbException ex)
{
// Access 数据库常见的可重试错误
// -2147467259: 数据库被锁定
// -2147217865: 无法打开数据库(可能正在被其他进程使用)
return ex.ErrorCode == -2147467259 || ex.ErrorCode == -2147217865;
}
/// <summary>
/// 克隆 OleDbParameter 以避免重试时参数被重复添加的问题
/// </summary>
private static OleDbParameter CloneParameter(OleDbParameter original)
{
var clone = new OleDbParameter
{
ParameterName = original.ParameterName,
Value = original.Value,
OleDbType = original.OleDbType,
Size = original.Size,
Direction = original.Direction,
IsNullable = original.IsNullable,
Precision = original.Precision,
Scale = original.Scale,
SourceColumn = original.SourceColumn,
SourceVersion = original.SourceVersion
};
return clone;
}
private static T MapReaderToObject<T>(IDataReader reader) where T : new()
{
var obj = new T();
var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
// 创建字段名到索引的映射(大小写不敏感)
var fieldMap = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
for (int i = 0; i < reader.FieldCount; i++)
{
var fieldName = reader.GetName(i);
fieldMap[fieldName] = i;
// Access 列名有时包含末尾空格,提前做一次修剪以便映射
var trimmed = fieldName.Trim();
if (!fieldMap.ContainsKey(trimmed))
{
fieldMap[trimmed] = i;
}
}
foreach (var property in properties)
{
if (!property.CanWrite) continue;
try
{
// 尝试查找匹配的字段(大小写不敏感)
if (fieldMap.TryGetValue(property.Name, out int ordinal))
{
var value = reader.GetValue(ordinal);
if (value != DBNull.Value && value != null)
{
// 尝试类型转换
if (property.PropertyType == value.GetType())
{
property.SetValue(obj, value);
}
else if (property.PropertyType == typeof(string))
{
property.SetValue(obj, value.ToString());
}
else
{
// 尝试使用 Convert.ChangeType
var convertedValue = Convert.ChangeType(value, property.PropertyType);
property.SetValue(obj, convertedValue);
}
}
}
}
catch
{
// 字段不存在或类型转换失败,保持默认值
}
}
return obj;
}
}
@@ -0,0 +1,118 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Sharp.Data.Access.Options;
using Sharp.Data.Access.Security;
using Sharp.Data.Access.Connections;
using Sharp.Data.Access.Initialization;
namespace Sharp.Data.Access;
/// <summary>
/// Access 数据服务的依赖注入扩展
/// </summary>
public static class AccessDataServiceExtensions
{
/// <summary>
/// 添加 Access 数据服务(最简单方式 - 仅指定数据库路径)
/// </summary>
/// <param name="services">服务集合</param>
/// <param name="databasePath">数据库文件路径(绝对或相对路径)</param>
/// <param name="password">数据库密码(可选)</param>
/// <param name="provider">OLE DB 提供程序,默认:Microsoft.ACE.OLEDB.16.0</param>
/// <returns>服务集合</returns>
public static IServiceCollection AddAccessDataService(
this IServiceCollection services,
string databasePath,
string? password = null,
string? provider = null)
{
return services.AddAccessDataService(options =>
{
options.DatabaseFile = databasePath;
if (!string.IsNullOrEmpty(password))
options.Password = password;
if (!string.IsNullOrEmpty(provider))
options.ProviderName = provider;
});
}
/// <summary>
/// 添加 Access 数据服务(使用配置委托)
/// </summary>
/// <param name="services">服务集合</param>
/// <param name="configureOptions">配置委托</param>
/// <returns>服务集合</returns>
public static IServiceCollection AddAccessDataService(
this IServiceCollection services,
Action<AccessDatabaseOptions> configureOptions)
{
if (services == null) throw new ArgumentNullException(nameof(services));
if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions));
var options = new AccessDatabaseOptions();
configureOptions(options);
options.Validate();
// 注册核心服务
services.TryAddSingleton<IAccessPasswordDecryptor, PassthroughAccessPasswordDecryptor>();
services.TryAddSingleton<IAccessConnectionStringFactory>(sp =>
new AccessConnectionStringFactory(sp.GetRequiredService<IAccessPasswordDecryptor>()));
services.TryAddSingleton<IAccessDatabaseInitializer, AccessDatabaseInitializer>();
// 注册配置
services.AddSingleton(options);
// 注册 AccessDataService
services.AddSingleton<IAccessDataService>(sp =>
new AccessDataService(
options,
sp.GetRequiredService<IAccessConnectionStringFactory>(),
sp.GetRequiredService<IAccessDatabaseInitializer>(),
sp.GetRequiredService<IAccessPasswordDecryptor>(),
sp.GetService<Microsoft.Extensions.Logging.ILogger<AccessDataService>>()));
return services;
}
/// <summary>
/// 添加 Access 数据服务(使用配置对象)
/// </summary>
/// <param name="services">服务集合</param>
/// <param name="options">配置选项</param>
/// <returns>服务集合</returns>
public static IServiceCollection AddAccessDataService(
this IServiceCollection services,
AccessDatabaseOptions options)
{
if (options == null) throw new ArgumentNullException(nameof(options));
return services.AddAccessDataService(_ => { });
}
/// <summary>
/// 注册自定义密码解密器
/// </summary>
/// <typeparam name="TDecryptor">解密器类型</typeparam>
/// <param name="services">服务集合</param>
/// <returns>服务集合</returns>
public static IServiceCollection AddAccessPasswordDecryptor<TDecryptor>(this IServiceCollection services)
where TDecryptor : class, IAccessPasswordDecryptor
{
services.AddSingleton<IAccessPasswordDecryptor, TDecryptor>();
return services;
}
/// <summary>
/// 注册自定义密码解密器(使用工厂方法)
/// </summary>
/// <param name="services">服务集合</param>
/// <param name="implementationFactory">解密器工厂</param>
/// <returns>服务集合</returns>
public static IServiceCollection AddAccessPasswordDecryptor(
this IServiceCollection services,
Func<IServiceProvider, IAccessPasswordDecryptor> implementationFactory)
{
services.AddSingleton(implementationFactory);
return services;
}
}
@@ -0,0 +1,58 @@
using System;
using System.Data.OleDb;
using Sharp.Data.Access.Internal;
using Sharp.Data.Access.Options;
using Sharp.Data.Access.Security;
namespace Sharp.Data.Access.Connections;
internal sealed class AccessConnectionStringFactory : IAccessConnectionStringFactory
{
private readonly IAccessPasswordDecryptor _decryptor;
public AccessConnectionStringFactory(IAccessPasswordDecryptor decryptor)
{
_decryptor = decryptor ?? throw new ArgumentNullException(nameof(decryptor));
}
public string Create(AccessDatabaseOptions options, IServiceProvider services)
{
var configuration = AccessDatabaseConfiguration.FromOptions(options, services);
if (!string.IsNullOrWhiteSpace(configuration.ConnectionString))
{
return configuration.ConnectionString;
}
if (string.IsNullOrWhiteSpace(configuration.DatabaseFile))
{
throw new InvalidOperationException("Access 数据库需要提供 DatabaseFile 或 ConnectionString。");
}
// 基础连接字符串
// 注意:Connect Timeout 参数在某些 ACE 版本中不稳定,已移除
var builder = new OleDbConnectionStringBuilder
{
["Provider"] = configuration.ProviderName,
["Data Source"] = AccessPathResolver.Resolve(configuration.DatabaseFile, configuration.DataDirectory)
};
var password = configuration.Password;
if (string.IsNullOrEmpty(password) && !string.IsNullOrWhiteSpace(configuration.EncryptedPassword))
{
password = _decryptor.Decrypt(configuration.EncryptedPassword);
}
if (!string.IsNullOrEmpty(password))
{
builder["Jet OLEDB:Database Password"] = password;
}
foreach (var parameter in configuration.AdditionalParameters)
{
builder[parameter.Key] = parameter.Value;
}
return builder.ConnectionString;
}
}
@@ -0,0 +1,12 @@
using System;
using Sharp.Data.Access.Options;
namespace Sharp.Data.Access.Connections;
/// <summary>
/// 构建 Access 连接字符串的工厂。
/// </summary>
public interface IAccessConnectionStringFactory
{
string Create(AccessDatabaseOptions options, IServiceProvider services);
}
+20
View File
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>Sharp.Data.Access</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="EntityFrameworkCore.Jet" Version="8.0.0-preview1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
<PackageReference Include="System.Data.OleDb" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Data\Data.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,72 @@
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Sharp.Data.Access.Connections;
using Sharp.Data.Access.Initialization;
using Sharp.Data.Access.Internal;
using Sharp.Data.Access.Options;
using Sharp.Data.Access.Security;
using Sharp.Data.Context;
namespace Sharp.Data.Access.DependencyInjection;
/// <summary>
/// Access 模块的依赖注入扩展。
/// </summary>
public static class AccessServiceCollectionExtensions
{
public static IServiceCollection AddAccessDataContext<TContext>(this IServiceCollection services,
Action<AccessContextOptions<TContext>> configure,
bool registerDataContext = true,
ServiceLifetime dataContextLifetime = ServiceLifetime.Scoped)
where TContext : DbContext
{
if (configure == null) throw new ArgumentNullException(nameof(configure));
return services.AddAccessDataContext(_ =>
{
var options = new AccessContextOptions<TContext>();
configure(options);
return options;
}, registerDataContext, dataContextLifetime);
}
public static IServiceCollection AddAccessDataContext<TContext>(this IServiceCollection services,
Func<IServiceProvider, AccessContextOptions<TContext>> configure,
bool registerDataContext = true,
ServiceLifetime dataContextLifetime = ServiceLifetime.Scoped)
where TContext : DbContext
{
if (services == null) throw new ArgumentNullException(nameof(services));
if (configure == null) throw new ArgumentNullException(nameof(configure));
services.TryAddSingleton<IAccessPasswordDecryptor, PassthroughAccessPasswordDecryptor>();
services.TryAddSingleton<IAccessConnectionStringFactory>(sp =>
new AccessConnectionStringFactory(sp.GetRequiredService<IAccessPasswordDecryptor>()));
services.TryAddSingleton<IAccessDatabaseInitializer, AccessDatabaseInitializer>();
services.TryAddSingleton(provider =>
{
var options = configure(provider) ?? throw new InvalidOperationException("AccessContextOptions 不能为空。");
options.Validate();
return options;
});
services.AddScoped<IAccessDataContextFactory<TContext>>(sp =>
new AccessDataContextFactory<TContext>(
sp,
sp.GetRequiredService<AccessContextOptions<TContext>>(),
sp.GetRequiredService<IAccessConnectionStringFactory>(),
sp.GetRequiredService<IAccessDatabaseInitializer>()));
if (registerDataContext)
{
services.Add(new ServiceDescriptor(typeof(IDataContext),
sp => sp.GetRequiredService<IAccessDataContextFactory<TContext>>().Create(),
dataContextLifetime));
}
return services;
}
}
@@ -0,0 +1,66 @@
using System;
using EntityFrameworkCore.Jet;
using EntityFrameworkCore.Jet.Infrastructure;
using Microsoft.EntityFrameworkCore;
using Sharp.Data.Access.Connections;
using Sharp.Data.Access.Initialization;
using Sharp.Data.Access.Internal;
using Sharp.Data.Access.Options;
using Sharp.Data.Access.Security;
using Sharp.Data;
namespace Sharp.Data.Access.Extensions;
/// <summary>
/// 手动使用 DataContextBuilder 时的 Access Helper。
/// </summary>
public static class AccessDataContextBuilderExtensions
{
/// <summary>
/// 通过现成的连接字符串配置 Access。
/// </summary>
public static DataContextBuilder<TContext> UseAccessDatabase<TContext>(
this DataContextBuilder<TContext> builder,
string connectionString,
Action<JetDbContextOptionsBuilder>? configure = null)
where TContext : DbContext
{
if (builder == null) throw new ArgumentNullException(nameof(builder));
if (string.IsNullOrWhiteSpace(connectionString))
{
throw new ArgumentException("连接字符串不能为空。", nameof(connectionString));
}
builder.ConfigureDbContext(options => options.UseJet(connectionString, configure));
return builder;
}
/// <summary>
/// 通过 AccessDatabaseOptions 配置 Access。
/// </summary>
public static DataContextBuilder<TContext> UseAccessDatabase<TContext>(
this DataContextBuilder<TContext> builder,
Action<AccessDatabaseOptions> configureOptions,
IServiceProvider? services = null,
IAccessPasswordDecryptor? decryptor = null,
Action<JetDbContextOptionsBuilder>? configure = null)
where TContext : DbContext
{
if (builder == null) throw new ArgumentNullException(nameof(builder));
if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions));
var options = new AccessDatabaseOptions();
configureOptions(options);
options.Validate();
var serviceProvider = services ?? NullServiceProvider.Instance;
var initializer = new AccessDatabaseInitializer();
var connectionFactory = new AccessConnectionStringFactory(decryptor ?? new PassthroughAccessPasswordDecryptor());
initializer.EnsureDatabase(options, serviceProvider);
var connectionString = connectionFactory.Create(options, serviceProvider);
builder.ConfigureDbContext(o => o.UseJet(connectionString, configure));
return builder;
}
}
@@ -0,0 +1,12 @@
using Microsoft.EntityFrameworkCore;
using Sharp.Data.Context;
namespace Sharp.Data.Access;
/// <summary>
/// Access 数据上下文工厂,避免直接依赖具体实现。
/// </summary>
public interface IAccessDataContextFactory<TContext> where TContext : DbContext
{
IDataContext Create();
}
@@ -0,0 +1,75 @@
using System.Data.OleDb;
namespace Sharp.Data.Access;
/// <summary>
/// Access 数据库事务接口
/// </summary>
public interface IAccessTransaction : IDisposable
{
/// <summary>
/// 执行非查询命令(INSERT, UPDATE, DELETE)
/// </summary>
int Execute(string sql, params OleDbParameter[] parameters);
/// <summary>
/// 查询多条记录
/// </summary>
List<T> Query<T>(string sql, params OleDbParameter[] parameters) where T : new();
/// <summary>
/// 执行标量查询(COUNT, SUM等)
/// </summary>
object? ExecuteScalar(string sql, params OleDbParameter[] parameters);
/// <summary>
/// 提交事务
/// </summary>
void Commit();
/// <summary>
/// 回滚事务
/// </summary>
void Rollback();
}
/// <summary>
/// Access 数据服务接口(基于 System.Data.OleDb)
/// </summary>
public interface IAccessDataService
{
/// <summary>
/// 查询多条记录
/// </summary>
List<T> Query<T>(string sql, params OleDbParameter[] parameters) where T : new();
/// <summary>
/// 查询单条记录
/// </summary>
T? QueryFirstOrDefault<T>(string sql, params OleDbParameter[] parameters) where T : new();
/// <summary>
/// 执行标量查询(COUNT, SUM等)
/// </summary>
object? ExecuteScalar(string sql, params OleDbParameter[] parameters);
/// <summary>
/// 执行非查询命令(INSERT, UPDATE, DELETE)
/// </summary>
int Execute(string sql, params OleDbParameter[] parameters);
/// <summary>
/// 开始事务
/// </summary>
IAccessTransaction BeginTransaction();
/// <summary>
/// 在事务中执行操作(自动提交/回滚)
/// </summary>
void ExecuteInTransaction(Action<IAccessTransaction> action);
/// <summary>
/// 测试数据库连接
/// </summary>
bool TestConnection();
}
@@ -0,0 +1,49 @@
using System;
using System.IO;
using Sharp.Data.Access.Internal;
using Sharp.Data.Access.Options;
namespace Sharp.Data.Access.Initialization;
internal sealed class AccessDatabaseInitializer : IAccessDatabaseInitializer
{
public void EnsureDatabase(AccessDatabaseOptions options, IServiceProvider services)
{
var configuration = AccessDatabaseConfiguration.FromOptions(options, services);
if (!string.IsNullOrWhiteSpace(configuration.ConnectionString) ||
string.IsNullOrWhiteSpace(configuration.DatabaseFile))
{
return;
}
var databasePath = AccessPathResolver.Resolve(configuration.DatabaseFile, configuration.DataDirectory);
var directory = Path.GetDirectoryName(databasePath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
if (File.Exists(databasePath))
{
return;
}
if (configuration.CopyTemplateWhenMissing && !string.IsNullOrWhiteSpace(configuration.TemplateFile))
{
var templatePath = AccessPathResolver.Resolve(configuration.TemplateFile, configuration.DataDirectory);
if (!File.Exists(templatePath))
{
throw new FileNotFoundException($"Access 模板文件 \"{templatePath}\" 不存在。", templatePath);
}
File.Copy(templatePath, databasePath);
return;
}
if (configuration.ThrowIfMissing)
{
throw new FileNotFoundException($"Access 数据库 \"{databasePath}\" 不存在。", databasePath);
}
}
}
@@ -0,0 +1,12 @@
using System;
using Sharp.Data.Access.Options;
namespace Sharp.Data.Access.Initialization;
/// <summary>
/// Access 数据库初始化器(确保目录/数据库文件准备就绪)。
/// </summary>
public interface IAccessDatabaseInitializer
{
void EnsureDatabase(AccessDatabaseOptions options, IServiceProvider services);
}
@@ -0,0 +1,48 @@
using System;
using EntityFrameworkCore.Jet;
using Microsoft.EntityFrameworkCore;
using Sharp.Data;
using Sharp.Data.Access.Connections;
using Sharp.Data.Access.Initialization;
using Sharp.Data.Access.Options;
using Sharp.Data.Context;
namespace Sharp.Data.Access.Internal;
internal sealed class AccessDataContextFactory<TContext> : IAccessDataContextFactory<TContext>
where TContext : DbContext
{
private readonly IServiceProvider _services;
private readonly AccessContextOptions<TContext> _options;
private readonly IAccessConnectionStringFactory _connectionFactory;
private readonly IAccessDatabaseInitializer _initializer;
public AccessDataContextFactory(
IServiceProvider services,
AccessContextOptions<TContext> options,
IAccessConnectionStringFactory connectionFactory,
IAccessDatabaseInitializer initializer)
{
_services = services;
_options = options;
_connectionFactory = connectionFactory;
_initializer = initializer;
}
public IDataContext Create()
{
var builder = new DataContextBuilder<TContext>();
_initializer.EnsureDatabase(_options.Database, _services);
var connectionString = _connectionFactory.Create(_options.Database, _services);
builder.ConfigureDbContext(options =>
{
options.UseJet(connectionString, jet => _options.ConfigureJet?.Invoke(jet));
});
_options.ConfigureDataContext?.Invoke(_services, builder);
return builder.Build();
}
}
@@ -0,0 +1,61 @@
using System;
using System.Collections.Generic;
using Sharp.Data.Access.Options;
namespace Sharp.Data.Access.Internal;
internal sealed record AccessDatabaseConfiguration(
string? ConnectionString,
string? DatabaseFile,
string ProviderName,
string? Password,
string? EncryptedPassword,
string? TemplateFile,
string? DataDirectory,
int ConnectionTimeout,
int MaxRetryCount,
int RetryDelayMilliseconds,
IReadOnlyDictionary<string, string> AdditionalParameters,
bool CopyTemplateWhenMissing,
bool ThrowIfMissing)
{
public static AccessDatabaseConfiguration FromOptions(AccessDatabaseOptions options, IServiceProvider services)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
var connectionString = Resolve(options.ConnectionString, options.ConnectionStringFactory, services);
var databaseFile = Resolve(options.DatabaseFile, options.DatabaseFileFactory, services);
var templateFile = Resolve(options.TemplateFile, options.TemplateFileFactory, services);
var dataDirectory = Resolve(options.DataDirectory, options.DataDirectoryFactory, services);
var password = Resolve(options.Password, options.PasswordFactory, services);
var encryptedPassword = Resolve(options.EncryptedPassword, options.EncryptedPasswordFactory, services);
return new AccessDatabaseConfiguration(
connectionString,
databaseFile,
string.IsNullOrWhiteSpace(options.ProviderName) ? AccessProviderNames.AceOleDb16 : options.ProviderName,
password,
encryptedPassword,
templateFile,
dataDirectory,
options.ConnectionTimeout,
options.MaxRetryCount,
options.RetryDelayMilliseconds,
new Dictionary<string, string>(options.AdditionalParameters, StringComparer.OrdinalIgnoreCase),
options.CopyTemplateWhenMissing,
options.ThrowIfMissing);
}
private static string? Resolve(string? value, Func<IServiceProvider, string?>? factory, IServiceProvider services)
{
if (!string.IsNullOrWhiteSpace(value))
{
return value;
}
return factory?.Invoke(services);
}
}
@@ -0,0 +1,26 @@
using System;
using System.IO;
namespace Sharp.Data.Access.Internal;
internal static class AccessPathResolver
{
public static string Resolve(string path, string? baseDirectory)
{
if (string.IsNullOrWhiteSpace(path))
{
throw new InvalidOperationException("Access 数据库路径不能为空。");
}
if (Path.IsPathFullyQualified(path))
{
return Path.GetFullPath(path);
}
var root = string.IsNullOrWhiteSpace(baseDirectory)
? AppContext.BaseDirectory
: baseDirectory!;
return Path.GetFullPath(Path.Combine(root, path));
}
}
@@ -0,0 +1,14 @@
using System;
namespace Sharp.Data.Access.Internal;
internal sealed class NullServiceProvider : IServiceProvider
{
public static readonly NullServiceProvider Instance = new();
private NullServiceProvider()
{
}
public object? GetService(Type serviceType) => null;
}
@@ -0,0 +1,30 @@
using System;
using EntityFrameworkCore.Jet.Infrastructure;
using Microsoft.EntityFrameworkCore;
using Sharp.Data;
namespace Sharp.Data.Access.Options;
/// <summary>
/// Access 数据上下文注册配置。
/// </summary>
/// <typeparam name="TContext">EF Core DbContext 类型。</typeparam>
public sealed class AccessContextOptions<TContext> where TContext : DbContext
{
/// <summary>
/// Access 数据库配置(连接方式、密码等)。
/// </summary>
public AccessDatabaseOptions Database { get; } = new();
/// <summary>
/// 允许调用方在 Build 之前继续配置 DataContextBuilder(例如启用审计、软删除、当前用户等)。
/// </summary>
public Action<IServiceProvider, DataContextBuilder<TContext>>? ConfigureDataContext { get; set; }
/// <summary>
/// 细化 Jet Provider 选项。
/// </summary>
public Action<JetDbContextOptionsBuilder>? ConfigureJet { get; set; }
internal void Validate() => Database.Validate();
}
@@ -0,0 +1,143 @@
using System;
using System.Collections.Generic;
namespace Sharp.Data.Access.Options;
/// <summary>
/// Access 数据库统一配置项,同时支持 OleDb 和 EF Core 两种方式。
/// </summary>
public class AccessDatabaseOptions
{
/// <summary>
/// 完整的连接字符串,可选。如果提供则优先使用,不再重新构建。
/// </summary>
public string? ConnectionString { get; set; }
/// <summary>
/// 通过依赖注入动态生成连接字符串。
/// </summary>
public Func<IServiceProvider, string?>? ConnectionStringFactory { get; set; }
/// <summary>
/// Access 数据库文件路径(相对或绝对)。
/// </summary>
public string? DatabaseFile { get; set; }
/// <summary>
/// 通过依赖注入动态解析数据库文件路径。
/// </summary>
public Func<IServiceProvider, string?>? DatabaseFileFactory { get; set; }
/// <summary>
/// 数据文件的基础目录(如果数据库路径为相对路径则会拼接)。
/// 默认使用 <see cref="AppContext.BaseDirectory" />。
/// </summary>
public string? DataDirectory { get; set; }
/// <summary>
/// 通过依赖注入动态解析数据目录。
/// </summary>
public Func<IServiceProvider, string?>? DataDirectoryFactory { get; set; }
/// <summary>
/// 明文密码,可选。用于访问加密的 Access 数据库。
/// </summary>
public string? Password { get; set; }
/// <summary>
/// 通过依赖注入动态解析明文密码。
/// </summary>
public Func<IServiceProvider, string?>? PasswordFactory { get; set; }
/// <summary>
/// 加密后的密码,可选。将通过 <see cref="Security.IAccessPasswordDecryptor" /> 解密。
/// </summary>
public string? EncryptedPassword { get; set; }
/// <summary>
/// 通过依赖注入动态解析加密密码。
/// </summary>
public Func<IServiceProvider, string?>? EncryptedPasswordFactory { get; set; }
/// <summary>
/// 数据库模板(*.mdb / *.accdb),在文件不存在时复制一份。
/// </summary>
public string? TemplateFile { get; set; }
/// <summary>
/// 通过依赖注入动态解析模板文件路径。
/// </summary>
public Func<IServiceProvider, string?>? TemplateFileFactory { get; set; }
/// <summary>
/// 当数据库文件缺失时是否尝试从模板复制。
/// </summary>
public bool CopyTemplateWhenMissing { get; set; } = true;
/// <summary>
/// 当数据库文件缺失(且无法复制)时是否抛出异常。
/// </summary>
public bool ThrowIfMissing { get; set; } = true;
/// <summary>
/// Provider 名称,默认为 ACE OLE DB 16.0(功能最完整)。
/// 可选值:
/// - AccessProviderNames.AceOleDb16 (Microsoft.ACE.OLEDB.16.0) - 推荐,需要 Office 2016+
/// - AccessProviderNames.AceOleDb12 (Microsoft.ACE.OLEDB.12.0) - 需要 Office 2010+,不支持 Connect Timeout
/// - AccessProviderNames.JetOleDb4 (Microsoft.Jet.OLEDB.4.0) - 仅支持 32 位和旧版 Access
/// </summary>
public string ProviderName { get; set; } = AccessProviderNames.AceOleDb16;
/// <summary>
/// 连接超时时间(秒),默认 15 秒。
/// </summary>
public int ConnectionTimeout { get; set; } = 15;
/// <summary>
/// 最大重试次数,默认 3 次。用于增强稳健性。
/// </summary>
public int MaxRetryCount { get; set; } = 3;
/// <summary>
/// 重试延迟(毫秒),默认 100 毫秒。
/// </summary>
public int RetryDelayMilliseconds { get; set; } = 100;
/// <summary>
/// 额外的 OLE DB 参数,按需添加。
/// </summary>
public IDictionary<string, string> AdditionalParameters { get; } =
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
internal void Validate()
{
if (string.IsNullOrWhiteSpace(ConnectionString) &&
ConnectionStringFactory is null &&
string.IsNullOrWhiteSpace(DatabaseFile) &&
DatabaseFileFactory is null)
{
throw new InvalidOperationException(
"AccessDatabaseOptions 需要连接字符串或数据库文件路径。");
}
if (string.IsNullOrWhiteSpace(ProviderName))
{
throw new InvalidOperationException("必须指定 Access Provider。");
}
if (ConnectionTimeout <= 0)
{
throw new InvalidOperationException("ConnectionTimeout 必须大于 0。");
}
if (MaxRetryCount < 0)
{
throw new InvalidOperationException("MaxRetryCount 不能小于 0。");
}
if (RetryDelayMilliseconds < 0)
{
throw new InvalidOperationException("RetryDelayMilliseconds 不能小于 0。");
}
}
}
@@ -0,0 +1,22 @@
namespace Sharp.Data.Access.Options;
/// <summary>
/// 常用 Access 数据库 Provider 名称。
/// </summary>
public static class AccessProviderNames
{
/// <summary>
/// Microsoft Access Database Engine 2016 (推荐)。
/// </summary>
public const string AceOleDb16 = "Microsoft.ACE.OLEDB.16.0";
/// <summary>
/// Microsoft Access Database Engine 2010。
/// </summary>
public const string AceOleDb12 = "Microsoft.ACE.OLEDB.12.0";
/// <summary>
/// 老旧的 Jet 4.0 Provider(仅限 32 位进程)。
/// </summary>
public const string JetOleDb4 = "Microsoft.Jet.OLEDB.4.0";
}
+374
View File
@@ -0,0 +1,374 @@
# AccessDataService 简化 API 使用指南
## 🎯 设计目标
Data.Access 模块的核心目标是**让调用者使用起来更简单**,将连接管理、SQL 执行、参数处理、事务管理等复杂度封装起来。
## 📊 代码对比
### ❌ 之前:直接使用 OleDb(复杂)
```csharp
// 每次都要手动创建连接
private OleDbConnection CreateConnection()
{
var dbPath = Path.GetFullPath("dbDwDmsSetup.accdb");
var connectionString = $"Provider=Microsoft.ACE.OLEDB.12.0;Data Source={dbPath}";
return new OleDbConnection(connectionString);
}
// 查询用户 - 需要写大量样板代码
private async Task<List<UserInfoDto>> LoadUsersAsync()
{
using var connection = CreateConnection();
await connection.OpenAsync();
using var command = new OleDbCommand("SELECT UserName, Password FROM UserInfo", connection);
using var reader = await Task.Run(() => command.ExecuteReader());
var users = new List<UserInfoDto>();
while (await Task.Run(() => reader.Read()))
{
var user = new UserInfoDto
{
UserName = reader["UserName"]?.ToString() ?? "",
Password = reader["Password"]?.ToString() ?? ""
};
users.Add(user);
}
return users;
}
// 删除用户 - 需要手动管理事务
private async Task DeleteUserAsync(string userName)
{
using var connection = CreateConnection();
await connection.OpenAsync();
using var transaction = connection.BeginTransaction();
try
{
using (var deletePermCmd = new OleDbCommand(
"DELETE FROM UserPermission WHERE UserInfo_UserName = ?", connection, transaction))
{
deletePermCmd.Parameters.AddWithValue("@UserName", userName);
await Task.Run(() => deletePermCmd.ExecuteNonQuery());
}
using (var deleteUserCmd = new OleDbCommand(
"DELETE FROM UserInfo WHERE UserName = ?", connection, transaction))
{
deleteUserCmd.Parameters.AddWithValue("@UserName", userName);
await Task.Run(() => deleteUserCmd.ExecuteNonQuery());
}
transaction.Commit();
}
catch
{
transaction.Rollback();
throw;
}
}
```
**问题**:
- 大量重复的连接管理代码
- 手动映射查询结果到对象
- 手动管理事务的提交和回滚
- 代码冗长,不易维护
---
### ✅ 现在:使用 AccessDataService(简洁)
```csharp
// 注入服务,无需手动创建连接
private readonly IAccessDataService _accessDataService;
// 查询用户 - 一行代码!自动映射!
private async Task<List<UserInfoDto>> LoadUsersAsync()
{
return await _accessDataService.QueryAsync<UserInfoDto>(
"SELECT UserName, [Password] FROM UserInfo");
}
// 删除用户 - 自动管理事务!
private async Task DeleteUserAsync(string userName)
{
await _accessDataService.ExecuteInTransactionAsync(async transaction =>
{
await transaction.ExecuteAsync(
"DELETE FROM UserPermission WHERE UserInfo_UserName = ?",
new OleDbParameter("@UserName", userName));
await transaction.ExecuteAsync(
"DELETE FROM UserInfo WHERE UserName = ?",
new OleDbParameter("@UserName", userName));
});
}
```
**优势**:
- ✅ 无需手动管理连接
- ✅ 自动泛型映射查询结果
- ✅ 自动管理事务(提交/回滚)
- ✅ 代码简洁,易于维护
- ✅ 从 60+ 行减少到 10 行左右
---
## 🚀 快速开始
### 1. 初始化服务
在 `App.xaml.cs` 中初始化:
```csharp
private void InitializeAccessDataService()
{
var databasePath = Path.GetFullPath("dbDwDmsSetup.accdb");
var options = new AccessDataServiceOptions
{
DatabasePath = databasePath
};
AccessDataService = new AccessDataService(options, LoggerFactory.CreateLogger<AccessDataService>());
}
```
### 2. 在窗口中使用
```csharp
public partial class AccessDatabaseWindowOleDb : Window
{
private readonly IAccessDataService _accessDataService;
public AccessDatabaseWindowOleDb()
{
InitializeComponent();
var app = (App)Application.Current;
_accessDataService = app.AccessDataService;
}
}
```
---
## 📚 API 使用示例
### 1. 查询多条记录
```csharp
// 自动映射到对象
var users = await _accessDataService.QueryAsync<UserInfoDto>(
"SELECT UserName, [Password] FROM UserInfo");
// 带参数的查询
var users = await _accessDataService.QueryAsync<UserInfoDto>(
"SELECT UserName, [Password] FROM UserInfo WHERE UserName LIKE ?",
new OleDbParameter("@Pattern", "%admin%"));
```
### 2. 查询单条记录
```csharp
var user = await _accessDataService.QueryFirstOrDefaultAsync<UserInfoDto>(
"SELECT UserName, [Password] FROM UserInfo WHERE UserName = ?",
new OleDbParameter("@UserName", "admin"));
if (user != null)
{
Console.WriteLine($"找到用户: {user.UserName}");
}
```
### 3. 执行标量查询
```csharp
// 查询数量
var count = await _accessDataService.ExecuteScalarAsync(
"SELECT COUNT(*) FROM UserInfo WHERE UserName = ?",
new OleDbParameter("@UserName", "admin"));
int userCount = Convert.ToInt32(count);
```
### 4. 插入数据
```csharp
var rowsAffected = await _accessDataService.ExecuteAsync(
"INSERT INTO UserInfo (UserName, [Password]) VALUES (?, ?)",
new OleDbParameter("@UserName", "newuser"),
new OleDbParameter("@Password", "password123"));
Console.WriteLine($"插入了 {rowsAffected} 行");
```
### 5. 更新数据
```csharp
var rowsAffected = await _accessDataService.ExecuteAsync(
"UPDATE UserInfo SET [Password] = ? WHERE UserName = ?",
new OleDbParameter("@Password", "newpassword"),
new OleDbParameter("@UserName", "admin"));
Console.WriteLine($"更新了 {rowsAffected} 行");
```
### 6. 删除数据
```csharp
var rowsAffected = await _accessDataService.ExecuteAsync(
"DELETE FROM UserInfo WHERE UserName = ?",
new OleDbParameter("@UserName", "testuser"));
Console.WriteLine($"删除了 {rowsAffected} 行");
```
### 7. 使用事务(自动提交/回滚)
```csharp
// 推荐:使用 ExecuteInTransactionAsync(自动管理)
await _accessDataService.ExecuteInTransactionAsync(async transaction =>
{
// 删除权限
await transaction.ExecuteAsync(
"DELETE FROM UserPermission WHERE UserInfo_UserName = ?",
new OleDbParameter("@UserName", userName));
// 删除用户
await transaction.ExecuteAsync(
"DELETE FROM UserInfo WHERE UserName = ?",
new OleDbParameter("@UserName", userName));
// 自动提交!如果有异常,自动回滚!
});
```
### 8. 手动管理事务
```csharp
// 如果需要手动控制
using var transaction = _accessDataService.BeginTransaction();
try
{
await transaction.ExecuteAsync("INSERT INTO ...", ...);
await transaction.ExecuteAsync("UPDATE ...", ...);
transaction.Commit(); // 手动提交
}
catch
{
transaction.Rollback(); // 手动回滚
throw;
}
```
### 9. 测试连接
```csharp
var success = await _accessDataService.TestConnectionAsync();
if (success)
{
Console.WriteLine("数据库连接成功!");
}
```
---
## 🎨 完整示例
```csharp
public class UserService
{
private readonly IAccessDataService _accessDataService;
public UserService(IAccessDataService accessDataService)
{
_accessDataService = accessDataService;
}
// 获取所有用户
public async Task<List<UserInfoDto>> GetAllUsersAsync()
{
return await _accessDataService.QueryAsync<UserInfoDto>(
"SELECT UserName, [Password] FROM UserInfo");
}
// 获取用户权限数量
public async Task<int> GetUserPermissionCountAsync(string userName)
{
var count = await _accessDataService.ExecuteScalarAsync(
"SELECT COUNT(*) FROM UserPermission WHERE UserInfo_UserName = ?",
new OleDbParameter("@UserName", userName));
return Convert.ToInt32(count);
}
// 添加用户
public async Task<bool> AddUserAsync(string userName, string password)
{
// 检查是否已存在
var exists = await _accessDataService.ExecuteScalarAsync(
"SELECT COUNT(*) FROM UserInfo WHERE UserName = ?",
new OleDbParameter("@UserName", userName));
if (Convert.ToInt32(exists) > 0)
return false;
// 插入用户
await _accessDataService.ExecuteAsync(
"INSERT INTO UserInfo (UserName, [Password]) VALUES (?, ?)",
new OleDbParameter("@UserName", userName),
new OleDbParameter("@Password", password));
return true;
}
// 删除用户(包含权限)
public async Task DeleteUserAsync(string userName)
{
await _accessDataService.ExecuteInTransactionAsync(async transaction =>
{
// 先删除权限
await transaction.ExecuteAsync(
"DELETE FROM UserPermission WHERE UserInfo_UserName = ?",
new OleDbParameter("@UserName", userName));
// 再删除用户
await transaction.ExecuteAsync(
"DELETE FROM UserInfo WHERE UserName = ?",
new OleDbParameter("@UserName", userName));
});
}
}
```
---
## 💡 核心优势总结
| 特性 | 直接使用 OleDb | 使用 AccessDataService |
|------|---------------|----------------------|
| 连接管理 | 手动创建、打开、释放 | ✅ 自动管理 |
| 结果映射 | 手动读取 DataReader | ✅ 自动泛型映射 |
| 事务管理 | 手动 BeginTransaction/Commit/Rollback | ✅ 自动管理 |
| 代码量 | 60+ 行 | ✅ 10 行左右 |
| 可维护性 | 低(大量重复代码) | ✅ 高 |
| 易用性 | 低(需要理解 ADO.NET) | ✅ 高 |
---
## 📖 参考文件
- **核心实现**: `Data.Access/AccessDataService.cs`
- **接口定义**: `Data.Access/IAccessDataService.cs`
- **配置选项**: `Data.Access/AccessDataServiceOptions.cs`
- **使用示例**: `Server/Views/AccessDatabaseWindowOleDb.xaml.cs`
---
**总结**:Data.Access 模块通过封装 OleDb 的复杂度,提供了简洁优雅的 API,让调用者可以用更少的代码完成相同的功能!
+115
View File
@@ -0,0 +1,115 @@
# Sharp.Data.Access
基于 EF Core + Access (Jet) 的数据库基础设施。遵循 “依赖大于配置” 的理念,提供易于集成的连接串构建、密码解密、数据库初始化以及依赖注入支持,让其他项目只专注于提供配置即可。
> **定位**:仅提供 Access 数据层能力(连接、仓储、上下文构建),不含任何业务逻辑。
## 特性
- ✅ **DI 集成** – 一行 `AddAccessDataContext<TContext>()` 注册,其他模块通过构造函数获取 `IDataContext`。
- ✅ **最小配置** – 只需要提供数据库文件或连接字符串即可完成集成。
- ✅ **密码解密** – 内置 `IAccessPasswordDecryptor`,可注入自定义实现来解密加密密码。
- ✅ **数据库初始化** – 自动创建目录、可选基于模板复制空库文件,避免运行期错误。
- ✅ **灵活扩展** – 支持通过委托访问 `IServiceProvider`,从任意配置中心或密钥管理系统读取参数。
## 快速开始
```csharp
using Microsoft.Extensions.DependencyInjection;
using Sharp.Data.Access.DependencyInjection;
using Sharp.Data.Context;
builder.Services.AddAccessDataContext<AppDbContext>(options =>
{
options.Database.DatabaseFile = "data/app.accdb";
options.Database.EncryptedPassword = configuration["Access:EncryptedPassword"];
// 配置 Data 模块能力
options.ConfigureDataContext = (sp, ctx) =>
{
ctx.UseAudit()
.UseSoftDelete()
.UseCurrentUserProvider(() => sp.GetService<ICurrentUser>()?.Id);
};
});
```
随后只需要注入 `IDataContext` 或 `IAccessDataContextFactory<AppDbContext>` 即可使用:
```csharp
public class UserService
{
private readonly IDataContext _db;
public UserService(IDataContext db) => _db = db;
public Task<List<User>> GetUsersAsync() => _db.Repository<User>().GetAllAsync();
}
```
## 依赖注入示例(带模板/密码)
```csharp
var services = new ServiceCollection();
services.AddSingleton<ISecretProvider, SecretProvider>(); // 自定义的密钥服务
services.AddAccessDataContext<AppDbContext>(options =>
{
options.Database.DatabaseFile = "data/app.accdb";
options.Database.TemplateFile = "data/template.accdb";
options.Database.CopyTemplateWhenMissing = true;
options.Database.EncryptedPasswordFactory = sp =>
sp.GetRequiredService<ISecretProvider>().Get("AccessDbPassword");
options.ConfigureDataContext = (sp, ctx) =>
{
ctx.UseAudit()
.UseSoftDelete()
.UseCurrentUserProvider(() => sp.GetRequiredService<ICurrentUserAccessor>().Id);
};
});
await using var provider = services.BuildServiceProvider();
var factory = provider.GetRequiredService<IAccessDataContextFactory<AppDbContext>>();
await using var dataContext = factory.Create();
```
> 在实际业务项目中可以把 `IAccessDataContextFactory<TContext>` 注入到需要“短生命周期”数据库连接的服务中,或直接注入 `IDataContext`(作用域作用域内共享同一实例)。
## 处理加密密码
实现 `IAccessPasswordDecryptor` 并注册到 DI:
```csharp
builder.Services.AddSingleton<IAccessPasswordDecryptor, AesPasswordDecryptor>();
```
`AccessDatabaseOptions.EncryptedPassword` 会在构建连接字符串前自动调用该服务完成解密,也可以直接提供明文 `Password`。
## 模板与初始化
- `CopyTemplateWhenMissing = true`(默认)时,如果指定了 `TemplateFile`,将在首次运行时复制模板数据库。
- `ThrowIfMissing = true` 可在缺少数据库文件时立即抛出异常,便于早期发现部署问题。
- 所有路径都支持相对路径(基于 `AppContext.BaseDirectory`),也可以通过 `DataDirectoryFactory` 自定义。
## 手动使用(无依赖注入)
```csharp
var builder = new DataContextBuilder<AppDbContext>();
builder.UseAccessDatabase(options =>
{
options.DatabaseFile = "data/app.accdb";
options.Password = "secret";
});
var db = builder.Build();
```
## 可扩展点
- `IAccessPasswordDecryptor`:自定义密码加解密。
- `IAccessConnectionStringFactory`:自定义连接串构建逻辑。
- `IAccessDatabaseInitializer`:自定义数据库初始化策略(检查锁、备份等)。
- `AccessContextOptions.ConfigureDataContext`:复用 Data 模块的审计、软删除、自动迁移等能力。
通过这些基础设施,Access 支持可在任何项目中复用,同时保持代码简洁、低耦合。
@@ -0,0 +1,14 @@
namespace Sharp.Data.Access.Security;
/// <summary>
/// Access 密码解密接口,调用方可替换以实现自定义加解密策略。
/// </summary>
public interface IAccessPasswordDecryptor
{
/// <summary>
/// 解密 Access 数据库密码。
/// </summary>
/// <param name="encryptedPassword">加密后的密码。</param>
/// <returns>返回明文密码,如果无法解密则返回 null。</returns>
string? Decrypt(string? encryptedPassword);
}
@@ -0,0 +1,9 @@
namespace Sharp.Data.Access.Security;
/// <summary>
/// 默认的密码解密器,直接返回原始值。
/// </summary>
public sealed class PassthroughAccessPasswordDecryptor : IAccessPasswordDecryptor
{
public string? Decrypt(string? encryptedPassword) => encryptedPassword;
}
+12 -1
View File
@@ -294,9 +294,20 @@ public class HttpServer
var controller = _serviceProvider.GetService(controllerType);
if (controller != null)
return controller;
// 使用 ActivatorUtilities 创建实例(可以解析构造函数参数)
try
{
return ActivatorUtilities.CreateInstance(_serviceProvider, controllerType);
}
catch (Exception ex)
{
_logger?.LogError(ex, "Failed to create controller {ControllerType} with DI", controllerType.Name);
throw;
}
}
// 使用激活器创建
// 没有 DI 容器时,使用默认激活器创建(需要无参构造)
return Activator.CreateInstance(controllerType)!;
}
@@ -1,24 +0,0 @@
using Microsoft.Extensions.DependencyInjection;
using Plc.Abstractions;
using Plc.Services;
using Plc.Siemens;
using Plc.Siemens.Options;
namespace Plc.Siemens.DependencyInjection;
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddSiemensPlc(this IServiceCollection services, Action<SiemensPlcOptions> configureOptions)
{
var options = new SiemensPlcOptions();
configureOptions(options);
services.AddSingleton(options);
services.AddSingleton<IPlcDriver, SiemensDriver>();
services.AddSingleton<IPlcOptimizer, SiemensOptimizer>();
services.AddSingleton<IDataConverter, BigEndianConverter>();
services.AddSingleton<IPlcService, PlcMonitorService>();
return services;
}
}
@@ -1,25 +0,0 @@
namespace Plc.Siemens.Models;
public enum SiemensMemoryArea
{
Inputs = 0x81,
Outputs = 0x82,
Memory = 0x83,
DataBlock = 0x84,
Timer = 0x1D,
Counter = 0x1C
}
public class SiemensAddress
{
public SiemensMemoryArea Area { get; set; }
public int DbNumber { get; set; } // Only for DataBlock
public int ByteOffset { get; set; }
public int BitOffset { get; set; }
// Helper to sort/group
public override string ToString()
{
return $"{Area}:{DbNumber}:{ByteOffset}.{BitOffset}";
}
}
@@ -1,23 +0,0 @@
using S7.Net;
namespace Plc.Siemens.Options;
public class SiemensPlcOptions
{
public string IpAddress { get; set; } = "127.0.0.1";
public int Port { get; set; } = 102;
public short Rack { get; set; } = 0;
public short Slot { get; set; } = 1;
/// <summary>
/// S7-1200/1500 通常支持 960,S7-300 通常支持 240。
/// 默认保守值设为 240。
/// </summary>
public int PduSize { get; set; } = 240;
/// <summary>
/// CPU 类型 (例如: S71200, S71500, S7300, S7400, S7200, S7200Smart).
/// 默认为 S71200。
/// </summary>
public CpuType CpuType { get; set; } = CpuType.S71200;
}
-18
View File
@@ -1,18 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Plc\Plc.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
<PackageReference Include="S7netplus" Version="0.20.0" />
</ItemGroup>
</Project>
-123
View File
@@ -1,123 +0,0 @@
using Common.Results;
using Plc.Abstractions;
using Plc.Siemens.Models;
using Plc.Siemens.Options;
using Plc.Siemens.Utils;
using S7.Net;
namespace Plc.Siemens;
public class SiemensDriver : IPlcDriver
{
private readonly SiemensPlcOptions _options;
private S7.Net.Plc? _plc;
private readonly object _lock = new();
public SiemensDriver(SiemensPlcOptions options)
{
_options = options;
}
public bool IsConnected => _plc?.IsConnected ?? false;
public int MaxPduSize => _options.PduSize;
public async Task<Result> ConnectAsync(CancellationToken cancellationToken = default)
{
try
{
// Close existing connection if any
if (_plc != null)
{
_plc.Close();
_plc = null;
}
// Create new instance with latest options
_plc = new S7.Net.Plc(_options.CpuType, _options.IpAddress, _options.Port, _options.Rack, _options.Slot);
await _plc.OpenAsync(cancellationToken);
if (_plc.IsConnected)
return Result.Success();
return Result.Failure("Failed to connect to PLC (IsConnected is false).");
}
catch (Exception ex)
{
return Result.Failure(ex.Message);
}
}
public async Task<Result> DisconnectAsync()
{
try
{
_plc?.Close();
return await Task.FromResult(Result.Success());
}
catch (Exception ex)
{
return Result.Failure(ex.Message);
}
}
public async Task<Result<byte[]>> ReadBytesAsync(string address, ushort length, CancellationToken cancellationToken = default)
{
if (!IsConnected || _plc == null)
return Result.Failure<byte[]>("PLC 未连接。");
try
{
var siemensAddr = SiemensAddressParser.Parse(address);
var dataType = MapToS7DataType(siemensAddr.Area);
var bytes = await _plc.ReadBytesAsync(dataType, siemensAddr.DbNumber, siemensAddr.ByteOffset, length, cancellationToken);
return Result.Success(bytes);
}
catch (Exception ex)
{
return Result.Failure<byte[]>($"Read failed: {ex.Message}");
}
}
public async Task<Result> WriteBytesAsync(string address, byte[] data, CancellationToken cancellationToken = default)
{
if (!IsConnected || _plc == null)
return Result.Failure("PLC 未连接。");
try
{
var siemensAddr = SiemensAddressParser.Parse(address);
var dataType = MapToS7DataType(siemensAddr.Area);
await _plc.WriteBytesAsync(dataType, siemensAddr.DbNumber, siemensAddr.ByteOffset, data, cancellationToken);
return Result.Success();
}
catch (Exception ex)
{
return Result.Failure($"Write failed: {ex.Message}");
}
}
private DataType MapToS7DataType(SiemensMemoryArea area)
{
return area switch
{
SiemensMemoryArea.DataBlock => DataType.DataBlock,
SiemensMemoryArea.Memory => DataType.Memory,
SiemensMemoryArea.Inputs => DataType.Input,
SiemensMemoryArea.Outputs => DataType.Output,
SiemensMemoryArea.Timer => DataType.Timer,
SiemensMemoryArea.Counter => DataType.Counter,
_ => throw new ArgumentOutOfRangeException(nameof(area), area, null)
};
}
public void Dispose()
{
_plc?.Close();
}
}
-107
View File
@@ -1,107 +0,0 @@
using Plc.Abstractions;
using Plc.Models;
using Plc.Siemens.Models;
using Plc.Siemens.Utils;
using Plc.Utils;
namespace Plc.Siemens;
public class SiemensOptimizer : IPlcOptimizer
{
// 允许变量之间合并为一次读取的最大间隔字节数
private const int MaxGapBytes = 20;
public List<PlcReadBatch> Optimize(IEnumerable<PlcTag> tags, int maxPduSize)
{
var batches = new List<PlcReadBatch>();
// 1. 准备标签(解析地址)
var validTags = new List<PlcTag>();
foreach (var tag in tags)
{
if (tag.ParsedAddress == null)
{
try
{
tag.ParsedAddress = SiemensAddressParser.Parse(tag.Address);
}
catch
{
// 记录错误?暂时跳过。
continue;
}
}
validTags.Add(tag);
}
// 2. 按区域和 DB 号分组(物理位置)
var groups = validTags.GroupBy(t =>
{
var addr = (SiemensAddress)t.ParsedAddress!;
return new { addr.Area, addr.DbNumber };
});
foreach (var group in groups)
{
// 3. 按字节偏移量排序
var sortedTags = group.OrderBy(t => ((SiemensAddress)t.ParsedAddress!).ByteOffset).ToList();
PlcReadBatch? currentBatch = null;
foreach (var tag in sortedTags)
{
var addr = (SiemensAddress)tag.ParsedAddress!;
int typeSize = TypeHelper.GetSize(tag.DataType);
int byteLength = typeSize * tag.Length; // 此标签的总字节数
// 如果是 bool,它可能在一个字节内,但我们在检查边界时假设它占用该字节
if (currentBatch == null)
{
currentBatch = CreateBatch(tag, addr, byteLength);
batches.Add(currentBatch);
continue;
}
var batchStartAddr = (SiemensAddress)currentBatch.ParsedStartAddress!;
int currentBatchEnd = batchStartAddr.ByteOffset + currentBatch.TotalLength;
// 计算间隔 (Gap)
int gap = addr.ByteOffset - currentBatchEnd;
// 计算潜在的新长度
// 新标签地址 + 其长度 - 批次起始位置
int newTotalLength = (addr.ByteOffset + byteLength) - batchStartAddr.ByteOffset;
// 合并条件:
// 1. 标签在当前结束之后(或位置相同)开始(因为已排序,所以是肯定的)
// 2. 间隔足够小
// 3. 总大小适合 PDU
if (gap >= 0 && gap <= MaxGapBytes && newTotalLength <= maxPduSize)
{
currentBatch.Tags.Add(tag);
currentBatch.TotalLength = newTotalLength;
}
else
{
// 开启新批次
currentBatch = CreateBatch(tag, addr, byteLength);
batches.Add(currentBatch);
}
}
}
return batches;
}
private PlcReadBatch CreateBatch(PlcTag tag, SiemensAddress addr, int byteLength)
{
return new PlcReadBatch
{
StartAddress = tag.Address,
ParsedStartAddress = addr,
TotalLength = byteLength,
Tags = new List<PlcTag> { tag }
};
}
}
@@ -1,84 +0,0 @@
using Plc.Siemens.Models;
using System.Text.RegularExpressions;
namespace Plc.Siemens.Utils;
public static class SiemensAddressParser
{
// DB匹配: DB1.DBB0, DB1.DBW2, DB1.DBD4, DB1.DBX0.0
// Group 1: DbNumber
// Group 2: Type (B,W,D,X) - Ignored
// Group 3: ByteOffset
// Group 5: BitOffset (Optional)
private static readonly Regex DbRegex = new(@"^DB(\d+)\.DB[B|W|D|X]?(\d+)(\.(\d+))?$", RegexOptions.IgnoreCase);
// M匹配: M100, MB100, MW100, MD100, MX100.0, M100.1
// Group 1: ByteOffset
// Group 3: BitOffset (Optional)
private static readonly Regex MRegex = new(@"^M[B|W|D|X]?(\d+)(\.(\d+))?$", RegexOptions.IgnoreCase);
// I匹配: I0.0, IB0, IW0, ID0
private static readonly Regex IRegex = new(@"^I[B|W|D|X]?(\d+)(\.(\d+))?$", RegexOptions.IgnoreCase);
// Q匹配: Q0.0, QB0, QW0, QD0
private static readonly Regex QRegex = new(@"^Q[B|W|D|X]?(\d+)(\.(\d+))?$", RegexOptions.IgnoreCase);
public static SiemensAddress Parse(string address)
{
address = address.Trim().ToUpper();
// 1. Try DB
var dbMatch = DbRegex.Match(address);
if (dbMatch.Success)
{
return new SiemensAddress
{
Area = SiemensMemoryArea.DataBlock,
DbNumber = int.Parse(dbMatch.Groups[1].Value),
ByteOffset = int.Parse(dbMatch.Groups[2].Value),
BitOffset = dbMatch.Groups[4].Success ? int.Parse(dbMatch.Groups[4].Value) : 0
};
}
// 2. Try M (Memory/Flags)
var mMatch = MRegex.Match(address);
if (mMatch.Success)
{
return new SiemensAddress
{
Area = SiemensMemoryArea.Memory,
DbNumber = 0,
ByteOffset = int.Parse(mMatch.Groups[1].Value),
BitOffset = mMatch.Groups[3].Success ? int.Parse(mMatch.Groups[3].Value) : 0
};
}
// 3. Try I (Inputs)
var iMatch = IRegex.Match(address);
if (iMatch.Success)
{
return new SiemensAddress
{
Area = SiemensMemoryArea.Inputs,
DbNumber = 0,
ByteOffset = int.Parse(iMatch.Groups[1].Value),
BitOffset = iMatch.Groups[3].Success ? int.Parse(iMatch.Groups[3].Value) : 0
};
}
// 4. Try Q (Outputs)
var qMatch = QRegex.Match(address);
if (qMatch.Success)
{
return new SiemensAddress
{
Area = SiemensMemoryArea.Outputs,
DbNumber = 0,
ByteOffset = int.Parse(qMatch.Groups[1].Value),
BitOffset = qMatch.Groups[3].Success ? int.Parse(qMatch.Groups[3].Value) : 0
};
}
throw new ArgumentException($"不支持或无效的地址格式: {address}");
}
}
@@ -1,10 +0,0 @@
namespace Plc.Abstractions;
public interface IDataConverter
{
// 将原始字节(从指定偏移量开始)转换为目标类型
object? Parse(byte[] buffer, int offset, Type type, int length);
// 将值转换为字节
byte[] ToBytes(object value, Type type, int length);
}
-45
View File
@@ -1,45 +0,0 @@
using Common.Results;
namespace Plc.Abstractions;
/// <summary>
/// 定义 PLC 驱动的底层契约。
/// 此接口的实现处理具体的协议细节(例如:S7, Modbus)。
/// </summary>
public interface IPlcDriver : IDisposable
{
/// <summary>
/// 获取一个值,指示驱动程序当前是否已连接到 PLC。
/// </summary>
bool IsConnected { get; }
/// <summary>
/// 获取连接支持的最大 PDU(协议数据单元)大小。
/// 优化器使用此值来确定最大批量大小。
/// </summary>
int MaxPduSize { get; }
/// <summary>
/// 建立与 PLC 的连接。
/// </summary>
Task<Result> ConnectAsync(CancellationToken cancellationToken = default);
/// <summary>
/// 关闭与 PLC 的连接。
/// </summary>
Task<Result> DisconnectAsync();
/// <summary>
/// 从指定地址读取原始字节序列。
/// </summary>
/// <param name="address">起始地址字符串(例如 "DB1.0")。</param>
/// <param name="length">要读取的字节数。</param>
Task<Result<byte[]>> ReadBytesAsync(string address, ushort length, CancellationToken cancellationToken = default);
/// <summary>
/// 将原始字节序列写入指定地址。
/// </summary>
/// <param name="address">起始地址字符串。</param>
/// <param name="data">要写入的字节。</param>
Task<Result> WriteBytesAsync(string address, byte[] data, CancellationToken cancellationToken = default);
}
@@ -1,43 +0,0 @@
using Plc.Models;
namespace Plc.Abstractions;
/// <summary>
/// 负责分析请求的标签列表,并将它们分组为优化的批量请求。
/// </summary>
public interface IPlcOptimizer
{
/// <summary>
/// 基于地址位置和 PDU 大小,将单个标签分组为高效的读取批次。
/// </summary>
/// <param name="tags">活动订阅列表。</param>
/// <param name="maxPduSize">单次请求中可读取的最大字节数。</param>
/// <returns>批量请求列表。</returns>
List<PlcReadBatch> Optimize(IEnumerable<PlcTag> tags, int maxPduSize);
}
/// <summary>
/// 表示覆盖多个标签的单个优化读取操作。
/// </summary>
public class PlcReadBatch
{
/// <summary>
/// 此批量读取的起始地址。
/// </summary>
public string StartAddress { get; set; } = string.Empty;
/// <summary>
/// 要读取的总字节数。
/// </summary>
public int TotalLength { get; set; }
/// <summary>
/// 包含在此批量范围内的标签列表。
/// </summary>
public List<PlcTag> Tags { get; set; } = new();
/// <summary>
/// 驱动程序特定的起始地址上下文(避免重新解析)。
/// </summary>
public object? ParsedStartAddress { get; set; }
}
-41
View File
@@ -1,41 +0,0 @@
using Common.Results;
using Plc.Models;
namespace Plc.Abstractions;
/// <summary>
/// 用于与 PLC 交互的高层服务。
/// 处理连接管理、批量读取和数据转换。
/// </summary>
public interface IPlcService
{
/// <summary>
/// 启动后台监控循环。
/// </summary>
Task StartAsync(CancellationToken cancellationToken = default);
/// <summary>
/// 停止后台监控循环。
/// </summary>
Task StopAsync();
/// <summary>
/// 对单个地址执行立即、临时的读取。
/// </summary>
Task<Result<T>> ReadAsync<T>(string address);
/// <summary>
/// 对单个地址执行立即、临时的写入。
/// </summary>
Task<Result> WriteAsync<T>(string address, T value);
/// <summary>
/// 订阅一个标签到监控循环。
/// </summary>
void Subscribe(PlcTag tag);
/// <summary>
/// 从监控循环取消订阅标签。
/// </summary>
void Unsubscribe(string tagId);
}
@@ -1,14 +0,0 @@
namespace Plc.Attributes;
[AttributeUsage(AttributeTargets.Property)]
public class PlcAddressAttribute : Attribute
{
public string Address { get; }
public int Length { get; }
public PlcAddressAttribute(string address, int length = 1)
{
Address = address;
Length = length;
}
}
-9
View File
@@ -1,9 +0,0 @@
namespace Plc.Models;
public enum PlcBrand
{
Siemens,
Omron, // 预留
Mitsubishi, // 预留
ModbusTcp // 预留
}
-44
View File
@@ -1,44 +0,0 @@
namespace Plc.Models;
/// <summary>
/// 表示用户想要监控或访问的 PLC 中的特定数据点。
/// </summary>
public class PlcTag
{
/// <summary>
/// 此标签订阅的唯一标识符。
/// </summary>
public string Id { get; set; } = Guid.NewGuid().ToString();
/// <summary>
/// PLC 地址字符串(例如 "DB1.DBW0", "M100.1")。
/// </summary>
public required string Address { get; set; }
/// <summary>
/// 此数据表示的 .NET 类型(例如 typeof(int), typeof(float))。
/// </summary>
public required Type DataType { get; set; }
/// <summary>
/// 数据长度(主要用于字符串或数组)。默认为 1。
/// </summary>
public int Length { get; set; } = 1;
/// <summary>
/// 此标签的最后已知值。
/// </summary>
public object? Value { get; set; }
/// <summary>
/// 当值更改(或刷新)时调用的操作。
/// 回调接收 Tag 实例和新值。
/// </summary>
public Action<PlcTag, object?>? OnChanged { get; set; }
/// <summary>
/// 内部使用:由驱动程序/优化器用于存储解析后的地址对象。
/// 这避免了在每个周期重新解析地址字符串。
/// </summary>
public object? ParsedAddress { get; set; }
}
-13
View File
@@ -1,13 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Common\Common.csproj" />
</ItemGroup>
</Project>
-106
View File
@@ -1,106 +0,0 @@
using System.ComponentModel;
using System.Reflection;
using Plc.Abstractions;
using Plc.Attributes;
using Plc.Models;
namespace Plc.Proxy;
public class PlcInterfaceProxy : DispatchProxy, INotifyPropertyChanged
{
private IPlcService _plcService = null!;
private readonly Dictionary<string, PlcTag> _tags = new();
// 关键修改:使用私有字段手动管理委托链,避免递归调用
private PropertyChangedEventHandler? _propertyChangedInternal;
// 声明为 virtual event 只是为了骗过编译器的 TypeLoadException,
// 但我们在 Invoke 里不操作它,而是操作 _propertyChangedInternal。
// 实际上,对于 DispatchProxy 来说,我们并不需要这个 event 真的工作,
// 只需要它存在以满足接口签名,并允许被重写。
public virtual event PropertyChangedEventHandler? PropertyChanged
{
add => _propertyChangedInternal += value;
remove => _propertyChangedInternal -= value;
}
protected override object? Invoke(MethodInfo? targetMethod, object?[]? args)
{
if (targetMethod == null) return null;
// 1. Handle Property Getters (get_PropName)
if (targetMethod.Name.StartsWith("get_"))
{
var propName = targetMethod.Name.Substring(4);
if (_tags.TryGetValue(propName, out var tag))
{
return tag.Value; // Return cached value
}
}
// 2. Handle Property Setters (set_PropName)
if (targetMethod.Name.StartsWith("set_"))
{
var propName = targetMethod.Name.Substring(4);
if (_tags.TryGetValue(propName, out var tag) && args != null && args.Length > 0)
{
var newValue = args[0];
_plcService.WriteAsync(tag.Address, newValue).GetAwaiter().GetResult();
return null;
}
}
// 3. Handle INotifyPropertyChanged methods (add_PropertyChanged, remove_PropertyChanged)
// 这里是关键:我们拦截对 add_PropertyChanged 的调用,
// 将其重定向到我们的私有字段 _propertyChangedInternal,
// 从而避免调用 this.PropertyChanged += ... 导致的递归。
if (targetMethod.Name == "add_PropertyChanged" && args != null && args.Length == 1)
{
var handler = (PropertyChangedEventHandler)args[0]!;
_propertyChangedInternal += handler;
return null;
}
if (targetMethod.Name == "remove_PropertyChanged" && args != null && args.Length == 1)
{
var handler = (PropertyChangedEventHandler)args[0]!;
_propertyChangedInternal -= handler;
return null;
}
return null;
}
public static T Create<T>(IPlcService plcService) where T : class, INotifyPropertyChanged
{
object proxy = Create<T, PlcInterfaceProxy>();
((PlcInterfaceProxy)proxy).Initialize(typeof(T), plcService);
return (T)proxy;
}
private void Initialize(Type interfaceType, IPlcService plcService)
{
_plcService = plcService;
foreach (var prop in interfaceType.GetProperties())
{
var attr = prop.GetCustomAttribute<PlcAddressAttribute>();
if (attr != null)
{
var tag = new PlcTag
{
Address = attr.Address,
DataType = prop.PropertyType,
Length = attr.Length,
OnChanged = (t, v) =>
{
// 触发时,调用私有字段
_propertyChangedInternal?.Invoke(this, new PropertyChangedEventArgs(prop.Name));
}
};
_tags[prop.Name] = tag;
_plcService.Subscribe(tag);
}
}
}
}
@@ -1,102 +0,0 @@
using System.Buffers.Binary;
using Plc.Abstractions;
namespace Plc.Services;
public class BigEndianConverter : IDataConverter
{
public object? Parse(byte[] buffer, int offset, Type type, int length)
{
// 为简洁起见省略了边界检查,但在生产环境中至关重要
if (type == typeof(bool))
return buffer[offset] > 0; // 简单的检查,真实的位逻辑需要位索引
if (type == typeof(byte))
return buffer[offset];
if (type == typeof(short))
return BinaryPrimitives.ReadInt16BigEndian(buffer.AsSpan(offset));
if (type == typeof(ushort))
return BinaryPrimitives.ReadUInt16BigEndian(buffer.AsSpan(offset));
if (type == typeof(int))
return BinaryPrimitives.ReadInt32BigEndian(buffer.AsSpan(offset));
if (type == typeof(uint))
return BinaryPrimitives.ReadUInt32BigEndian(buffer.AsSpan(offset));
if (type == typeof(float))
return BitConverter.Int32BitsToSingle(BinaryPrimitives.ReadInt32BigEndian(buffer.AsSpan(offset)));
if (type == typeof(double))
return BitConverter.Int64BitsToDouble(BinaryPrimitives.ReadInt64BigEndian(buffer.AsSpan(offset)));
// 简单处理字符串 (假设 ASCII/S7 String)
if (type == typeof(string))
{
// S7 String 前两个字节是 MaxLen 和 ActualLen,后面是字符
// 这里假设是纯字符数组或者需要特定处理
// 简单起见,按 ASCII 读取指定长度
return System.Text.Encoding.ASCII.GetString(buffer, offset, length);
}
return null;
}
public byte[] ToBytes(object value, Type type, int length)
{
if (type == typeof(bool))
return new[] { (bool)value ? (byte)1 : (byte)0 };
if (type == typeof(byte))
return new[] { (byte)value };
if (type == typeof(short))
{
var bytes = new byte[2];
BinaryPrimitives.WriteInt16BigEndian(bytes, (short)value);
return bytes;
}
if (type == typeof(ushort))
{
var bytes = new byte[2];
BinaryPrimitives.WriteUInt16BigEndian(bytes, (ushort)value);
return bytes;
}
if (type == typeof(int))
{
var bytes = new byte[4];
BinaryPrimitives.WriteInt32BigEndian(bytes, (int)value);
return bytes;
}
if (type == typeof(uint))
{
var bytes = new byte[4];
BinaryPrimitives.WriteUInt32BigEndian(bytes, (uint)value);
return bytes;
}
if (type == typeof(float))
{
var bytes = new byte[4];
var intVal = BitConverter.SingleToInt32Bits((float)value);
BinaryPrimitives.WriteInt32BigEndian(bytes, intVal);
return bytes;
}
if (type == typeof(double))
{
var bytes = new byte[8];
var longVal = BitConverter.DoubleToInt64Bits((double)value);
BinaryPrimitives.WriteInt64BigEndian(bytes, longVal);
return bytes;
}
return Array.Empty<byte>();
}
}
-169
View File
@@ -1,169 +0,0 @@
using Common.Results;
using Plc.Abstractions;
using Plc.Models;
using Plc.Utils;
using System.Collections.Concurrent;
namespace Plc.Services;
public class PlcMonitorService : IPlcService, IDisposable
{
private readonly IPlcDriver _driver;
private readonly IPlcOptimizer _optimizer;
private readonly IDataConverter _converter;
private readonly ConcurrentDictionary<string, PlcTag> _subscriptions = new();
private Task? _monitorTask;
private CancellationTokenSource? _cts;
private readonly TimeSpan _refreshRate = TimeSpan.FromMilliseconds(200);
public PlcMonitorService(IPlcDriver driver, IPlcOptimizer optimizer, IDataConverter converter)
{
_driver = driver;
_optimizer = optimizer;
_converter = converter;
}
public async Task StartAsync(CancellationToken cancellationToken = default)
{
if (_monitorTask != null) return;
await _driver.ConnectAsync(cancellationToken);
_cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
_monitorTask = Task.Run(() => MonitorLoop(_cts.Token), _cts.Token);
}
public async Task StopAsync()
{
_cts?.Cancel();
if (_monitorTask != null)
{
try { await _monitorTask; } catch (OperationCanceledException) { }
}
await _driver.DisconnectAsync();
}
public void Subscribe(PlcTag tag)
{
_subscriptions.TryAdd(tag.Id, tag);
}
public void Unsubscribe(string tagId)
{
_subscriptions.TryRemove(tagId, out _);
}
public async Task<Result<T>> ReadAsync<T>(string address)
{
// 临时读取(未优化)
int size = TypeHelper.GetSize(typeof(T));
var result = await _driver.ReadBytesAsync(address, (ushort)size);
if (result.IsFailure)
return Result.Failure<T>(result.ErrorMessage ?? result.Exception?.Message ?? "Read failed");
if (result.Value is null)
return Result.Failure<T>("Read returned null data");
var val = _converter.Parse(result.Value, 0, typeof(T), 1);
if (val is null)
return Result.Failure<T>("Conversion failed");
return Result.Success((T)val);
}
public async Task<Result> WriteAsync<T>(string address, T value)
{
if (value == null)
return Result.Failure("Cannot write null value");
int size = TypeHelper.GetSize(typeof(T));
var bytes = _converter.ToBytes(value, typeof(T), size);
if (bytes.Length == 0)
return Result.Failure("Conversion to bytes returned empty array. Type might not be supported.");
return await _driver.WriteBytesAsync(address, bytes);
}
private async Task MonitorLoop(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
try
{
if (!_driver.IsConnected)
{
await _driver.ConnectAsync(token);
}
var currentTags = _subscriptions.Values.ToList();
if (currentTags.Count == 0)
{
await Task.Delay(1000, token);
continue;
}
// 优化:计算分桶
var batches = _optimizer.Optimize(currentTags, _driver.MaxPduSize);
// 执行读取
foreach (var batch in batches)
{
if (token.IsCancellationRequested) break;
var readResult = await _driver.ReadBytesAsync(batch.StartAddress, (ushort)batch.TotalLength, token);
if (readResult.IsSuccess && readResult.Value != null)
{
DistributeValues(batch, readResult.Value);
}
else
{
// 记录错误
}
}
}
catch (Exception)
{
// 记录异常
}
await Task.Delay(_refreshRate, token);
}
}
private void DistributeValues(PlcReadBatch batch, byte[] data)
{
dynamic batchStart = batch.ParsedStartAddress!;
int batchStartByte = batchStart.ByteOffset;
foreach (var tag in batch.Tags)
{
dynamic tagAddr = tag.ParsedAddress!;
int tagStartByte = tagAddr.ByteOffset;
int relativeOffset = tagStartByte - batchStartByte;
// 检查边界
int typeSize = TypeHelper.GetSize(tag.DataType);
if (relativeOffset + typeSize <= data.Length)
{
var newVal = _converter.Parse(data, relativeOffset, tag.DataType, tag.Length);
// 变更检测
if (!Equals(tag.Value, newVal))
{
tag.Value = newVal;
tag.OnChanged?.Invoke(tag, newVal);
}
}
}
}
public void Dispose()
{
_cts?.Cancel();
_driver.Dispose();
}
}
-15
View File
@@ -1,15 +0,0 @@
namespace Plc.Utils;
public static class TypeHelper
{
public static int GetSize(Type type)
{
if (type == typeof(bool)) return 1; // S7 usually treats bool as bit, but allocation often aligns to bytes or we need spec handling
if (type == typeof(byte)) return 1;
if (type == typeof(short) || type == typeof(ushort)) return 2;
if (type == typeof(int) || type == typeof(uint) || type == typeof(float)) return 4;
if (type == typeof(long) || type == typeof(ulong) || type == typeof(double)) return 8;
return 1; // Default or throw
}
}
+66 -3
View File
@@ -1,9 +1,12 @@
using System.Windows;
using System.IO;
using System.Windows;
using Microsoft.Extensions.Logging;
using Config;
using Sharp.Log;
using Sharp.Data;
using Sharp.Data.Context;
using Sharp.Data.Access;
using Sharp.Data.Access.Options;
using Server.Config;
using Server.Services;
using Server.Data;
@@ -20,6 +23,7 @@ namespace Server
public ILoggerFactory? LoggerFactory { get; private set; }
public ServerService? ServerService { get; private set; }
public IDataContext? DataContext { get; private set; }
public IAccessDataService? AccessDataService { get; private set; }
protected override void OnStartup(StartupEventArgs e)
{
@@ -36,7 +40,10 @@ namespace Server
// 3. 初始化数据库
InitializeDatabase();
// 4. 初始化服务器服务
// 4. 初始化 Access 数据服务
InitializeAccessDataService();
// 5. 初始化服务器服务
InitializeServerService();
var logger = LoggerFactory?.CreateLogger<App>();
@@ -74,7 +81,7 @@ namespace Server
options.Enrich.Application ??= "SharpServer";
});
}
private void InitializeDatabase()
{
if (LoggerFactory == null)
@@ -104,6 +111,62 @@ namespace Server
throw;
}
}
private void InitializeAccessDataService()
{
if (LoggerFactory == null)
{
throw new InvalidOperationException("日志系统未初始化");
}
var logger = LoggerFactory.CreateLogger<App>();
logger.LogInformation("正在初始化 Access 数据服务...");
try
{
// 诊断:输出当前目录信息
logger.LogInformation("AppContext.BaseDirectory: {BaseDirectory}", AppContext.BaseDirectory);
logger.LogInformation("Environment.CurrentDirectory: {CurrentDirectory}", Environment.CurrentDirectory);
logger.LogInformation("数据库文件在输出目录: {DbPath}",
Path.Combine(AppContext.BaseDirectory, "dbDwDmsSetup.accdb"));
logger.LogInformation("数据库文件是否存在: {Exists}",
File.Exists(Path.Combine(AppContext.BaseDirectory, "dbDwDmsSetup.accdb")));
// 配置 Access 数据库选项
var options = new AccessDatabaseOptions
{
DatabaseFile = "dbDwDmsSetup.accdb",
// 使用 ACE OLE DB 16.0(功能最完整,支持所有连接字符串参数)
// 注意:12.0 不支持 Connect Timeout 参数,会导致 0x80040E21 错误
ProviderName = AccessProviderNames.AceOleDb16,
// 如果数据库有密码,可以在这里设置:
// Password = "your_password",
// 或者使用加密密码(需要先注册自定义解密器):
// EncryptedPassword = "encrypted_password",
// 可选:调整稳健性配置
// MaxRetryCount = 3, // 最大重试次数
// RetryDelayMilliseconds = 100, // 重试延迟
// ConnectionTimeout = 15, // 连接超时(秒)
};
// 创建 Access 数据服务
AccessDataService = new AccessDataService(
options,
logger: LoggerFactory.CreateLogger<AccessDataService>());
logger.LogInformation("Access 数据服务初始化成功");
}
catch (Exception ex)
{
logger.LogError(ex, "Access 数据服务初始化失败");
// 不抛出异常,因为 Access 数据库是可选的
logger.LogWarning("Access 数据服务初始化失败,但应用程序将继续运行");
}
}
private void InitializeServerService()
{
+9 -13
View File
@@ -23,13 +23,13 @@ namespace Server.Controllers
public async Task GetAll(HttpContext context)
{
var items = _dataStore.Values.ToList();
var result = ResultExtensions.HttpSuccess(new
var data = new
{
Count = items.Count,
Items = items
});
};
await WriteSuccessResponseAsync(context, result);
await WriteSuccessResponseAsync(context, data);
}
/// <summary>
@@ -49,8 +49,7 @@ namespace Server.Controllers
if (_dataStore.TryGetValue(id, out var item))
{
var result = ResultExtensions.HttpSuccess(item);
await WriteSuccessResponseAsync(context, result);
await WriteSuccessResponseAsync(context, item);
}
else
{
@@ -87,8 +86,7 @@ namespace Server.Controllers
_dataStore[id] = item;
var result = ResultExtensions.HttpSuccess(item, "Data created successfully");
await WriteSuccessResponseAsync(context, result);
await WriteSuccessResponseAsync(context, item, "Data created successfully");
}
/// <summary>
@@ -129,8 +127,7 @@ namespace Server.Controllers
item.UpdatedAt = DateTime.UtcNow;
}
var result = ResultExtensions.HttpSuccess(item, "Data updated successfully");
await WriteSuccessResponseAsync(context, result);
await WriteSuccessResponseAsync(context, item, "Data updated successfully");
}
/// <summary>
@@ -150,8 +147,7 @@ namespace Server.Controllers
if (_dataStore.TryRemove(id, out var item))
{
var result = ResultExtensions.HttpSuccess(item, "Data deleted successfully");
await WriteSuccessResponseAsync(context, result);
await WriteSuccessResponseAsync(context, item, "Data deleted successfully");
}
else
{
@@ -169,8 +165,8 @@ namespace Server.Controllers
var count = _dataStore.Count;
_dataStore.Clear();
var result = ResultExtensions.HttpSuccess(new { DeletedCount = count }, "All data cleared");
await WriteSuccessResponseAsync(context, result);
var data = new { DeletedCount = count };
await WriteSuccessResponseAsync(context, data, "All data cleared");
}
}
@@ -19,14 +19,14 @@ namespace Server.Controllers
[HttpGet]
public async Task Check(HttpContext context)
{
var result = ResultExtensions.HttpSuccess(new
var data = new
{
Status = "Healthy",
Timestamp = DateTime.UtcNow,
Uptime = DateTime.UtcNow - Process.GetCurrentProcess().StartTime
});
};
await WriteSuccessResponseAsync(context, result);
await WriteSuccessResponseAsync(context, data);
}
/// <summary>
@@ -37,7 +37,7 @@ namespace Server.Controllers
public async Task DetailedCheck(HttpContext context)
{
var process = Process.GetCurrentProcess();
var result = ResultExtensions.HttpSuccess(new
var data = new
{
Status = "Healthy",
Timestamp = DateTime.UtcNow,
@@ -60,9 +60,9 @@ namespace Server.Controllers
},
Threads = process.Threads.Count
}
});
};
await WriteSuccessResponseAsync(context, result);
await WriteSuccessResponseAsync(context, data);
}
}
}
@@ -18,7 +18,7 @@ namespace Server.Controllers
[HttpGet("/info")]
public async Task GetSystemInfo(HttpContext context)
{
var result = ResultExtensions.HttpSuccess(new
var data = new
{
MachineName = Environment.MachineName,
UserName = Environment.UserName,
@@ -30,9 +30,9 @@ namespace Server.Controllers
CurrentDirectory = Environment.CurrentDirectory,
TickCount = Environment.TickCount64,
Version = Environment.Version.ToString()
});
};
await WriteSuccessResponseAsync(context, result);
await WriteSuccessResponseAsync(context, data);
}
/// <summary>
@@ -59,7 +59,7 @@ namespace Server.Controllers
return;
}
var successResult = ResultExtensions.HttpSuccess(new { Name = name, Value = value });
var successResult = new { Name = name, Value = value };
await WriteSuccessResponseAsync(context, successResult);
}
@@ -73,7 +73,7 @@ namespace Server.Controllers
var now = DateTime.Now;
var utcNow = DateTime.UtcNow;
var result = ResultExtensions.HttpSuccess(new
var data = new
{
Local = new
{
@@ -89,9 +89,9 @@ namespace Server.Controllers
Time = utcNow.TimeOfDay,
UnixTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
}
});
};
await WriteSuccessResponseAsync(context, result);
await WriteSuccessResponseAsync(context, data);
}
}
}
@@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore;
namespace Server.Data.Access;
/// <summary>
/// Access 数据库上下文
/// </summary>
public class AccessDbContext : DbContext
{
public AccessDbContext(DbContextOptions<AccessDbContext> options) : base(options)
{
}
public DbSet<UserInfo> UserInfos => Set<UserInfo>();
public DbSet<UserPermission> UserPermissions => Set<UserPermission>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<UserInfo>(entity =>
{
entity.HasKey(e => e.UserName);
entity.Property(e => e.UserName).IsRequired().HasMaxLength(255);
entity.Property(e => e.Password).IsRequired().HasMaxLength(255);
});
modelBuilder.Entity<UserPermission>(entity =>
{
entity.HasKey(e => new { e.UserInfo_UserName, e.Permission_Code });
entity.Property(e => e.UserInfo_UserName).IsRequired().HasMaxLength(255);
entity.Property(e => e.Permission_Code).IsRequired().HasMaxLength(255);
entity.HasOne(e => e.UserInfo)
.WithMany(u => u.Permissions)
.HasForeignKey(e => e.UserInfo_UserName)
.OnDelete(DeleteBehavior.Cascade);
});
}
}
@@ -0,0 +1,40 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Server.Data.Access;
/// <summary>
/// UserInfo 表实体
/// </summary>
[Table("UserInfo")]
public class UserInfo
{
[Key]
[Required]
[MaxLength(255)]
public string UserName { get; set; } = string.Empty;
[Required]
[MaxLength(255)]
public string Password { get; set; } = string.Empty;
public ICollection<UserPermission> Permissions { get; set; } = new List<UserPermission>();
}
/// <summary>
/// UserPermission 表实体
/// </summary>
[Table("UserPermission")]
public class UserPermission
{
[Required]
[MaxLength(255)]
public string UserInfo_UserName { get; set; } = string.Empty;
[Required]
[MaxLength(255)]
public string Permission_Code { get; set; } = string.Empty;
[ForeignKey(nameof(UserInfo_UserName))]
public UserInfo? UserInfo { get; set; }
}
+438
View File
@@ -0,0 +1,438 @@
# Access 数据库 CRUD 完整示例
使用 **System.Data.OleDb** 对 Access 数据库进行增删改查操作。
## 📦 连接字符串
```csharp
var connectionString = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=dbDwDmsSetup.accdb";
using var connection = new OleDbConnection(connectionString);
connection.Open();
```
---
## 1️⃣ 查询(SELECT)
### 查询所有用户
```csharp
using var connection = new OleDbConnection(connectionString);
connection.Open();
var users = new List<UserInfo>();
using var command = new OleDbCommand("SELECT UserName, Password FROM UserInfo", connection);
using var reader = command.ExecuteReader();
while (reader.Read())
{
var user = new UserInfo
{
UserName = reader["UserName"]?.ToString() ?? "",
Password = reader["Password"]?.ToString() ?? ""
};
users.Add(user);
}
Console.WriteLine($"查询到 {users.Count} 个用户");
```
### 条件查询
```csharp
// 根据用户名查询
using var command = new OleDbCommand(
"SELECT UserName, Password FROM UserInfo WHERE UserName = ?", connection);
command.Parameters.AddWithValue("@UserName", "admin");
using var reader = command.ExecuteReader();
if (reader.Read())
{
var userName = reader["UserName"]?.ToString();
var password = reader["Password"]?.ToString();
Console.WriteLine($"找到用户: {userName}");
}
```
### 统计数量
```csharp
using var command = new OleDbCommand(
"SELECT COUNT(*) FROM UserPermission WHERE UserInfo_UserName = ?", connection);
command.Parameters.AddWithValue("@UserName", "admin");
var count = Convert.ToInt32(command.ExecuteScalar());
Console.WriteLine($"该用户有 {count} 个权限");
```
---
## 2️⃣ 添加(INSERT)
### 添加用户
```csharp
using var connection = new OleDbConnection(connectionString);
connection.Open();
// 1. 先检查用户是否已存在
using (var checkCmd = new OleDbCommand(
"SELECT COUNT(*) FROM UserInfo WHERE UserName = ?", connection))
{
checkCmd.Parameters.AddWithValue("@UserName", "newuser");
var exists = Convert.ToInt32(checkCmd.ExecuteScalar()) > 0;
if (exists)
{
Console.WriteLine("用户已存在!");
return;
}
}
// 2. 插入新用户
using (var insertCmd = new OleDbCommand(
"INSERT INTO UserInfo (UserName, Password) VALUES (?, ?)", connection))
{
insertCmd.Parameters.AddWithValue("@UserName", "newuser");
insertCmd.Parameters.AddWithValue("@Password", "password123");
var rowsAffected = insertCmd.ExecuteNonQuery();
Console.WriteLine($"成功添加 {rowsAffected} 个用户");
}
```
### 添加权限
```csharp
using var insertCmd = new OleDbCommand(
"INSERT INTO UserPermission (UserInfo_UserName, Permission_Code) VALUES (?, ?)", connection);
insertCmd.Parameters.AddWithValue("@UserName", "newuser");
insertCmd.Parameters.AddWithValue("@PermissionCode", "READ_DATA");
var rowsAffected = insertCmd.ExecuteNonQuery();
Console.WriteLine($"成功添加权限: {rowsAffected} 条");
```
---
## 3️⃣ 修改(UPDATE)
### 修改用户密码
```csharp
using var connection = new OleDbConnection(connectionString);
connection.Open();
using var updateCmd = new OleDbCommand(
"UPDATE UserInfo SET Password = ? WHERE UserName = ?", connection);
updateCmd.Parameters.AddWithValue("@Password", "newpassword");
updateCmd.Parameters.AddWithValue("@UserName", "admin");
var rowsAffected = updateCmd.ExecuteNonQuery();
if (rowsAffected > 0)
{
Console.WriteLine("密码修改成功!");
}
else
{
Console.WriteLine("用户不存在!");
}
```
### 批量更新
```csharp
// 将所有用户的密码更新为默认密码
using var updateCmd = new OleDbCommand(
"UPDATE UserInfo SET Password = ? WHERE Password IS NULL OR Password = ''", connection);
updateCmd.Parameters.AddWithValue("@Password", "default123");
var rowsAffected = updateCmd.ExecuteNonQuery();
Console.WriteLine($"更新了 {rowsAffected} 个用户的密码");
```
---
## 4️⃣ 删除(DELETE)
### 删除用户(含事务)
```csharp
using var connection = new OleDbConnection(connectionString);
connection.Open();
// 使用事务确保数据一致性
using var transaction = connection.BeginTransaction();
try
{
// 1. 先删除用户的所有权限
using (var deletePermCmd = new OleDbCommand(
"DELETE FROM UserPermission WHERE UserInfo_UserName = ?", connection, transaction))
{
deletePermCmd.Parameters.AddWithValue("@UserName", "testuser");
var permCount = deletePermCmd.ExecuteNonQuery();
Console.WriteLine($"删除了 {permCount} 个权限");
}
// 2. 再删除用户
using (var deleteUserCmd = new OleDbCommand(
"DELETE FROM UserInfo WHERE UserName = ?", connection, transaction))
{
deleteUserCmd.Parameters.AddWithValue("@UserName", "testuser");
var userCount = deleteUserCmd.ExecuteNonQuery();
if (userCount == 0)
{
transaction.Rollback();
Console.WriteLine("用户不存在!");
return;
}
Console.WriteLine($"删除了 {userCount} 个用户");
}
// 3. 提交事务
transaction.Commit();
Console.WriteLine("删除成功!");
}
catch (Exception ex)
{
// 回滚事务
transaction.Rollback();
Console.WriteLine($"删除失败: {ex.Message}");
}
```
### 删除权限
```csharp
using var deleteCmd = new OleDbCommand(
"DELETE FROM UserPermission WHERE UserInfo_UserName = ? AND Permission_Code = ?", connection);
deleteCmd.Parameters.AddWithValue("@UserName", "admin");
deleteCmd.Parameters.AddWithValue("@PermissionCode", "DELETE_DATA");
var rowsAffected = deleteCmd.ExecuteNonQuery();
Console.WriteLine($"删除了 {rowsAffected} 个权限");
```
### 批量删除
```csharp
// 删除所有没有权限的用户
using var deleteCmd = new OleDbCommand(@"
DELETE FROM UserInfo
WHERE UserName NOT IN (
SELECT DISTINCT UserInfo_UserName FROM UserPermission
)", connection);
var rowsAffected = deleteCmd.ExecuteNonQuery();
Console.WriteLine($"删除了 {rowsAffected} 个无权限用户");
```
---
## 🔥 高级操作
### 1. JOIN 查询
```csharp
// 查询用户及其所有权限
using var command = new OleDbCommand(@"
SELECT u.UserName, u.Password, p.Permission_Code
FROM UserInfo u
LEFT JOIN UserPermission p ON u.UserName = p.UserInfo_UserName
ORDER BY u.UserName", connection);
using var reader = command.ExecuteReader();
while (reader.Read())
{
var userName = reader["UserName"]?.ToString();
var permission = reader["Permission_Code"]?.ToString();
Console.WriteLine($"{userName}: {permission ?? "(无权限)"}");
}
```
### 2. 分组统计
```csharp
// 统计每个用户的权限数量
using var command = new OleDbCommand(@"
SELECT u.UserName, COUNT(p.Permission_Code) AS PermCount
FROM UserInfo u
LEFT JOIN UserPermission p ON u.UserName = p.UserInfo_UserName
GROUP BY u.UserName
ORDER BY PermCount DESC", connection);
using var reader = command.ExecuteReader();
while (reader.Read())
{
var userName = reader["UserName"]?.ToString();
var count = Convert.ToInt32(reader["PermCount"]);
Console.WriteLine($"{userName}: {count} 个权限");
}
```
### 3. 参数化批量插入
```csharp
using var connection = new OleDbConnection(connectionString);
connection.Open();
var users = new[]
{
("user1", "pass1"),
("user2", "pass2"),
("user3", "pass3")
};
using var transaction = connection.BeginTransaction();
try
{
using var insertCmd = new OleDbCommand(
"INSERT INTO UserInfo (UserName, Password) VALUES (?, ?)", connection, transaction);
foreach (var (userName, password) in users)
{
insertCmd.Parameters.Clear();
insertCmd.Parameters.AddWithValue("@UserName", userName);
insertCmd.Parameters.AddWithValue("@Password", password);
insertCmd.ExecuteNonQuery();
}
transaction.Commit();
Console.WriteLine($"成功添加 {users.Length} 个用户");
}
catch
{
transaction.Rollback();
throw;
}
```
### 4. 存在性检查
```csharp
// 检查用户是否存在
public bool UserExists(string userName)
{
using var connection = new OleDbConnection(connectionString);
connection.Open();
using var command = new OleDbCommand(
"SELECT COUNT(*) FROM UserInfo WHERE UserName = ?", connection);
command.Parameters.AddWithValue("@UserName", userName);
return Convert.ToInt32(command.ExecuteScalar()) > 0;
}
// 使用
if (UserExists("admin"))
{
Console.WriteLine("用户存在");
}
```
---
## 📝 最佳实践
### 1. 使用 using 语句
```csharp
// ✅ 推荐:自动释放资源
using var connection = new OleDbConnection(connectionString);
using var command = new OleDbCommand(sql, connection);
using var reader = command.ExecuteReader();
// ❌ 不推荐:需要手动释放
var connection = new OleDbConnection(connectionString);
try
{
// ...
}
finally
{
connection.Dispose();
}
```
### 2. 使用参数化查询(防止 SQL 注入)
```csharp
// ✅ 推荐:参数化查询
var sql = "SELECT * FROM UserInfo WHERE UserName = ?";
command.Parameters.AddWithValue("@UserName", userName);
// ❌ 危险:字符串拼接(SQL 注入风险)
var sql = $"SELECT * FROM UserInfo WHERE UserName = '{userName}'";
```
### 3. 使用事务保证数据一致性
```csharp
// ✅ 推荐:使用事务
using var transaction = connection.BeginTransaction();
try
{
// 多个操作
command1.ExecuteNonQuery();
command2.ExecuteNonQuery();
transaction.Commit();
}
catch
{
transaction.Rollback();
throw;
}
```
### 4. 异常处理
```csharp
try
{
using var connection = new OleDbConnection(connectionString);
connection.Open();
// 数据库操作
}
catch (OleDbException ex)
{
Console.WriteLine($"数据库错误: {ex.Message}");
// 记录日志
}
catch (Exception ex)
{
Console.WriteLine($"未知错误: {ex.Message}");
// 记录日志
}
```
---
## 🎯 完整示例项目
完整的 CRUD 操作已集成到 **Server 项目**中:
**文件位置**:
- UI: `Server/Views/AccessDatabaseWindowOleDb.xaml`
- 代码: `Server/Views/AccessDatabaseWindowOleDb.xaml.cs`
**功能列表**:
- ✅ 查询所有用户
- ✅ 添加新用户
- ✅ 修改用户密码
- ✅ 删除用户(含级联删除权限)
- ✅ 添加用户权限
- ✅ 删除用户权限
- ✅ 事务支持
- ✅ 异常处理
- ✅ 异步操作(不阻塞 UI)
**运行方式**:
1. 启动 Server 项目
2. 点击 "Access数据库" 按钮
3. 尽情测试增删改查功能!
数据库随意修改,不用担心!🎉
+186
View File
@@ -0,0 +1,186 @@
# Access 数据库访问方案对比
## 方案选择
### ✅ 方案 A:System.Data.OleDb(推荐)
**适用场景**:
- 简单的增删改查操作
- 性能要求高
- 需要稳定可靠的方案
- 不需要复杂的 LINQ 查询
**实现文件**:
- `Views/AccessDatabaseWindowOleDb.xaml`
- `Views/AccessDatabaseWindowOleDb.xaml.cs`
**示例代码**:
```csharp
// 1. 创建连接
var connectionString = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=dbDwDmsSetup.accdb";
using var connection = new OleDbConnection(connectionString);
connection.Open();
// 2. 查询数据
using var command = new OleDbCommand("SELECT UserName, Password FROM UserInfo", connection);
using var reader = command.ExecuteReader();
while (reader.Read())
{
var userName = reader["UserName"]?.ToString();
var password = reader["Password"]?.ToString();
}
// 3. 插入数据
using var insertCmd = new OleDbCommand(
"INSERT INTO UserInfo (UserName, Password) VALUES (?, ?)", connection);
insertCmd.Parameters.AddWithValue("@UserName", "newuser");
insertCmd.Parameters.AddWithValue("@Password", "password");
insertCmd.ExecuteNonQuery();
// 4. 更新数据
using var updateCmd = new OleDbCommand(
"UPDATE UserInfo SET Password = ? WHERE UserName = ?", connection);
updateCmd.Parameters.AddWithValue("@Password", "newpassword");
updateCmd.Parameters.AddWithValue("@UserName", "username");
updateCmd.ExecuteNonQuery();
// 5. 删除数据
using var deleteCmd = new OleDbCommand(
"DELETE FROM UserInfo WHERE UserName = ?", connection);
deleteCmd.Parameters.AddWithValue("@UserName", "username");
deleteCmd.ExecuteNonQuery();
```
**优点**:
- ✅ 微软官方支持
- ✅ 无需额外 NuGet 包
- ✅ 性能最佳
- ✅ 稳定可靠
- ✅ 适用于所有 .NET 版本
**缺点**:
- ❌ 需要手写 SQL
- ❌ 没有 LINQ 支持
- ❌ 没有自动实体映射
---
### 方案 B:EntityFrameworkCore.Jet
**适用场景**:
- 需要复杂的 LINQ 查询
- 需要实体关系映射
- 代码优雅度要求高
- 可以接受第三方库
**实现文件**:
- `Views/AccessDatabaseWindow.xaml`
- `Views/AccessDatabaseWindow.xaml.cs`
- `Data/Access/AccessEntities.cs`
- `Data/Access/AccessDbContext.cs`
**示例代码**:
```csharp
// 1. 创建 DbContext
var optionsBuilder = new DbContextOptionsBuilder<AccessDbContext>();
optionsBuilder.UseJet("Provider=Microsoft.ACE.OLEDB.12.0;Data Source=dbDwDmsSetup.accdb");
using var dbContext = new AccessDbContext(optionsBuilder.Options);
// 2. LINQ 查询
var users = await dbContext.UserInfos
.Include(u => u.Permissions)
.Where(u => u.UserName.Contains("admin"))
.ToListAsync();
// 3. 插入数据
var newUser = new UserInfo { UserName = "newuser", Password = "password" };
dbContext.UserInfos.Add(newUser);
await dbContext.SaveChangesAsync();
// 4. 更新数据
var user = await dbContext.UserInfos.FindAsync("username");
if (user != null)
{
user.Password = "newpassword";
await dbContext.SaveChangesAsync();
}
// 5. 删除数据
var userToDelete = await dbContext.UserInfos.FindAsync("username");
if (userToDelete != null)
{
dbContext.UserInfos.Remove(userToDelete);
await dbContext.SaveChangesAsync();
}
```
**优点**:
- ✅ LINQ 查询支持
- ✅ 自动实体映射
- ✅ 代码更优雅
- ✅ 支持导航属性
**缺点**:
- ❌ 第三方库(EntityFrameworkCore.Jet)
- ❌ 版本兼容性问题
- ❌ 性能开销
- ❌ 可能与其他 EF Core 提供程序冲突
---
## 推荐方案
### 对于简单的增删改查:
**推荐使用方案 A(System.Data.OleDb)**
理由:
1. 稳定可靠,微软官方支持
2. 性能最佳
3. 无依赖问题
4. 代码简单直接
### 对于复杂的业务逻辑:
可以考虑方案 B(EntityFrameworkCore.Jet),但需要:
1. 接受第三方库的风险
2. 处理版本兼容性问题
3. 确保不与其他数据库提供程序冲突
---
## 当前 Server 项目使用的方案
当前使用:**方案 A(System.Data.OleDb)** ✅
主窗口按钮 "Access数据库" 打开的是 `AccessDatabaseWindowOleDb` 窗口。
如需切换到方案 B,修改 `MainWindow.xaml.cs` 中的 `AccessDbButton_Click` 方法:
```csharp
// 方案 A(当前)
var window = new AccessDatabaseWindowOleDb { Owner = this };
// 方案 B
var window = new AccessDatabaseWindow { Owner = this };
```
---
## 性能对比
| 操作 | OleDb | EF Core Jet |
|------|-------|-------------|
| 简单查询 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| 复杂查询 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 插入操作 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| 批量操作 | ⭐⭐⭐⭐⭐ | ⭐⭐ |
| 内存占用 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
---
## 总结
对于你的需求(简单的增删改查),**强烈推荐使用 System.Data.OleDb(方案 A)**。
它是最稳定、性能最好、最简单的方案。
+2
View File
@@ -55,6 +55,8 @@
Background="#FF9800" Foreground="White" BorderThickness="0" FontWeight="Bold" Cursor="Hand" IsEnabled="False"/>
<Button x:Name="DataManagementButton" Content="数据管理" Width="100" Height="32" Margin="0,0,10,0" Click="DataManagementButton_Click"
Background="#673AB7" Foreground="White" BorderThickness="0" FontWeight="Bold" Cursor="Hand"/>
<Button x:Name="AccessDbButton" Content="Access数据库" Width="110" Height="32" Margin="0,0,10,0" Click="AccessDbButton_Click"
Background="#009688" Foreground="White" BorderThickness="0" FontWeight="Bold" Cursor="Hand"/>
<Button x:Name="ClearLogButton" Content="清空日志" Width="100" Height="32" Click="ClearLogButton_Click"
Background="#9E9E9E" Foreground="White" BorderThickness="0" FontWeight="Bold" Cursor="Hand"/>
</StackPanel>
+20
View File
@@ -155,6 +155,26 @@ namespace Server
}
}
private void AccessDbButton_Click(object sender, RoutedEventArgs e)
{
try
{
_logger?.LogInformation("打开 Access 数据库管理窗口 (OleDb)");
var window = new AccessDatabaseWindowOleDb
{
Owner = this
};
window.ShowDialog();
_logger?.LogInformation("Access 数据库管理窗口已关闭");
}
catch (Exception ex)
{
_logger?.LogError(ex, "打开 Access 数据库管理窗口失败");
AppendLog($"打开 Access 数据库管理窗口失败: {ex.Message}");
MessageBox.Show($"打开 Access 数据库管理窗口失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void OnServerStatusChanged(object? sender, string status)
{
Dispatcher.Invoke(() =>
+7
View File
@@ -7,6 +7,9 @@
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
<!-- 强制使用 x64 架构,与已安装的 64 位 ACE 驱动匹配 -->
<PlatformTarget>x64</PlatformTarget>
<PublishSingleFile>true</PublishSingleFile>
<SelfContained>true</SelfContained>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
@@ -22,6 +25,7 @@
<ProjectReference Include="..\Wpf\Wpf.csproj" />
<ProjectReference Include="..\Common\Common.csproj" />
<ProjectReference Include="..\Data\Data.csproj" />
<ProjectReference Include="..\Data.Access\Data.Access.csproj" />
</ItemGroup>
<ItemGroup>
@@ -34,6 +38,9 @@
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="dbDwDmsSetup.accdb">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
@@ -0,0 +1,54 @@
<Window x:Class="Server.Views.AccessDatabaseWindow"
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="Access 数据库管理" Height="600" Width="900"
WindowStartupLocation="CenterScreen">
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="200"/>
</Grid.RowDefinitions>
<!-- 标题和操作栏 -->
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,10">
<TextBlock Text="Access 数据库管理" FontSize="20" FontWeight="Bold" VerticalAlignment="Center"/>
<TextBlock Text=" (dbDwDmsSetup.accdb)" FontSize="14" Foreground="Gray" VerticalAlignment="Center" Margin="5,0,0,0"/>
<Button Content="刷新数据" Click="RefreshButton_Click" Margin="20,0,0,0" Padding="10,5"/>
<Button Content="测试连接" Click="TestConnectionButton_Click" Margin="10,0,0,0" Padding="10,5"/>
</StackPanel>
<!-- 用户列表 -->
<GroupBox Grid.Row="1" Header="用户列表" Margin="0,0,0,10">
<DataGrid x:Name="UsersDataGrid"
AutoGenerateColumns="False"
IsReadOnly="True"
SelectionMode="Single"
SelectionChanged="UsersDataGrid_SelectionChanged">
<DataGrid.Columns>
<DataGridTextColumn Header="用户名" Binding="{Binding UserName}" Width="200"/>
<DataGridTextColumn Header="密码" Binding="{Binding Password}" Width="*"/>
<DataGridTextColumn Header="权限数量" Binding="{Binding Permissions.Count}" Width="100"/>
</DataGrid.Columns>
</DataGrid>
</GroupBox>
<!-- 分隔符 -->
<GridSplitter Grid.Row="2" Height="5" HorizontalAlignment="Stretch" Background="LightGray"/>
<!-- 权限列表 -->
<GroupBox Grid.Row="3" Header="选中用户的权限">
<DataGrid x:Name="PermissionsDataGrid"
AutoGenerateColumns="False"
IsReadOnly="True">
<DataGrid.Columns>
<DataGridTextColumn Header="权限代码" Binding="{Binding Permission_Code}" Width="*"/>
</DataGrid.Columns>
</DataGrid>
</GroupBox>
</Grid>
</Window>
@@ -0,0 +1,146 @@
using System;
using System.Data.OleDb;
using System.IO;
using System.Linq;
using System.Windows;
using EntityFrameworkCore.Jet;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Sharp.Data.Access.Options;
using Server.Data.Access;
namespace Server.Views;
public partial class AccessDatabaseWindow : Window
{
private readonly ILogger<AccessDatabaseWindow>? _logger;
private const string DatabaseFile = "dbDwDmsSetup.accdb";
public AccessDatabaseWindow()
{
InitializeComponent();
try
{
var app = (App)Application.Current;
_logger = app.LoggerFactory?.CreateLogger<AccessDatabaseWindow>();
Loaded += AccessDatabaseWindow_Loaded;
}
catch (Exception ex)
{
MessageBox.Show($"初始化失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private async void AccessDatabaseWindow_Loaded(object sender, RoutedEventArgs e)
{
await LoadDataAsync();
}
private async void RefreshButton_Click(object sender, RoutedEventArgs e)
{
await LoadDataAsync();
}
private async void TestConnectionButton_Click(object sender, RoutedEventArgs e)
{
try
{
_logger?.LogInformation("测试 Access 数据库连接...");
using var dbContext = CreateDbContext();
var canConnect = await dbContext.Database.CanConnectAsync();
if (canConnect)
{
_logger?.LogInformation("数据库连接成功!");
MessageBox.Show("数据库连接成功!", "成功", MessageBoxButton.OK, MessageBoxImage.Information);
}
else
{
_logger?.LogWarning("无法连接到数据库");
MessageBox.Show("无法连接到数据库", "警告", MessageBoxButton.OK, MessageBoxImage.Warning);
}
}
catch (Exception ex)
{
_logger?.LogError(ex, "测试连接失败");
MessageBox.Show($"测试连接失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private async System.Threading.Tasks.Task LoadDataAsync()
{
try
{
_logger?.LogInformation("开始加载 Access 数据库数据...");
using var dbContext = CreateDbContext();
// 查询所有用户及其权限
var users = await dbContext.UserInfos
.Include(u => u.Permissions)
.ToListAsync();
_logger?.LogInformation($"加载了 {users.Count} 个用户");
// 更新 UI
UsersDataGrid.ItemsSource = users;
PermissionsDataGrid.ItemsSource = null;
MessageBox.Show($"成功加载 {users.Count} 个用户!", "成功", MessageBoxButton.OK, MessageBoxImage.Information);
}
catch (Exception ex)
{
_logger?.LogError(ex, "加载数据失败");
MessageBox.Show($"加载数据失败: {ex.Message}\n\n详细信息:\n{ex.ToString()}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void UsersDataGrid_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
{
if (UsersDataGrid.SelectedItem is UserInfo selectedUser)
{
_logger?.LogInformation($"选中用户: {selectedUser.UserName}, 权限数: {selectedUser.Permissions.Count}");
PermissionsDataGrid.ItemsSource = selectedUser.Permissions;
}
else
{
PermissionsDataGrid.ItemsSource = null;
}
}
/// <summary>
/// 创建 Access 数据库上下文(使用新的统一配置体系)
/// </summary>
private AccessDbContext CreateDbContext()
{
// 获取数据库文件的完整路径
var dbPath = Path.GetFullPath(DatabaseFile);
if (!File.Exists(dbPath))
{
throw new FileNotFoundException($"找不到 Access 数据库文件: {dbPath}");
}
// 使用统一的配置方式构建连接字符串
var builder = new OleDbConnectionStringBuilder
{
["Provider"] = AccessProviderNames.AceOleDb16, // 使用 16.0
["Data Source"] = dbPath
// Connect Timeout 在某些情况下不稳定,已移除
};
// 如果数据库有密码,可以在这里设置:
// builder["Jet OLEDB:Database Password"] = "your_password";
var connectionString = builder.ConnectionString;
// 创建 DbContext 选项
var optionsBuilder = new DbContextOptionsBuilder<AccessDbContext>();
optionsBuilder.UseJet(connectionString);
return new AccessDbContext(optionsBuilder.Options);
}
}
@@ -0,0 +1,76 @@
<Window x:Class="Server.Views.AccessDatabaseWindowOleDb"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Access 数据库管理 (OleDb方式)" Height="700" Width="1000"
WindowStartupLocation="CenterScreen">
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="200"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 标题和操作栏 -->
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,10">
<TextBlock Text="Access 数据库管理" FontSize="20" FontWeight="Bold" VerticalAlignment="Center"/>
<TextBlock Text=" (System.Data.OleDb)" FontSize="14" Foreground="Green" VerticalAlignment="Center" Margin="5,0,0,0"/>
<Button Content="刷新数据" Click="RefreshButton_Click" Margin="20,0,0,0" Padding="10,5"/>
<Button Content="测试连接" Click="TestConnectionButton_Click" Margin="10,0,0,0" Padding="10,5"/>
</StackPanel>
<!-- 用户操作区 -->
<GroupBox Grid.Row="1" Header="用户操作" Margin="0,0,0,10">
<StackPanel Orientation="Horizontal" Margin="5">
<TextBlock Text="用户名:" VerticalAlignment="Center" Margin="0,0,5,0"/>
<TextBox x:Name="UserNameTextBox" Width="150" Margin="0,0,10,0"/>
<TextBlock Text="密码:" VerticalAlignment="Center" Margin="0,0,5,0"/>
<TextBox x:Name="PasswordTextBox" Width="150" Margin="0,0,20,0"/>
<Button Content="添加用户" Click="AddUserButton_Click" Padding="10,5" Margin="0,0,5,0" Background="#4CAF50" Foreground="White"/>
<Button Content="修改密码" Click="UpdatePasswordButton_Click" Padding="10,5" Margin="0,0,5,0" Background="#FF9800" Foreground="White"/>
<Button Content="删除用户" Click="DeleteUserButton_Click" Padding="10,5" Background="#F44336" Foreground="White"/>
</StackPanel>
</GroupBox>
<!-- 用户列表 -->
<GroupBox Grid.Row="2" Header="用户列表" Margin="0,0,0,10">
<DataGrid x:Name="UsersDataGrid"
AutoGenerateColumns="False"
IsReadOnly="True"
SelectionMode="Single"
SelectionChanged="UsersDataGrid_SelectionChanged">
<DataGrid.Columns>
<DataGridTextColumn Header="用户名" Binding="{Binding UserName}" Width="200"/>
<DataGridTextColumn Header="密码" Binding="{Binding Password}" Width="*"/>
<DataGridTextColumn Header="权限数量" Binding="{Binding PermissionCount}" Width="100"/>
</DataGrid.Columns>
</DataGrid>
</GroupBox>
<!-- 分隔符 -->
<GridSplitter Grid.Row="3" Height="5" HorizontalAlignment="Stretch" Background="LightGray"/>
<!-- 权限列表 -->
<GroupBox Grid.Row="4" Header="选中用户的权限">
<DataGrid x:Name="PermissionsDataGrid"
AutoGenerateColumns="False"
IsReadOnly="True">
<DataGrid.Columns>
<DataGridTextColumn Header="权限代码" Binding="{Binding Permission_Code}" Width="*"/>
</DataGrid.Columns>
</DataGrid>
</GroupBox>
<!-- 权限操作区 -->
<GroupBox Grid.Row="5" Header="权限操作" Margin="0,10,0,0">
<StackPanel Orientation="Horizontal" Margin="5">
<TextBlock Text="权限代码:" VerticalAlignment="Center" Margin="0,0,5,0"/>
<TextBox x:Name="PermissionCodeTextBox" Width="200" Margin="0,0,20,0"/>
<Button Content="添加权限" Click="AddPermissionButton_Click" Padding="10,5" Margin="0,0,5,0" Background="#4CAF50" Foreground="White"/>
<Button Content="删除权限" Click="DeletePermissionButton_Click" Padding="10,5" Background="#F44336" Foreground="White"/>
</StackPanel>
</GroupBox>
</Grid>
</Window>
@@ -0,0 +1,560 @@
using System;
using System.Collections.Generic;
using System.Data.OleDb;
using System.Linq;
using System.Windows;
using Microsoft.Extensions.Logging;
using Sharp.Data.Access;
namespace Server.Views;
/// <summary>
/// 使用 AccessDataService 访问 Access 数据库(推荐方式)
/// 包含完整的增删改查功能
/// </summary>
public partial class AccessDatabaseWindowOleDb : Window
{
private readonly IAccessDataService? _accessDataService;
private readonly ILogger<AccessDatabaseWindowOleDb>? _logger;
private string? _selectedUserName;
public AccessDatabaseWindowOleDb()
{
InitializeComponent();
try
{
var app = (App)Application.Current;
_logger = app.LoggerFactory?.CreateLogger<AccessDatabaseWindowOleDb>();
_accessDataService = app.AccessDataService;
if (_accessDataService == null)
{
_logger?.LogWarning("AccessDataService 未初始化");
MessageBox.Show("Access 数据服务未初始化,某些功能可能不可用", "警告",
MessageBoxButton.OK, MessageBoxImage.Warning);
}
Loaded += Window_Loaded;
}
catch (Exception ex)
{
MessageBox.Show($"初始化失败: {ex.Message}", "错误",
MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private async void Window_Loaded(object sender, RoutedEventArgs e)
{
await Task.Run(() => LoadData());
}
#region 基础操作
private async void RefreshButton_Click(object sender, RoutedEventArgs e)
{
await Task.Run(() => LoadData());
}
private async void TestConnectionButton_Click(object sender, RoutedEventArgs e)
{
if (_accessDataService == null)
{
MessageBox.Show("Access 数据服务未初始化", "错误",
MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
try
{
_logger?.LogInformation("测试 Access 数据库连接...");
var success = await Task.Run(() => _accessDataService.TestConnection());
if (success)
{
_logger?.LogInformation("数据库连接成功!");
MessageBox.Show("数据库连接成功!", "成功",
MessageBoxButton.OK, MessageBoxImage.Information);
}
else
{
MessageBox.Show("数据库连接失败", "错误",
MessageBoxButton.OK, MessageBoxImage.Error);
}
}
catch (Exception ex)
{
_logger?.LogError(ex, "测试连接失败");
MessageBox.Show($"测试连接失败: {ex.Message}", "错误",
MessageBoxButton.OK, MessageBoxImage.Error);
}
}
#endregion
#region 用户管理(增删改查)
/// <summary>
/// 添加用户
/// </summary>
private async void AddUserButton_Click(object sender, RoutedEventArgs e)
{
if (_accessDataService == null) return;
var userName = UserNameTextBox.Text.Trim();
var password = PasswordTextBox.Text.Trim();
if (string.IsNullOrEmpty(userName))
{
MessageBox.Show("请输入用户名", "提示",
MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
if (string.IsNullOrEmpty(password))
{
MessageBox.Show("请输入密码", "提示",
MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
try
{
await Task.Run(() =>
{
// 检查用户是否已存在
var count = _accessDataService.ExecuteScalar(
"SELECT COUNT(*) FROM UserInfo WHERE UserName = ?",
new OleDbParameter("@UserName", userName));
if (Convert.ToInt32(count) > 0)
{
Dispatcher.Invoke(() =>
{
MessageBox.Show($"用户 '{userName}' 已存在!", "警告",
MessageBoxButton.OK, MessageBoxImage.Warning);
});
return;
}
// 插入新用户
_accessDataService.Execute(
"INSERT INTO UserInfo (UserName, [Password]) VALUES (?, ?)",
new OleDbParameter("@UserName", userName),
new OleDbParameter("@Password", password));
_logger?.LogInformation($"成功添加用户: {userName}");
Dispatcher.Invoke(() =>
{
MessageBox.Show($"成功添加用户 '{userName}'!", "成功",
MessageBoxButton.OK, MessageBoxImage.Information);
UserNameTextBox.Clear();
PasswordTextBox.Clear();
});
LoadData();
});
}
catch (Exception ex)
{
_logger?.LogError(ex, "添加用户失败");
MessageBox.Show($"添加用户失败: {ex.Message}", "错误",
MessageBoxButton.OK, MessageBoxImage.Error);
}
}
/// <summary>
/// 修改用户密码
/// </summary>
private async void UpdatePasswordButton_Click(object sender, RoutedEventArgs e)
{
if (_accessDataService == null) return;
var userName = UserNameTextBox.Text.Trim();
var newPassword = PasswordTextBox.Text.Trim();
if (string.IsNullOrEmpty(userName))
{
MessageBox.Show("请输入用户名或从列表中选择用户", "提示",
MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
if (string.IsNullOrEmpty(newPassword))
{
MessageBox.Show("请输入新密码", "提示",
MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
var result = MessageBox.Show(
$"确定要修改用户 '{userName}' 的密码吗?",
"确认",
MessageBoxButton.YesNo,
MessageBoxImage.Question);
if (result != MessageBoxResult.Yes)
return;
try
{
await Task.Run(() =>
{
// 更新密码
var rowsAffected = _accessDataService.Execute(
"UPDATE UserInfo SET [Password] = ? WHERE UserName = ?",
new OleDbParameter("@Password", newPassword),
new OleDbParameter("@UserName", userName));
if (rowsAffected == 0)
{
Dispatcher.Invoke(() =>
{
MessageBox.Show($"用户 '{userName}' 不存在!", "警告",
MessageBoxButton.OK, MessageBoxImage.Warning);
});
return;
}
_logger?.LogInformation($"成功修改用户 {userName} 的密码");
Dispatcher.Invoke(() =>
{
MessageBox.Show($"成功修改用户 '{userName}' 的密码!", "成功",
MessageBoxButton.OK, MessageBoxImage.Information);
PasswordTextBox.Clear();
});
LoadData();
});
}
catch (Exception ex)
{
_logger?.LogError(ex, "修改密码失败");
MessageBox.Show($"修改密码失败: {ex.Message}", "错误",
MessageBoxButton.OK, MessageBoxImage.Error);
}
}
/// <summary>
/// 删除用户
/// </summary>
private async void DeleteUserButton_Click(object sender, RoutedEventArgs e)
{
if (_accessDataService == null) return;
var userName = UserNameTextBox.Text.Trim();
if (string.IsNullOrEmpty(userName))
{
MessageBox.Show("请输入用户名或从列表中选择用户", "提示",
MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
var result = MessageBox.Show(
$"确定要删除用户 '{userName}' 及其所有权限吗?\n此操作不可恢复!",
"确认删除",
MessageBoxButton.YesNo,
MessageBoxImage.Warning);
if (result != MessageBoxResult.Yes)
return;
try
{
// 使用事务删除用户及其权限
await Task.Run(() =>
{
_accessDataService.ExecuteInTransaction(transaction =>
{
// 先删除用户的所有权限
transaction.Execute(
"DELETE FROM UserPermission WHERE UserInfo_UserName = ?",
new OleDbParameter("@UserName", userName));
// 再删除用户
var rowsAffected = transaction.Execute(
"DELETE FROM UserInfo WHERE UserName = ?",
new OleDbParameter("@UserName", userName));
if (rowsAffected == 0)
{
throw new InvalidOperationException($"用户 '{userName}' 不存在!");
}
});
_logger?.LogInformation($"成功删除用户: {userName}");
Dispatcher.Invoke(() =>
{
MessageBox.Show($"成功删除用户 '{userName}'!", "成功",
MessageBoxButton.OK, MessageBoxImage.Information);
UserNameTextBox.Clear();
PasswordTextBox.Clear();
});
LoadData();
});
}
catch (InvalidOperationException ex)
{
MessageBox.Show(ex.Message, "警告",
MessageBoxButton.OK, MessageBoxImage.Warning);
}
catch (Exception ex)
{
_logger?.LogError(ex, "删除用户失败");
MessageBox.Show($"删除用户失败: {ex.Message}", "错误",
MessageBoxButton.OK, MessageBoxImage.Error);
}
}
#endregion
#region 权限管理
/// <summary>
/// 添加权限
/// </summary>
private async void AddPermissionButton_Click(object sender, RoutedEventArgs e)
{
if (_accessDataService == null) return;
if (string.IsNullOrEmpty(_selectedUserName))
{
MessageBox.Show("请先选择一个用户", "提示",
MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
var permissionCode = PermissionCodeTextBox.Text.Trim();
if (string.IsNullOrEmpty(permissionCode))
{
MessageBox.Show("请输入权限代码", "提示",
MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
try
{
await Task.Run(() =>
{
// 检查权限是否已存在
var count = _accessDataService.ExecuteScalar(
"SELECT COUNT(*) FROM UserPermission WHERE UserInfo_UserName = ? AND Permission_Code = ?",
new OleDbParameter("@UserName", _selectedUserName),
new OleDbParameter("@PermissionCode", permissionCode));
if (Convert.ToInt32(count) > 0)
{
Dispatcher.Invoke(() =>
{
MessageBox.Show($"用户 '{_selectedUserName}' 已拥有权限 '{permissionCode}'!", "警告",
MessageBoxButton.OK, MessageBoxImage.Warning);
});
return;
}
// 插入权限
_accessDataService.Execute(
"INSERT INTO UserPermission (UserInfo_UserName, Permission_Code) VALUES (?, ?)",
new OleDbParameter("@UserName", _selectedUserName),
new OleDbParameter("@PermissionCode", permissionCode));
_logger?.LogInformation($"成功为用户 {_selectedUserName} 添加权限: {permissionCode}");
Dispatcher.Invoke(() =>
{
MessageBox.Show($"成功添加权限 '{permissionCode}'!", "成功",
MessageBoxButton.OK, MessageBoxImage.Information);
PermissionCodeTextBox.Clear();
LoadUserPermissions(_selectedUserName);
});
});
}
catch (Exception ex)
{
_logger?.LogError(ex, "添加权限失败");
MessageBox.Show($"添加权限失败: {ex.Message}", "错误",
MessageBoxButton.OK, MessageBoxImage.Error);
}
}
/// <summary>
/// 删除权限
/// </summary>
private async void DeletePermissionButton_Click(object sender, RoutedEventArgs e)
{
if (_accessDataService == null) return;
if (string.IsNullOrEmpty(_selectedUserName))
{
MessageBox.Show("请先选择一个用户", "提示",
MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
if (PermissionsDataGrid.SelectedItem is not UserPermissionDto selectedPermission)
{
MessageBox.Show("请从列表中选择要删除的权限", "提示",
MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
var result = MessageBox.Show(
$"确定要删除用户 '{_selectedUserName}' 的权限 '{selectedPermission.Permission_Code}' 吗?",
"确认删除",
MessageBoxButton.YesNo,
MessageBoxImage.Question);
if (result != MessageBoxResult.Yes)
return;
try
{
await Task.Run(() =>
{
_accessDataService.Execute(
"DELETE FROM UserPermission WHERE UserInfo_UserName = ? AND Permission_Code = ?",
new OleDbParameter("@UserName", _selectedUserName),
new OleDbParameter("@PermissionCode", selectedPermission.Permission_Code));
_logger?.LogInformation($"成功删除用户 {_selectedUserName} 的权限: {selectedPermission.Permission_Code}");
Dispatcher.Invoke(() =>
{
MessageBox.Show($"成功删除权限 '{selectedPermission.Permission_Code}'!", "成功",
MessageBoxButton.OK, MessageBoxImage.Information);
LoadUserPermissions(_selectedUserName);
});
});
}
catch (Exception ex)
{
_logger?.LogError(ex, "删除权限失败");
MessageBox.Show($"删除权限失败: {ex.Message}", "错误",
MessageBoxButton.OK, MessageBoxImage.Error);
}
}
#endregion
#region 数据加载
private void LoadData()
{
if (_accessDataService == null) return;
try
{
_logger?.LogInformation("开始加载 Access 数据库数据...");
// 查询所有用户(使用泛型映射)
var users = _accessDataService.Query<UserInfoDto>(
"SELECT [UserName], [Password] AS [Password] FROM [UserInfo]");
var userDetails = string.Join(" | ",
users.Select(u => $"UserName='{u.UserName}', Password='{u.Password}'"));
_logger?.LogInformation("用户列表: {UserDetails}", userDetails);
// 为每个用户查询权限数量
foreach (var user in users)
{
var count = _accessDataService.ExecuteScalar(
"SELECT COUNT(*) FROM UserPermission WHERE UserInfo_UserName = ?",
new OleDbParameter("@UserName", user.UserName));
user.PermissionCount = Convert.ToInt32(count);
}
_logger?.LogInformation($"加载了 {users.Count} 个用户");
// 更新 UI(需要在 UI 线程上执行)
Dispatcher.Invoke(() =>
{
UsersDataGrid.ItemsSource = users;
PermissionsDataGrid.ItemsSource = null;
_selectedUserName = null;
});
}
catch (Exception ex)
{
_logger?.LogError(ex, "加载数据失败");
Dispatcher.Invoke(() =>
{
MessageBox.Show($"加载数据失败: {ex.Message}", "错误",
MessageBoxButton.OK, MessageBoxImage.Error);
});
}
}
private void UsersDataGrid_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
{
if (UsersDataGrid.SelectedItem is UserInfoDto selectedUser)
{
_selectedUserName = selectedUser.UserName;
UserNameTextBox.Text = selectedUser.UserName;
PasswordTextBox.Text = selectedUser.Password;
LoadUserPermissions(selectedUser.UserName);
}
else
{
_selectedUserName = null;
PermissionsDataGrid.ItemsSource = null;
}
}
private void LoadUserPermissions(string userName)
{
if (_accessDataService == null) return;
try
{
// 查询用户权限(使用泛型映射)
var permissions = _accessDataService.Query<UserPermissionDto>(
"SELECT Permission_Code FROM UserPermission WHERE UserInfo_UserName = ?",
new OleDbParameter("@UserName", userName));
Dispatcher.Invoke(() =>
{
PermissionsDataGrid.ItemsSource = permissions;
});
_logger?.LogInformation($"用户 {userName} 有 {permissions.Count} 个权限");
}
catch (Exception ex)
{
_logger?.LogError(ex, "加载权限失败");
Dispatcher.Invoke(() =>
{
MessageBox.Show($"加载权限失败: {ex.Message}", "错误",
MessageBoxButton.OK, MessageBoxImage.Error);
});
}
}
#endregion
}
/// <summary>
/// 用户信息 DTO
/// </summary>
public class UserInfoDto
{
public string UserName { get; set; } = "";
public string Password { get; set; } = "";
public int PermissionCount { get; set; }
}
/// <summary>
/// 用户权限 DTO
/// </summary>
public class UserPermissionDto
{
public string Permission_Code { get; set; } = "";
}
+1 -1
View File
@@ -6,7 +6,7 @@
},
"SharpLog": {
"UseConsole": true,
"MinLevel": "Information",
"MinLevel": "Debug",
"File": {
"Enabled": true,
"Path": "logs/server-.log",
Binary file not shown.
@@ -0,0 +1,4 @@
{"Timestamp":"2025-11-19T10:17:05.3241502+08:00","Level":"Information","MessageTemplate":"正在初始化数据库...","RenderedMessage":"正在初始化数据库...","Properties":{"SourceContext":"Server.App","MachineName":"WEIYP","ProcessId":51312,"ThreadId":1,"EnvironmentName":"Production","Application":"SharpServer","Environment":"Production"}}
{"Timestamp":"2025-11-19T10:17:06.0810621+08:00","Level":"Information","MessageTemplate":"数据库初始化成功","RenderedMessage":"数据库初始化成功","Properties":{"SourceContext":"Server.App","MachineName":"WEIYP","ProcessId":51312,"ThreadId":1,"EnvironmentName":"Production","Application":"SharpServer","Environment":"Production"}}
{"Timestamp":"2025-11-19T10:17:06.0815557+08:00","Level":"Information","MessageTemplate":"应用程序启动成功","RenderedMessage":"应用程序启动成功","Properties":{"SourceContext":"Server.App","MachineName":"WEIYP","ProcessId":51312,"ThreadId":1,"EnvironmentName":"Production","Application":"SharpServer","Environment":"Production"}}
{"Timestamp":"2025-11-19T10:21:42.6459518+08:00","Level":"Information","MessageTemplate":"应用程序正常退出","RenderedMessage":"应用程序正常退出","Properties":{"SourceContext":"Server.App","MachineName":"WEIYP","ProcessId":51312,"ThreadId":1,"EnvironmentName":"Production","Application":"SharpServer","Environment":"Production"}}
+10 -10
View File
@@ -39,9 +39,9 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Data", "Data\Data.csproj",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Data.Test", "..\..\test\Data.Test\Data.Test.csproj", "{8EEC8BF4-CF9A-473D-B7A0-005E151C645D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Plc", "Plc\Plc.csproj", "{12FA0DCD-A69B-4862-901C-A8CF8694B9E3}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Data.Access", "Data.Access\Data.Access.csproj", "{944051DB-AAB1-4502-9AF6-B496EA8FD737}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Plc.Siemens", "Plc.Siemens\Plc.Siemens.csproj", "{7C877AB8-1938-4CC7-8692-0D2253C728DF}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Data.Access.Test", "..\..\test\Data.Access.Test\Data.Access.Test.csproj", "{48A70F46-8764-4FEA-BAFA-42776B0B526F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -121,14 +121,14 @@ Global
{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
{12FA0DCD-A69B-4862-901C-A8CF8694B9E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{12FA0DCD-A69B-4862-901C-A8CF8694B9E3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{12FA0DCD-A69B-4862-901C-A8CF8694B9E3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{12FA0DCD-A69B-4862-901C-A8CF8694B9E3}.Release|Any CPU.Build.0 = Release|Any CPU
{7C877AB8-1938-4CC7-8692-0D2253C728DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7C877AB8-1938-4CC7-8692-0D2253C728DF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7C877AB8-1938-4CC7-8692-0D2253C728DF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7C877AB8-1938-4CC7-8692-0D2253C728DF}.Release|Any CPU.Build.0 = Release|Any CPU
{944051DB-AAB1-4502-9AF6-B496EA8FD737}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{944051DB-AAB1-4502-9AF6-B496EA8FD737}.Debug|Any CPU.Build.0 = Debug|Any CPU
{944051DB-AAB1-4502-9AF6-B496EA8FD737}.Release|Any CPU.ActiveCfg = Release|Any CPU
{944051DB-AAB1-4502-9AF6-B496EA8FD737}.Release|Any CPU.Build.0 = Release|Any CPU
{48A70F46-8764-4FEA-BAFA-42776B0B526F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{48A70F46-8764-4FEA-BAFA-42776B0B526F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{48A70F46-8764-4FEA-BAFA-42776B0B526F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{48A70F46-8764-4FEA-BAFA-42776B0B526F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -15,7 +15,7 @@ public sealed class ExceptionHandlingOptions
public bool RethrowAfterHandle { get; set; } = false;
/// <summary>
/// Custom logger hook.
/// 日志钩子.
/// </summary>
public Action<Exception>? Log { get; set; }
= ex => System.Diagnostics.Debug.WriteLine(ex.ToString());
@@ -1,4 +1,5 @@
using System;
using System.Net.Http;
namespace Sharp.Wpf.Exceptions;
@@ -1,4 +1,5 @@
using System;
using System.Net.Http;
using System.Threading.Tasks;
using WpfMessageBox = System.Windows.MessageBox;
@@ -35,6 +36,10 @@ public sealed class UiExceptionHandler : IUiExceptionHandler
private void OnDispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e)
{
Handle(e.Exception);
if (CanRecover(e.Exception))
{
e.Handled = true;
}
e.Handled = true;
}
@@ -86,4 +91,20 @@ public sealed class UiExceptionHandler : IUiExceptionHandler
}
}
}
protected bool CanRecover(Exception exception)
{
return exception switch
{
ArgumentException => true,
InvalidOperationException => true,
HttpRequestException => true,
TimeoutException => true,
OutOfMemoryException => false,
StackOverflowException => false,
AccessViolationException => false,
_ => true
};
}
}
@@ -1,10 +1,9 @@
//------------------------------------------------------------------------------
// <auto-generated>
// 此代码由工具生成。
// 运行时版本:4.0.30319.42000
// 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>
//------------------------------------------------------------------------------
@@ -14,7 +13,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+53d1dd2dbf05d818528814223b58610a790fcb26")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+e334e65c2d8abbef0063f92b88e6672859b4ea19")]
[assembly: System.Reflection.AssemblyProductAttribute("Sharp.Wpf")]
[assembly: System.Reflection.AssemblyTitleAttribute("Sharp.Wpf")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
@@ -1 +1 @@
fbdba11393faed768ef4c9b8f4eccef99aed87397a79576b900953e4aad05633
b6ad12dd3a3fcd3e5222848b29876246dac97d6908271610ecbcd090928adcb7
@@ -70,11 +70,6 @@
},
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.203/PortableRuntimeIdentifierGraph.json"
}
},
"runtimes": {
"win-x64": {
"#import": []
}
}
}
}
+1 -7
View File
@@ -1,8 +1,7 @@
{
"version": 3,
"targets": {
"net8.0-windows7.0": {},
"net8.0-windows7.0/win-x64": {}
"net8.0-windows7.0": {}
},
"libraries": {},
"projectFileDependencyGroups": {
@@ -78,11 +77,6 @@
},
"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": "9ZityDkLrvk=",
"dgSpecHash": "EQ+I1wXrehg=",
"success": true,
"projectFilePath": "D:\\yuanll\\sharp-basic\\src\\Sharp\\Wpf\\Wpf.csproj",
"expectedPackageFiles": [],
@@ -1 +1 @@
17634268840015055
17634526945569668
+1 -1
View File
@@ -1 +1 @@
17633637013370903
17634577083448171
@@ -0,0 +1,74 @@
using System;
using System.IO;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Sharp.Data.Access;
using Sharp.Data.Access.DependencyInjection;
using Sharp.Data.Access.Options;
using Sharp.Data.Access.Security;
using Sharp.Data.Access.Connections;
namespace Data.Access.Test;
[TestClass]
public sealed class AccessConnectionStringFactoryTests
{
[TestMethod]
public void Create_UsesDecryptedPasswordAndAdditionalParameters()
{
var tempDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(tempDirectory);
var options = new AccessDatabaseOptions
{
DatabaseFile = "data/app.accdb",
DataDirectory = tempDirectory,
EncryptedPassword = "encrypted-secret",
ProviderName = AccessProviderNames.AceOleDb12
};
options.AdditionalParameters["Jet OLEDB:Database Locking Mode"] = "1";
var decryptor = new FakeDecryptor("plain-text");
var services = new ServiceCollection();
services.AddSingleton<IAccessPasswordDecryptor>(decryptor);
services.AddAccessDataContext<FakeContext>(ctx =>
{
ctx.Database.ConnectionString = $"Provider={AccessProviderNames.AceOleDb16};Data Source=fake;";
}, registerDataContext: false);
var provider = services.BuildServiceProvider();
var factory = provider.GetRequiredService<IAccessConnectionStringFactory>();
var connectionString = factory.Create(options, provider);
var expectedPath = Path.GetFullPath(Path.Combine(tempDirectory, "data", "app.accdb"));
StringAssert.Contains(connectionString, $"Provider={AccessProviderNames.AceOleDb12}", "Provider should be set.");
StringAssert.Contains(connectionString, expectedPath, "Database path should be resolved to absolute path.");
StringAssert.Contains(connectionString, "Jet OLEDB:Database Password=plain-text", "Decrypted password should be used.");
StringAssert.Contains(connectionString, "Jet OLEDB:Database Locking Mode=1", "Additional parameters should be appended.");
Assert.AreEqual("encrypted-secret", decryptor.LastValue, "Decryptor should receive encrypted value.");
}
private sealed class FakeDecryptor : IAccessPasswordDecryptor
{
private readonly string? _result;
public FakeDecryptor(string? result) => _result = result;
public string? LastValue { get; private set; }
public string? Decrypt(string? encryptedPassword)
{
LastValue = encryptedPassword;
return _result;
}
}
private sealed class FakeContext : Microsoft.EntityFrameworkCore.DbContext
{
public FakeContext(Microsoft.EntityFrameworkCore.DbContextOptions<FakeContext> options) : base(options)
{
}
}
}
@@ -0,0 +1,82 @@
using System;
using System.IO;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Sharp.Data.Access;
using Sharp.Data.Access.DependencyInjection;
using Sharp.Data.Access.Initialization;
using Sharp.Data.Access.Options;
namespace Data.Access.Test;
[TestClass]
public sealed class AccessDatabaseInitializerTests
{
[TestMethod]
public void EnsureDatabase_CopiesTemplateWhenMissing()
{
var tempRoot = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(tempRoot);
var templateDir = Path.Combine(tempRoot, "template");
Directory.CreateDirectory(templateDir);
var templatePath = Path.Combine(templateDir, "seed.accdb");
File.WriteAllText(templatePath, "seed-data");
var options = new AccessDatabaseOptions
{
DatabaseFile = "db/app.accdb",
TemplateFile = "template/seed.accdb",
DataDirectory = tempRoot,
CopyTemplateWhenMissing = true,
ThrowIfMissing = true
};
var services = BuildServices();
var initializer = services.GetRequiredService<IAccessDatabaseInitializer>();
initializer.EnsureDatabase(options, services);
var targetPath = Path.Combine(tempRoot, "db", "app.accdb");
Assert.IsTrue(File.Exists(targetPath), "Database file should be created from template.");
Assert.AreEqual("seed-data", File.ReadAllText(targetPath));
}
[TestMethod]
public void EnsureDatabase_ThrowsWhenMissingAndNoTemplate()
{
var tempRoot = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(tempRoot);
var options = new AccessDatabaseOptions
{
DatabaseFile = "missing/app.accdb",
DataDirectory = tempRoot,
CopyTemplateWhenMissing = false,
ThrowIfMissing = true
};
var services = BuildServices();
var initializer = services.GetRequiredService<IAccessDatabaseInitializer>();
Assert.ThrowsException<FileNotFoundException>(() =>
initializer.EnsureDatabase(options, services));
}
private static ServiceProvider BuildServices()
{
var services = new ServiceCollection();
services.AddAccessDataContext<FakeContext>(ctx =>
{
ctx.Database.ConnectionString = $"Provider={AccessProviderNames.AceOleDb16};Data Source=fake;";
}, registerDataContext: false);
return services.BuildServiceProvider();
}
private sealed class FakeContext : Microsoft.EntityFrameworkCore.DbContext
{
public FakeContext(Microsoft.EntityFrameworkCore.DbContextOptions<FakeContext> options) : base(options)
{
}
}
}
@@ -0,0 +1,49 @@
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Sharp.Data;
using Sharp.Data.Access;
using Sharp.Data.Access.DependencyInjection;
using Sharp.Data.Access.Options;
namespace Data.Access.Test;
[TestClass]
public sealed class AccessServiceCollectionExtensionsTests
{
[TestMethod]
public void AddAccessDataContext_RegistersFactoryAndOptions()
{
var services = new ServiceCollection();
services.AddAccessDataContext<FakeContext>(options =>
{
options.Database.ConnectionString = $"Provider={AccessProviderNames.AceOleDb16};Data Source=fake;";
options.ConfigureDataContext = (sp, builder) =>
{
builder.UseAudit(false)
.UseSoftDelete(false)
.UseAutoMigration(false)
.EnableDetailedErrors(false);
};
}, registerDataContext: false);
var provider = services.BuildServiceProvider();
var options = provider.GetRequiredService<AccessContextOptions<FakeContext>>();
Assert.AreEqual($"Provider={AccessProviderNames.AceOleDb16};Data Source=fake;", options.Database.ConnectionString);
var decryptor = provider.GetRequiredService<Sharp.Data.Access.Security.IAccessPasswordDecryptor>();
Assert.IsInstanceOfType(decryptor, typeof(Sharp.Data.Access.Security.PassthroughAccessPasswordDecryptor));
var factory = provider.GetRequiredService<IAccessDataContextFactory<FakeContext>>();
Assert.IsNotNull(factory);
}
private sealed class FakeContext : DbContext
{
public FakeContext(DbContextOptions<FakeContext> options) : base(options)
{
}
}
}
@@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0"/>
<PackageReference Include="MSTest" Version="3.6.4"/>
<PackageReference Include="EntityFrameworkCore.Jet" Version="8.0.0-preview1" />
<PackageReference Include="System.Data.OleDb" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<Using Include="Microsoft.VisualStudio.TestTools.UnitTesting"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Sharp\Data.Access\Data.Access.csproj" />
<ProjectReference Include="..\..\src\Sharp\Data\Data.csproj" />
<ProjectReference Include="..\..\src\Sharp\Common\Common.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="dbDwDmsSetup.accdb">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
@@ -0,0 +1,44 @@
using Microsoft.EntityFrameworkCore;
namespace Data.Access.Test.Entities;
/// <summary>
/// 真实 Access 数据库的 DbContext
/// </summary>
public class AccessDbContext : DbContext
{
public AccessDbContext(DbContextOptions<AccessDbContext> options) : base(options)
{
}
public DbSet<UserInfoEntity> UserInfos => Set<UserInfoEntity>();
public DbSet<UserPermissionEntity> UserPermissions => Set<UserPermissionEntity>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// 配置 UserInfo
modelBuilder.Entity<UserInfoEntity>(entity =>
{
entity.HasKey(e => e.UserName);
entity.Property(e => e.UserName).IsRequired().HasMaxLength(255);
entity.Property(e => e.Password).IsRequired().HasMaxLength(255);
});
// 配置 UserPermission
modelBuilder.Entity<UserPermissionEntity>(entity =>
{
// 复合主键
entity.HasKey(e => new { e.UserInfo_UserName, e.Permission_Code });
entity.Property(e => e.UserInfo_UserName).IsRequired().HasMaxLength(255);
entity.Property(e => e.Permission_Code).IsRequired().HasMaxLength(255);
// 配置关系
entity.HasOne(e => e.UserInfo)
.WithMany(u => u.Permissions)
.HasForeignKey(e => e.UserInfo_UserName)
.OnDelete(DeleteBehavior.Cascade);
});
}
}
@@ -0,0 +1,31 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Data.Access.Test.Entities;
/// <summary>
/// UserInfo 表的实体映射
/// </summary>
[Table("UserInfo")]
public class UserInfoEntity
{
/// <summary>
/// 用户名 (主键)
/// </summary>
[Key]
[Required]
[MaxLength(255)]
public string UserName { get; set; } = string.Empty;
/// <summary>
/// 密码
/// </summary>
[Required]
[MaxLength(255)]
public string Password { get; set; } = string.Empty;
/// <summary>
/// 用户权限 (导航属性)
/// </summary>
public ICollection<UserPermissionEntity> Permissions { get; set; } = new List<UserPermissionEntity>();
}
@@ -0,0 +1,31 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Data.Access.Test.Entities;
/// <summary>
/// UserPermission 表的实体映射
/// </summary>
[Table("UserPermission")]
public class UserPermissionEntity
{
/// <summary>
/// 用户名 (外键)
/// </summary>
[Required]
[MaxLength(255)]
public string UserInfo_UserName { get; set; } = string.Empty;
/// <summary>
/// 权限代码
/// </summary>
[Required]
[MaxLength(255)]
public string Permission_Code { get; set; } = string.Empty;
/// <summary>
/// 用户信息 (导航属性)
/// </summary>
[ForeignKey(nameof(UserInfo_UserName))]
public UserInfoEntity? UserInfo { get; set; }
}
+1
View File
@@ -0,0 +1 @@
[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)]
@@ -0,0 +1,193 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Sharp.Data;
using Sharp.Data.Access.DependencyInjection;
using Sharp.Data.Access.Extensions;
using Sharp.Data.Context;
using Data.Access.Test.Entities;
namespace Data.Access.Test;
/// <summary>
/// 使用真实 Access 数据库进行 CRUD 操作的完整测试
/// </summary>
[TestClass]
public class RealAccessDatabaseIntegrationTests
{
private const string DatabaseFile = "dbDwDmsSetup.accdb";
[TestMethod]
public async Task CanQueryUsersUsingDataContextBuilder()
{
// 方式1: 使用 DataContextBuilder (无 DI)
var builder = new DataContextBuilder<AccessDbContext>();
builder.UseAccessDatabase(options =>
{
options.DatabaseFile = DatabaseFile;
options.CopyTemplateWhenMissing = false;
options.ThrowIfMissing = true;
}, configure: jet =>
{
// 禁用自动迁移以避免冲突
});
builder.UseAutoMigration(false); // 禁用自动迁移
var dataContext = builder.Build();
// 查询所有用户
var users = await dataContext.Repository<UserInfoEntity>().GetAllAsync();
Console.WriteLine($"找到 {users.Count} 个用户:");
foreach (var user in users)
{
Console.WriteLine($" - {user.UserName}");
}
Assert.IsTrue(users.Count >= 0, "应该能够查询用户表");
}
[TestMethod]
public async Task CanQueryUsersUsingDependencyInjection()
{
// 方式2: 使用依赖注入
var services = new ServiceCollection();
services.AddAccessDataContext<AccessDbContext>(options =>
{
options.Database.DatabaseFile = DatabaseFile;
options.Database.CopyTemplateWhenMissing = false;
options.Database.ThrowIfMissing = true;
});
var provider = services.BuildServiceProvider();
var dataContext = provider.GetRequiredService<IDataContext>();
// 查询所有用户
var users = await dataContext.Repository<UserInfoEntity>().GetAllAsync();
Console.WriteLine($"\n通过 DI 找到 {users.Count} 个用户:");
foreach (var user in users)
{
Console.WriteLine($" - {user.UserName}");
}
Assert.IsTrue(users.Count >= 0, "应该能够通过 DI 查询用户表");
}
[TestMethod]
public async Task CanQueryUserPermissions()
{
var builder = new DataContextBuilder<AccessDbContext>();
builder.UseAccessDatabase(options =>
{
options.DatabaseFile = DatabaseFile;
});
var dataContext = builder.Build();
// 查询所有权限
var permissions = await dataContext.Repository<UserPermissionEntity>().GetAllAsync();
Console.WriteLine($"\n找到 {permissions.Count} 个权限记录:");
foreach (var perm in permissions.Take(10)) // 只显示前 10 条
{
Console.WriteLine($" - 用户: {perm.UserInfo_UserName}, 权限: {perm.Permission_Code}");
}
Assert.IsTrue(permissions.Count >= 0, "应该能够查询权限表");
}
[TestMethod]
public async Task CanQueryWithFilters()
{
var builder = new DataContextBuilder<AccessDbContext>();
builder.UseAccessDatabase(options =>
{
options.DatabaseFile = DatabaseFile;
});
var dataContext = builder.Build();
// 使用 LINQ 查询
var dbContext = dataContext.Context;
var adminUsers = await dbContext.Set<UserInfoEntity>()
.Where(u => u.UserName.Contains("admin") || u.UserName.Contains("Admin"))
.ToListAsync();
Console.WriteLine($"\n找到 {adminUsers.Count} 个管理员用户:");
foreach (var user in adminUsers)
{
Console.WriteLine($" - {user.UserName}");
}
}
[TestMethod]
public async Task CanQueryUserWithPermissions()
{
var builder = new DataContextBuilder<AccessDbContext>();
builder.UseAccessDatabase(options =>
{
options.DatabaseFile = DatabaseFile;
});
var dataContext = builder.Build();
var dbContext = dataContext.Context;
// 查询用户及其权限
var usersWithPermissions = await dbContext.Set<UserInfoEntity>()
.Include(u => u.Permissions)
.Take(5) // 只取前 5 个用户
.ToListAsync();
Console.WriteLine($"\n查询到 {usersWithPermissions.Count} 个用户及其权限:");
foreach (var user in usersWithPermissions)
{
Console.WriteLine($" 用户: {user.UserName}");
Console.WriteLine($" 权限数: {user.Permissions.Count}");
foreach (var perm in user.Permissions.Take(3)) // 每个用户只显示前 3 个权限
{
Console.WriteLine($" - {perm.Permission_Code}");
}
}
}
[TestMethod]
public async Task CanUseRepositoryPattern()
{
var builder = new DataContextBuilder<AccessDbContext>();
builder.UseAccessDatabase(options =>
{
options.DatabaseFile = DatabaseFile;
});
var dataContext = builder.Build();
// 使用 Repository 模式
var userRepo = dataContext.Repository<UserInfoEntity>();
// 获取总数
var totalUsers = await userRepo.CountAsync();
Console.WriteLine($"\n用户总数: {totalUsers}");
// 分页查询
var pagedResult = await userRepo.GetPagedAsync(1, 10);
Console.WriteLine($"第一页 (10条/页): {pagedResult.Items.Count} 条记录");
// 查询单个用户
if (totalUsers > 0)
{
var firstUser = await userRepo.FirstOrDefaultAsync(u => true);
if (firstUser != null)
{
Console.WriteLine($"第一个用户: {firstUser.UserName}");
}
}
Assert.IsTrue(totalUsers >= 0, "应该能够使用 Repository 模式");
}
}
+134
View File
@@ -0,0 +1,134 @@
using System;
using System.Data.OleDb;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Sharp.Data;
using Sharp.Data.Access.Extensions;
using Sharp.Data.Context;
namespace Data.Access.Test;
/// <summary>
/// 使用真实 Access 数据库的集成测试
/// </summary>
[TestClass]
public class RealDatabaseTests
{
private const string DatabaseFile = "dbDwDmsSetup.accdb";
[TestMethod]
public void CanReadDatabaseSchema()
{
// 读取数据库表结构
var connectionString = $"Provider=Microsoft.ACE.OLEDB.12.0;Data Source={DatabaseFile}";
using var connection = new OleDbConnection(connectionString);
connection.Open();
// 获取所有表
var tables = connection.GetSchema("Tables");
Assert.IsTrue(tables.Rows.Count > 0, "数据库应该包含表");
Console.WriteLine($"数据库包含 {tables.Rows.Count} 个对象:");
foreach (System.Data.DataRow row in tables.Rows)
{
var tableName = row["TABLE_NAME"]?.ToString();
var tableType = row["TABLE_TYPE"]?.ToString();
if (tableType == "TABLE")
{
Console.WriteLine($" - {tableName}");
}
}
}
[TestMethod]
public void CanListAllTables()
{
var connectionString = $"Provider=Microsoft.ACE.OLEDB.12.0;Data Source={DatabaseFile}";
using var connection = new OleDbConnection(connectionString);
connection.Open();
var restrictions = new string?[4];
restrictions[3] = "TABLE"; // 只获取表,不包括视图和系统表
var tables = connection.GetSchema("Tables", restrictions);
Console.WriteLine("\n=== 用户表列表 ===");
foreach (System.Data.DataRow row in tables.Rows)
{
var tableName = row["TABLE_NAME"]?.ToString();
if (!string.IsNullOrEmpty(tableName) && !tableName.StartsWith("MSys"))
{
Console.WriteLine($"表名: {tableName}");
// 获取表的列信息
var columnRestrictions = new string?[4];
columnRestrictions[2] = tableName;
var columns = connection.GetSchema("Columns", columnRestrictions);
Console.WriteLine($" 列数: {columns.Rows.Count}");
foreach (System.Data.DataRow colRow in columns.Rows)
{
var columnName = colRow["COLUMN_NAME"]?.ToString();
var dataType = colRow["DATA_TYPE"]?.ToString();
Console.WriteLine($" - {columnName} ({GetDataTypeName(dataType)})");
}
Console.WriteLine();
}
}
}
[TestMethod]
public async Task CanQueryDatabaseUsingDataContextBuilder()
{
var builder = new DataContextBuilder<TestDbContext>();
builder.UseAccessDatabase(options =>
{
options.DatabaseFile = DatabaseFile;
options.CopyTemplateWhenMissing = false;
options.ThrowIfMissing = true;
});
var dataContext = builder.Build();
// 尝试连接数据库
var dbContext = dataContext.Context;
var canConnect = await dbContext.Database.CanConnectAsync();
Assert.IsTrue(canConnect, "应该能够连接到数据库");
Console.WriteLine("成功使用 DataContextBuilder 连接到 Access 数据库!");
}
private static string GetDataTypeName(string? dataType)
{
return dataType switch
{
"130" => "nvarchar",
"3" => "int",
"7" => "datetime",
"11" => "bit",
"5" => "double",
"4" => "float",
"2" => "smallint",
"6" => "currency",
"128" => "binary",
"205" => "image",
_ => $"unknown({dataType})"
};
}
}
/// <summary>
/// 测试用的 DbContext
/// </summary>
public class TestDbContext : DbContext
{
public TestDbContext(DbContextOptions<TestDbContext> options) : base(options)
{
}
}
Loaded 100 of 102 files, more files were not shown because too many files have changed in this diff. Show more