Compare commits
5
Commits
479eae64a1
...
my-main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1d059ed29 | ||
|
|
2739a71134 | ||
|
|
dd993e5b45 | ||
|
|
e334e65c2d | ||
|
|
1d412bba33 |
No files matched your search
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -10,3 +10,7 @@
|
|||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
</Application.Resources>
|
</Application.Resources>
|
||||||
</Application>
|
</Application>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -2,7 +2,9 @@ using System;
|
|||||||
using System.Windows;
|
using System.Windows;
|
||||||
using Client.Services;
|
using Client.Services;
|
||||||
using Client.ViewModels;
|
using Client.ViewModels;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Common;
|
using Common;
|
||||||
using Config;
|
using Config;
|
||||||
@@ -10,6 +12,7 @@ using NetClient.DependencyInjection;
|
|||||||
using Sharp.Infrastructure.Configuration;
|
using Sharp.Infrastructure.Configuration;
|
||||||
using Sharp.Log.Extensions;
|
using Sharp.Log.Extensions;
|
||||||
using Sharp.NetServer.DependencyInjection;
|
using Sharp.NetServer.DependencyInjection;
|
||||||
|
using Sharp.Wpf.Exceptions;
|
||||||
using Sharp.Wpf.Services;
|
using Sharp.Wpf.Services;
|
||||||
|
|
||||||
namespace Client
|
namespace Client
|
||||||
@@ -33,6 +36,7 @@ namespace Client
|
|||||||
|
|
||||||
// ConfigManager and typed SystemConfig accessor
|
// ConfigManager and typed SystemConfig accessor
|
||||||
services.AddSingleton(cm);
|
services.AddSingleton(cm);
|
||||||
|
services.TryAddSingleton<IConfiguration>(cm.Configuration); // Register IConfiguration for BindConfiguration
|
||||||
services.AddSystemConfig(cm);
|
services.AddSystemConfig(cm);
|
||||||
|
|
||||||
// Logging (binds from section "SharpLog")
|
// Logging (binds from section "SharpLog")
|
||||||
@@ -42,6 +46,9 @@ namespace Client
|
|||||||
services.AddNetClient(cm);
|
services.AddNetClient(cm);
|
||||||
services.AddNetServer(cm);
|
services.AddNetServer(cm);
|
||||||
|
|
||||||
|
// WPF exception handler
|
||||||
|
services.AddSingleton<IUiExceptionHandler, UiExceptionHandler>();
|
||||||
|
|
||||||
// WPF app services
|
// WPF app services
|
||||||
services.AddSingleton<IDialogService, DialogService>();
|
services.AddSingleton<IDialogService, DialogService>();
|
||||||
services.AddSingleton<IAuthenticationService, DemoAuthenticationService>();
|
services.AddSingleton<IAuthenticationService, DemoAuthenticationService>();
|
||||||
@@ -54,22 +61,32 @@ namespace Client
|
|||||||
|
|
||||||
_serviceProvider = services.BuildServiceProvider();
|
_serviceProvider = services.BuildServiceProvider();
|
||||||
|
|
||||||
// 3) Register global facades for convenience
|
// 3) Activate global WPF exception handler
|
||||||
|
var exceptionHandler = _serviceProvider.GetRequiredService<IUiExceptionHandler>();
|
||||||
|
exceptionHandler.Register();
|
||||||
|
|
||||||
|
// 4) Register global facades for convenience
|
||||||
Global.RegisterSingleton(_serviceProvider);
|
Global.RegisterSingleton(_serviceProvider);
|
||||||
Global.RegisterSingleton(cm);
|
Global.RegisterSingleton(cm);
|
||||||
Global.RegisterSingleton(_serviceProvider.GetRequiredService<ISystemConfigAccessor>());
|
Global.RegisterSingleton(_serviceProvider.GetRequiredService<ISystemConfigAccessor>());
|
||||||
|
|
||||||
// 4) Show login window first, then main shell if authenticated
|
// 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 loginWindow = _serviceProvider.GetRequiredService<LoginWindow>();
|
||||||
var loginResult = loginWindow.ShowDialog();
|
var loginResult = loginWindow.ShowDialog();
|
||||||
|
|
||||||
|
logger.LogInformation("Login window closed with result: {LoginResult}", loginResult);
|
||||||
|
|
||||||
if (loginResult == true)
|
if (loginResult == true)
|
||||||
{
|
{
|
||||||
|
logger.LogInformation("Login successful, showing main window");
|
||||||
var main = _serviceProvider.GetRequiredService<MainWindow>();
|
var main = _serviceProvider.GetRequiredService<MainWindow>();
|
||||||
main.Show();
|
main.Show();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
logger.LogInformation("Login cancelled or failed, shutting down application");
|
||||||
Shutdown();
|
Shutdown();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,27 @@
|
|||||||
using System;
|
using System;
|
||||||
using HandyControl.Controls;
|
using HandyControl.Controls;
|
||||||
using Client.ViewModels;
|
using Client.ViewModels;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Client;
|
namespace Client;
|
||||||
|
|
||||||
public partial class LoginWindow : GlowWindow
|
public partial class LoginWindow : GlowWindow
|
||||||
{
|
{
|
||||||
private readonly LoginViewModel _viewModel;
|
private readonly LoginViewModel _viewModel;
|
||||||
|
private readonly ILogger<LoginWindow>? _logger;
|
||||||
|
|
||||||
public LoginWindow(LoginViewModel viewModel)
|
public LoginWindow(LoginViewModel viewModel, ILogger<LoginWindow>? logger = null)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
_viewModel = viewModel;
|
_viewModel = viewModel;
|
||||||
|
_logger = logger;
|
||||||
DataContext = viewModel;
|
DataContext = viewModel;
|
||||||
_viewModel.LoginSucceeded += HandleLoginSucceeded;
|
_viewModel.LoginSucceeded += HandleLoginSucceeded;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleLoginSucceeded(object? sender, EventArgs e)
|
private void HandleLoginSucceeded(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
|
_logger?.LogInformation("HandleLoginSucceeded called, setting DialogResult = true");
|
||||||
DialogResult = true;
|
DialogResult = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Client.Services;
|
using Client.Services;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Sharp.Wpf.Commands;
|
using Sharp.Wpf.Commands;
|
||||||
using Sharp.Wpf.Mvvm;
|
using Sharp.Wpf.Mvvm;
|
||||||
|
|
||||||
@@ -10,6 +11,7 @@ public sealed class LoginViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
private readonly IAuthenticationService _authenticationService;
|
private readonly IAuthenticationService _authenticationService;
|
||||||
private readonly IUserSession _userSession;
|
private readonly IUserSession _userSession;
|
||||||
|
private readonly ILogger<LoginViewModel> _logger;
|
||||||
private readonly IAsyncCommand _loginCommand;
|
private readonly IAsyncCommand _loginCommand;
|
||||||
|
|
||||||
private string _userName = "admin";
|
private string _userName = "admin";
|
||||||
@@ -18,10 +20,11 @@ public sealed class LoginViewModel : ViewModelBase
|
|||||||
|
|
||||||
public event EventHandler? LoginSucceeded;
|
public event EventHandler? LoginSucceeded;
|
||||||
|
|
||||||
public LoginViewModel(IAuthenticationService authenticationService, IUserSession userSession)
|
public LoginViewModel(IAuthenticationService authenticationService, IUserSession userSession, ILogger<LoginViewModel> logger)
|
||||||
{
|
{
|
||||||
_authenticationService = authenticationService;
|
_authenticationService = authenticationService;
|
||||||
_userSession = userSession;
|
_userSession = userSession;
|
||||||
|
_logger = logger;
|
||||||
_loginCommand = new AsyncCommand(LoginAsync);
|
_loginCommand = new AsyncCommand(LoginAsync);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,20 +70,29 @@ public sealed class LoginViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
_logger.LogInformation("Login attempt for user: {UserName}", UserName);
|
||||||
IsBusy = true;
|
IsBusy = true;
|
||||||
ErrorMessage = null;
|
ErrorMessage = null;
|
||||||
|
|
||||||
var result = await _authenticationService.LoginAsync(UserName, Password).ConfigureAwait(false);
|
var result = await _authenticationService.LoginAsync(UserName, Password);
|
||||||
|
|
||||||
if (!result.Succeeded)
|
if (!result.Succeeded)
|
||||||
{
|
{
|
||||||
|
_logger.LogWarning("Login failed for user: {UserName}, Error: {Error}", UserName, result.Error);
|
||||||
ErrorMessage = result.Error;
|
ErrorMessage = result.Error;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Login succeeded for user: {UserName}", UserName);
|
||||||
_userSession.SignIn(UserName, result.DisplayName);
|
_userSession.SignIn(UserName, result.DisplayName);
|
||||||
|
_logger.LogInformation("Invoking LoginSucceeded event");
|
||||||
LoginSucceeded?.Invoke(this, EventArgs.Empty);
|
LoginSucceeded?.Invoke(this, EventArgs.Empty);
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Login failed for user: {UserName}", UserName);
|
||||||
|
ErrorMessage = "An unexpected error occurred.";
|
||||||
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
IsBusy = false;
|
IsBusy = false;
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// <auto-generated>
|
// <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>
|
// </auto-generated>
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
@@ -13,7 +14,7 @@ using System.Reflection;
|
|||||||
[assembly: System.Reflection.AssemblyCompanyAttribute("Client")]
|
[assembly: System.Reflection.AssemblyCompanyAttribute("Client")]
|
||||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+53d1dd2dbf05d818528814223b58610a790fcb26")]
|
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+f7660c49e81d5fe2747f6514545ceb961c2066d3")]
|
||||||
[assembly: System.Reflection.AssemblyProductAttribute("Client")]
|
[assembly: System.Reflection.AssemblyProductAttribute("Client")]
|
||||||
[assembly: System.Reflection.AssemblyTitleAttribute("Client")]
|
[assembly: System.Reflection.AssemblyTitleAttribute("Client")]
|
||||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
f8321da7c6a777fc7770c7234548af806317c95cd5f1beb1c1743888aa1bde11
|
eeea53cf7db257ee716efdd2fdcdb5f1a810b49927811d410a05e1ff19899369
|
||||||
@@ -0,0 +1,503 @@
|
|||||||
|
using System.Data;
|
||||||
|
using System.Data.OleDb;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Sharp.Data.Access.Connections;
|
||||||
|
using Sharp.Data.Access.Initialization;
|
||||||
|
using Sharp.Data.Access.Internal;
|
||||||
|
using Sharp.Data.Access.Options;
|
||||||
|
using Sharp.Data.Access.Security;
|
||||||
|
|
||||||
|
namespace Sharp.Data.Access;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Access 数据库事务实现
|
||||||
|
/// </summary>
|
||||||
|
internal class AccessTransaction : IAccessTransaction
|
||||||
|
{
|
||||||
|
private readonly OleDbConnection _connection;
|
||||||
|
private readonly OleDbTransaction _transaction;
|
||||||
|
private readonly ILogger? _logger;
|
||||||
|
private readonly bool _ownsConnection;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public AccessTransaction(OleDbConnection connection, bool ownsConnection, ILogger? logger)
|
||||||
|
{
|
||||||
|
_connection = connection;
|
||||||
|
_ownsConnection = ownsConnection;
|
||||||
|
_transaction = connection.BeginTransaction();
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Execute(string sql, params OleDbParameter[] parameters)
|
||||||
|
{
|
||||||
|
using var command = new OleDbCommand(sql, _connection, _transaction);
|
||||||
|
if (parameters.Length > 0)
|
||||||
|
command.Parameters.AddRange(parameters);
|
||||||
|
|
||||||
|
return command.ExecuteNonQuery();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<T> Query<T>(string sql, params OleDbParameter[] parameters) where T : new()
|
||||||
|
{
|
||||||
|
using var command = new OleDbCommand(sql, _connection, _transaction);
|
||||||
|
if (parameters.Length > 0)
|
||||||
|
command.Parameters.AddRange(parameters);
|
||||||
|
|
||||||
|
var results = new List<T>();
|
||||||
|
using var reader = command.ExecuteReader();
|
||||||
|
|
||||||
|
while (reader.Read())
|
||||||
|
{
|
||||||
|
var item = MapReaderToObject<T>(reader);
|
||||||
|
results.Add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
public object? ExecuteScalar(string sql, params OleDbParameter[] parameters)
|
||||||
|
{
|
||||||
|
using var command = new OleDbCommand(sql, _connection, _transaction);
|
||||||
|
if (parameters.Length > 0)
|
||||||
|
command.Parameters.AddRange(parameters);
|
||||||
|
|
||||||
|
return command.ExecuteScalar();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Commit()
|
||||||
|
{
|
||||||
|
_transaction.Commit();
|
||||||
|
_logger?.LogDebug("事务已提交");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Rollback()
|
||||||
|
{
|
||||||
|
_transaction.Rollback();
|
||||||
|
_logger?.LogDebug("事务已回滚");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_transaction.Dispose();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_ownsConnection)
|
||||||
|
{
|
||||||
|
_connection.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static T MapReaderToObject<T>(IDataReader reader) where T : new()
|
||||||
|
{
|
||||||
|
var obj = new T();
|
||||||
|
var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
|
||||||
|
|
||||||
|
// 创建字段名到索引的映射(大小写不敏感)
|
||||||
|
var fieldMap = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
for (int i = 0; i < reader.FieldCount; i++)
|
||||||
|
{
|
||||||
|
var fieldName = reader.GetName(i);
|
||||||
|
fieldMap[fieldName] = i;
|
||||||
|
|
||||||
|
// Access 列名有时包含末尾空格,提前做一次修剪以便映射
|
||||||
|
var trimmed = fieldName.Trim();
|
||||||
|
if (!fieldMap.ContainsKey(trimmed))
|
||||||
|
{
|
||||||
|
fieldMap[trimmed] = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var property in properties)
|
||||||
|
{
|
||||||
|
if (!property.CanWrite) continue;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 尝试查找匹配的字段(大小写不敏感)
|
||||||
|
if (fieldMap.TryGetValue(property.Name, out int ordinal))
|
||||||
|
{
|
||||||
|
var value = reader.GetValue(ordinal);
|
||||||
|
|
||||||
|
if (value != DBNull.Value && value != null)
|
||||||
|
{
|
||||||
|
// 尝试类型转换
|
||||||
|
if (property.PropertyType == value.GetType())
|
||||||
|
{
|
||||||
|
property.SetValue(obj, value);
|
||||||
|
}
|
||||||
|
else if (property.PropertyType == typeof(string))
|
||||||
|
{
|
||||||
|
property.SetValue(obj, value.ToString());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 尝试使用 Convert.ChangeType
|
||||||
|
var convertedValue = Convert.ChangeType(value, property.PropertyType);
|
||||||
|
property.SetValue(obj, convertedValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// 字段不存在或类型转换失败,保持默认值
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Access 数据服务实现(基于 System.Data.OleDb)
|
||||||
|
/// </summary>
|
||||||
|
public class AccessDataService : IAccessDataService
|
||||||
|
{
|
||||||
|
private readonly AccessDatabaseOptions _options;
|
||||||
|
private readonly IAccessConnectionStringFactory _connectionFactory;
|
||||||
|
private readonly IAccessDatabaseInitializer _initializer;
|
||||||
|
private readonly IAccessPasswordDecryptor _decryptor;
|
||||||
|
private readonly ILogger<AccessDataService>? _logger;
|
||||||
|
private readonly string _connectionString;
|
||||||
|
|
||||||
|
public AccessDataService(
|
||||||
|
AccessDatabaseOptions options,
|
||||||
|
IAccessConnectionStringFactory? connectionFactory = null,
|
||||||
|
IAccessDatabaseInitializer? initializer = null,
|
||||||
|
IAccessPasswordDecryptor? decryptor = null,
|
||||||
|
ILogger<AccessDataService>? logger = null)
|
||||||
|
{
|
||||||
|
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||||
|
_options.Validate();
|
||||||
|
|
||||||
|
_connectionFactory = connectionFactory ?? new AccessConnectionStringFactory(decryptor ?? new PassthroughAccessPasswordDecryptor());
|
||||||
|
_initializer = initializer ?? new AccessDatabaseInitializer();
|
||||||
|
_decryptor = decryptor ?? new PassthroughAccessPasswordDecryptor();
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 确保数据库存在(如果配置了模板复制)
|
||||||
|
_initializer.EnsureDatabase(_options, NullServiceProvider.Instance);
|
||||||
|
|
||||||
|
// 构建连接字符串
|
||||||
|
_connectionString = _connectionFactory.Create(_options, NullServiceProvider.Instance);
|
||||||
|
|
||||||
|
_logger?.LogInformation("AccessDataService 已初始化,数据库: {DatabaseFile}, Provider: {Provider}",
|
||||||
|
_options.DatabaseFile, _options.ProviderName);
|
||||||
|
_logger?.LogDebug("实际连接字符串: {ConnectionString}", _connectionString);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger?.LogError(ex, "AccessDataService 初始化失败");
|
||||||
|
|
||||||
|
// 提供详细的错误诊断信息
|
||||||
|
if (ex.Message.Contains("找不到") || ex is FileNotFoundException)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"找不到 Access 数据库文件: {_options.DatabaseFile}。" +
|
||||||
|
$"请确保文件存在或配置 TemplateFile 以自动创建。", ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<T> Query<T>(string sql, params OleDbParameter[] parameters) where T : new()
|
||||||
|
{
|
||||||
|
_logger?.LogDebug("执行查询: {Sql}", sql);
|
||||||
|
|
||||||
|
return ExecuteWithRetry(() =>
|
||||||
|
{
|
||||||
|
using var connection = CreateConnection();
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
using var command = new OleDbCommand(sql, connection);
|
||||||
|
if (parameters.Length > 0)
|
||||||
|
{
|
||||||
|
// 克隆参数以避免重试时的"已包含在另一个集合中"错误
|
||||||
|
var clonedParams = parameters.Select(p => CloneParameter(p)).ToArray();
|
||||||
|
command.Parameters.AddRange(clonedParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
var results = new List<T>();
|
||||||
|
using var reader = command.ExecuteReader();
|
||||||
|
|
||||||
|
while (reader.Read())
|
||||||
|
{
|
||||||
|
var item = MapReaderToObject<T>(reader);
|
||||||
|
results.Add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger?.LogDebug("查询返回 {Count} 条记录", results.Count);
|
||||||
|
return results;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public T? QueryFirstOrDefault<T>(string sql, params OleDbParameter[] parameters) where T : new()
|
||||||
|
{
|
||||||
|
_logger?.LogDebug("执行单条查询: {Sql}", sql);
|
||||||
|
|
||||||
|
return ExecuteWithRetry(() =>
|
||||||
|
{
|
||||||
|
using var connection = CreateConnection();
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
using var command = new OleDbCommand(sql, connection);
|
||||||
|
if (parameters.Length > 0)
|
||||||
|
{
|
||||||
|
// 克隆参数以避免重试时的"已包含在另一个集合中"错误
|
||||||
|
var clonedParams = parameters.Select(p => CloneParameter(p)).ToArray();
|
||||||
|
command.Parameters.AddRange(clonedParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
using var reader = command.ExecuteReader();
|
||||||
|
|
||||||
|
if (reader.Read())
|
||||||
|
{
|
||||||
|
return MapReaderToObject<T>(reader);
|
||||||
|
}
|
||||||
|
|
||||||
|
return default;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public object? ExecuteScalar(string sql, params OleDbParameter[] parameters)
|
||||||
|
{
|
||||||
|
_logger?.LogDebug("执行标量查询: {Sql}", sql);
|
||||||
|
|
||||||
|
return ExecuteWithRetry(() =>
|
||||||
|
{
|
||||||
|
using var connection = CreateConnection();
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
using var command = new OleDbCommand(sql, connection);
|
||||||
|
if (parameters.Length > 0)
|
||||||
|
{
|
||||||
|
// 克隆参数以避免重试时的"已包含在另一个集合中"错误
|
||||||
|
var clonedParams = parameters.Select(p => CloneParameter(p)).ToArray();
|
||||||
|
command.Parameters.AddRange(clonedParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = command.ExecuteScalar();
|
||||||
|
_logger?.LogDebug("标量查询返回: {Result}", result);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Execute(string sql, params OleDbParameter[] parameters)
|
||||||
|
{
|
||||||
|
_logger?.LogDebug("执行命令: {Sql}", sql);
|
||||||
|
|
||||||
|
return ExecuteWithRetry(() =>
|
||||||
|
{
|
||||||
|
using var connection = CreateConnection();
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
using var command = new OleDbCommand(sql, connection);
|
||||||
|
if (parameters.Length > 0)
|
||||||
|
{
|
||||||
|
// 克隆参数以避免重试时的"已包含在另一个集合中"错误
|
||||||
|
var clonedParams = parameters.Select(p => CloneParameter(p)).ToArray();
|
||||||
|
command.Parameters.AddRange(clonedParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
var rowsAffected = command.ExecuteNonQuery();
|
||||||
|
_logger?.LogDebug("命令影响了 {RowsAffected} 行", rowsAffected);
|
||||||
|
return rowsAffected;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public IAccessTransaction BeginTransaction()
|
||||||
|
{
|
||||||
|
_logger?.LogDebug("开始事务");
|
||||||
|
var connection = CreateConnection();
|
||||||
|
connection.Open();
|
||||||
|
return new AccessTransaction(connection, ownsConnection: true, _logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ExecuteInTransaction(Action<IAccessTransaction> action)
|
||||||
|
{
|
||||||
|
_logger?.LogDebug("在事务中执行操作");
|
||||||
|
|
||||||
|
using var transaction = BeginTransaction();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
action(transaction);
|
||||||
|
transaction.Commit();
|
||||||
|
_logger?.LogDebug("事务执行成功");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger?.LogError(ex, "事务执行失败,正在回滚");
|
||||||
|
transaction.Rollback();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TestConnection()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger?.LogDebug("测试数据库连接");
|
||||||
|
using var connection = CreateConnection();
|
||||||
|
connection.Open();
|
||||||
|
_logger?.LogDebug("数据库连接成功");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger?.LogError(ex, "数据库连接失败");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private OleDbConnection CreateConnection()
|
||||||
|
{
|
||||||
|
return new OleDbConnection(_connectionString);
|
||||||
|
}
|
||||||
|
|
||||||
|
private T ExecuteWithRetry<T>(Func<T> operation)
|
||||||
|
{
|
||||||
|
var maxRetries = _options.MaxRetryCount;
|
||||||
|
var retryDelay = _options.RetryDelayMilliseconds;
|
||||||
|
|
||||||
|
for (int attempt = 0; attempt <= maxRetries; attempt++)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return operation();
|
||||||
|
}
|
||||||
|
catch (OleDbException ex) when (attempt < maxRetries && IsRetriableError(ex))
|
||||||
|
{
|
||||||
|
_logger?.LogWarning(ex, "数据库操作失败,正在重试 ({Attempt}/{MaxRetries})...", attempt + 1, maxRetries);
|
||||||
|
Thread.Sleep(retryDelay);
|
||||||
|
}
|
||||||
|
catch (OleDbException ex)
|
||||||
|
{
|
||||||
|
// 提供详细的错误诊断信息
|
||||||
|
var errorMessage = $"Access 数据库操作失败: {ex.Message}";
|
||||||
|
|
||||||
|
// 检查是否是 Provider 相关的错误
|
||||||
|
if (ex.Message.Contains("找不到") || ex.Message.Contains("未注册") ||
|
||||||
|
ex.Message.Contains("OLE DB") || ex.Message.Contains("多步"))
|
||||||
|
{
|
||||||
|
errorMessage += $"\n\n可能的原因:" +
|
||||||
|
$"\n1. 未安装 Access Database Engine:当前使用 Provider = {_options.ProviderName}" +
|
||||||
|
$"\n - 如果使用 16.0,需要安装 Access Database Engine 2016 或 Office 2016+" +
|
||||||
|
$"\n - 如果使用 12.0,需要安装 Access Database Engine 2010 或 Office 2010+" +
|
||||||
|
$"\n - 建议:优先使用 12.0 版本,兼容性最好" +
|
||||||
|
$"\n2. 32位/64位不匹配:进程架构必须与 Access Driver 架构一致" +
|
||||||
|
$"\n3. 数据库文件损坏或被其他程序锁定" +
|
||||||
|
$"\n4. 数据库文件路径不正确:{_options.DatabaseFile}";
|
||||||
|
|
||||||
|
_logger?.LogError(ex, "数据库连接失败,Provider: {Provider}", _options.ProviderName);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException(errorMessage, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 不会到这里,因为最后一次尝试会抛出异常
|
||||||
|
throw new InvalidOperationException("重试逻辑错误");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsRetriableError(OleDbException ex)
|
||||||
|
{
|
||||||
|
// Access 数据库常见的可重试错误
|
||||||
|
// -2147467259: 数据库被锁定
|
||||||
|
// -2147217865: 无法打开数据库(可能正在被其他进程使用)
|
||||||
|
return ex.ErrorCode == -2147467259 || ex.ErrorCode == -2147217865;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 克隆 OleDbParameter 以避免重试时参数被重复添加的问题
|
||||||
|
/// </summary>
|
||||||
|
private static OleDbParameter CloneParameter(OleDbParameter original)
|
||||||
|
{
|
||||||
|
var clone = new OleDbParameter
|
||||||
|
{
|
||||||
|
ParameterName = original.ParameterName,
|
||||||
|
Value = original.Value,
|
||||||
|
OleDbType = original.OleDbType,
|
||||||
|
Size = original.Size,
|
||||||
|
Direction = original.Direction,
|
||||||
|
IsNullable = original.IsNullable,
|
||||||
|
Precision = original.Precision,
|
||||||
|
Scale = original.Scale,
|
||||||
|
SourceColumn = original.SourceColumn,
|
||||||
|
SourceVersion = original.SourceVersion
|
||||||
|
};
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static T MapReaderToObject<T>(IDataReader reader) where T : new()
|
||||||
|
{
|
||||||
|
var obj = new T();
|
||||||
|
var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
|
||||||
|
|
||||||
|
// 创建字段名到索引的映射(大小写不敏感)
|
||||||
|
var fieldMap = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
for (int i = 0; i < reader.FieldCount; i++)
|
||||||
|
{
|
||||||
|
var fieldName = reader.GetName(i);
|
||||||
|
fieldMap[fieldName] = i;
|
||||||
|
|
||||||
|
// Access 列名有时包含末尾空格,提前做一次修剪以便映射
|
||||||
|
var trimmed = fieldName.Trim();
|
||||||
|
if (!fieldMap.ContainsKey(trimmed))
|
||||||
|
{
|
||||||
|
fieldMap[trimmed] = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var property in properties)
|
||||||
|
{
|
||||||
|
if (!property.CanWrite) continue;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 尝试查找匹配的字段(大小写不敏感)
|
||||||
|
if (fieldMap.TryGetValue(property.Name, out int ordinal))
|
||||||
|
{
|
||||||
|
var value = reader.GetValue(ordinal);
|
||||||
|
|
||||||
|
if (value != DBNull.Value && value != null)
|
||||||
|
{
|
||||||
|
// 尝试类型转换
|
||||||
|
if (property.PropertyType == value.GetType())
|
||||||
|
{
|
||||||
|
property.SetValue(obj, value);
|
||||||
|
}
|
||||||
|
else if (property.PropertyType == typeof(string))
|
||||||
|
{
|
||||||
|
property.SetValue(obj, value.ToString());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 尝试使用 Convert.ChangeType
|
||||||
|
var convertedValue = Convert.ChangeType(value, property.PropertyType);
|
||||||
|
property.SetValue(obj, convertedValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// 字段不存在或类型转换失败,保持默认值
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
using Sharp.Data.Access.Options;
|
||||||
|
using Sharp.Data.Access.Security;
|
||||||
|
using Sharp.Data.Access.Connections;
|
||||||
|
using Sharp.Data.Access.Initialization;
|
||||||
|
|
||||||
|
namespace Sharp.Data.Access;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Access 数据服务的依赖注入扩展
|
||||||
|
/// </summary>
|
||||||
|
public static class AccessDataServiceExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 添加 Access 数据服务(最简单方式 - 仅指定数据库路径)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="services">服务集合</param>
|
||||||
|
/// <param name="databasePath">数据库文件路径(绝对或相对路径)</param>
|
||||||
|
/// <param name="password">数据库密码(可选)</param>
|
||||||
|
/// <param name="provider">OLE DB 提供程序,默认:Microsoft.ACE.OLEDB.16.0</param>
|
||||||
|
/// <returns>服务集合</returns>
|
||||||
|
public static IServiceCollection AddAccessDataService(
|
||||||
|
this IServiceCollection services,
|
||||||
|
string databasePath,
|
||||||
|
string? password = null,
|
||||||
|
string? provider = null)
|
||||||
|
{
|
||||||
|
return services.AddAccessDataService(options =>
|
||||||
|
{
|
||||||
|
options.DatabaseFile = databasePath;
|
||||||
|
if (!string.IsNullOrEmpty(password))
|
||||||
|
options.Password = password;
|
||||||
|
if (!string.IsNullOrEmpty(provider))
|
||||||
|
options.ProviderName = provider;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 添加 Access 数据服务(使用配置委托)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="services">服务集合</param>
|
||||||
|
/// <param name="configureOptions">配置委托</param>
|
||||||
|
/// <returns>服务集合</returns>
|
||||||
|
public static IServiceCollection AddAccessDataService(
|
||||||
|
this IServiceCollection services,
|
||||||
|
Action<AccessDatabaseOptions> configureOptions)
|
||||||
|
{
|
||||||
|
if (services == null) throw new ArgumentNullException(nameof(services));
|
||||||
|
if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions));
|
||||||
|
|
||||||
|
var options = new AccessDatabaseOptions();
|
||||||
|
configureOptions(options);
|
||||||
|
options.Validate();
|
||||||
|
|
||||||
|
// 注册核心服务
|
||||||
|
services.TryAddSingleton<IAccessPasswordDecryptor, PassthroughAccessPasswordDecryptor>();
|
||||||
|
services.TryAddSingleton<IAccessConnectionStringFactory>(sp =>
|
||||||
|
new AccessConnectionStringFactory(sp.GetRequiredService<IAccessPasswordDecryptor>()));
|
||||||
|
services.TryAddSingleton<IAccessDatabaseInitializer, AccessDatabaseInitializer>();
|
||||||
|
|
||||||
|
// 注册配置
|
||||||
|
services.AddSingleton(options);
|
||||||
|
|
||||||
|
// 注册 AccessDataService
|
||||||
|
services.AddSingleton<IAccessDataService>(sp =>
|
||||||
|
new AccessDataService(
|
||||||
|
options,
|
||||||
|
sp.GetRequiredService<IAccessConnectionStringFactory>(),
|
||||||
|
sp.GetRequiredService<IAccessDatabaseInitializer>(),
|
||||||
|
sp.GetRequiredService<IAccessPasswordDecryptor>(),
|
||||||
|
sp.GetService<Microsoft.Extensions.Logging.ILogger<AccessDataService>>()));
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 添加 Access 数据服务(使用配置对象)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="services">服务集合</param>
|
||||||
|
/// <param name="options">配置选项</param>
|
||||||
|
/// <returns>服务集合</returns>
|
||||||
|
public static IServiceCollection AddAccessDataService(
|
||||||
|
this IServiceCollection services,
|
||||||
|
AccessDatabaseOptions options)
|
||||||
|
{
|
||||||
|
if (options == null) throw new ArgumentNullException(nameof(options));
|
||||||
|
|
||||||
|
return services.AddAccessDataService(_ => { });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 注册自定义密码解密器
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TDecryptor">解密器类型</typeparam>
|
||||||
|
/// <param name="services">服务集合</param>
|
||||||
|
/// <returns>服务集合</returns>
|
||||||
|
public static IServiceCollection AddAccessPasswordDecryptor<TDecryptor>(this IServiceCollection services)
|
||||||
|
where TDecryptor : class, IAccessPasswordDecryptor
|
||||||
|
{
|
||||||
|
services.AddSingleton<IAccessPasswordDecryptor, TDecryptor>();
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 注册自定义密码解密器(使用工厂方法)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="services">服务集合</param>
|
||||||
|
/// <param name="implementationFactory">解密器工厂</param>
|
||||||
|
/// <returns>服务集合</returns>
|
||||||
|
public static IServiceCollection AddAccessPasswordDecryptor(
|
||||||
|
this IServiceCollection services,
|
||||||
|
Func<IServiceProvider, IAccessPasswordDecryptor> implementationFactory)
|
||||||
|
{
|
||||||
|
services.AddSingleton(implementationFactory);
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
using System;
|
||||||
|
using System.Data.OleDb;
|
||||||
|
using Sharp.Data.Access.Internal;
|
||||||
|
using Sharp.Data.Access.Options;
|
||||||
|
using Sharp.Data.Access.Security;
|
||||||
|
|
||||||
|
namespace Sharp.Data.Access.Connections;
|
||||||
|
|
||||||
|
internal sealed class AccessConnectionStringFactory : IAccessConnectionStringFactory
|
||||||
|
{
|
||||||
|
private readonly IAccessPasswordDecryptor _decryptor;
|
||||||
|
|
||||||
|
public AccessConnectionStringFactory(IAccessPasswordDecryptor decryptor)
|
||||||
|
{
|
||||||
|
_decryptor = decryptor ?? throw new ArgumentNullException(nameof(decryptor));
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Create(AccessDatabaseOptions options, IServiceProvider services)
|
||||||
|
{
|
||||||
|
var configuration = AccessDatabaseConfiguration.FromOptions(options, services);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(configuration.ConnectionString))
|
||||||
|
{
|
||||||
|
return configuration.ConnectionString;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(configuration.DatabaseFile))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Access 数据库需要提供 DatabaseFile 或 ConnectionString。");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 基础连接字符串
|
||||||
|
// 注意:Connect Timeout 参数在某些 ACE 版本中不稳定,已移除
|
||||||
|
var builder = new OleDbConnectionStringBuilder
|
||||||
|
{
|
||||||
|
["Provider"] = configuration.ProviderName,
|
||||||
|
["Data Source"] = AccessPathResolver.Resolve(configuration.DatabaseFile, configuration.DataDirectory)
|
||||||
|
};
|
||||||
|
|
||||||
|
var password = configuration.Password;
|
||||||
|
if (string.IsNullOrEmpty(password) && !string.IsNullOrWhiteSpace(configuration.EncryptedPassword))
|
||||||
|
{
|
||||||
|
password = _decryptor.Decrypt(configuration.EncryptedPassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(password))
|
||||||
|
{
|
||||||
|
builder["Jet OLEDB:Database Password"] = password;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var parameter in configuration.AdditionalParameters)
|
||||||
|
{
|
||||||
|
builder[parameter.Key] = parameter.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.ConnectionString;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using System;
|
||||||
|
using Sharp.Data.Access.Options;
|
||||||
|
|
||||||
|
namespace Sharp.Data.Access.Connections;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构建 Access 连接字符串的工厂。
|
||||||
|
/// </summary>
|
||||||
|
public interface IAccessConnectionStringFactory
|
||||||
|
{
|
||||||
|
string Create(AccessDatabaseOptions options, IServiceProvider services);
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<RootNamespace>Sharp.Data.Access</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="EntityFrameworkCore.Jet" Version="8.0.0-preview1" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
|
||||||
|
<PackageReference Include="System.Data.OleDb" Version="8.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Data\Data.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
using Sharp.Data.Access.Connections;
|
||||||
|
using Sharp.Data.Access.Initialization;
|
||||||
|
using Sharp.Data.Access.Internal;
|
||||||
|
using Sharp.Data.Access.Options;
|
||||||
|
using Sharp.Data.Access.Security;
|
||||||
|
using Sharp.Data.Context;
|
||||||
|
|
||||||
|
namespace Sharp.Data.Access.DependencyInjection;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Access 模块的依赖注入扩展。
|
||||||
|
/// </summary>
|
||||||
|
public static class AccessServiceCollectionExtensions
|
||||||
|
{
|
||||||
|
public static IServiceCollection AddAccessDataContext<TContext>(this IServiceCollection services,
|
||||||
|
Action<AccessContextOptions<TContext>> configure,
|
||||||
|
bool registerDataContext = true,
|
||||||
|
ServiceLifetime dataContextLifetime = ServiceLifetime.Scoped)
|
||||||
|
where TContext : DbContext
|
||||||
|
{
|
||||||
|
if (configure == null) throw new ArgumentNullException(nameof(configure));
|
||||||
|
|
||||||
|
return services.AddAccessDataContext(_ =>
|
||||||
|
{
|
||||||
|
var options = new AccessContextOptions<TContext>();
|
||||||
|
configure(options);
|
||||||
|
return options;
|
||||||
|
}, registerDataContext, dataContextLifetime);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IServiceCollection AddAccessDataContext<TContext>(this IServiceCollection services,
|
||||||
|
Func<IServiceProvider, AccessContextOptions<TContext>> configure,
|
||||||
|
bool registerDataContext = true,
|
||||||
|
ServiceLifetime dataContextLifetime = ServiceLifetime.Scoped)
|
||||||
|
where TContext : DbContext
|
||||||
|
{
|
||||||
|
if (services == null) throw new ArgumentNullException(nameof(services));
|
||||||
|
if (configure == null) throw new ArgumentNullException(nameof(configure));
|
||||||
|
|
||||||
|
services.TryAddSingleton<IAccessPasswordDecryptor, PassthroughAccessPasswordDecryptor>();
|
||||||
|
services.TryAddSingleton<IAccessConnectionStringFactory>(sp =>
|
||||||
|
new AccessConnectionStringFactory(sp.GetRequiredService<IAccessPasswordDecryptor>()));
|
||||||
|
services.TryAddSingleton<IAccessDatabaseInitializer, AccessDatabaseInitializer>();
|
||||||
|
|
||||||
|
services.TryAddSingleton(provider =>
|
||||||
|
{
|
||||||
|
var options = configure(provider) ?? throw new InvalidOperationException("AccessContextOptions 不能为空。");
|
||||||
|
options.Validate();
|
||||||
|
return options;
|
||||||
|
});
|
||||||
|
|
||||||
|
services.AddScoped<IAccessDataContextFactory<TContext>>(sp =>
|
||||||
|
new AccessDataContextFactory<TContext>(
|
||||||
|
sp,
|
||||||
|
sp.GetRequiredService<AccessContextOptions<TContext>>(),
|
||||||
|
sp.GetRequiredService<IAccessConnectionStringFactory>(),
|
||||||
|
sp.GetRequiredService<IAccessDatabaseInitializer>()));
|
||||||
|
|
||||||
|
if (registerDataContext)
|
||||||
|
{
|
||||||
|
services.Add(new ServiceDescriptor(typeof(IDataContext),
|
||||||
|
sp => sp.GetRequiredService<IAccessDataContextFactory<TContext>>().Create(),
|
||||||
|
dataContextLifetime));
|
||||||
|
}
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
using System;
|
||||||
|
using EntityFrameworkCore.Jet;
|
||||||
|
using EntityFrameworkCore.Jet.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Sharp.Data.Access.Connections;
|
||||||
|
using Sharp.Data.Access.Initialization;
|
||||||
|
using Sharp.Data.Access.Internal;
|
||||||
|
using Sharp.Data.Access.Options;
|
||||||
|
using Sharp.Data.Access.Security;
|
||||||
|
using Sharp.Data;
|
||||||
|
|
||||||
|
namespace Sharp.Data.Access.Extensions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 手动使用 DataContextBuilder 时的 Access Helper。
|
||||||
|
/// </summary>
|
||||||
|
public static class AccessDataContextBuilderExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 通过现成的连接字符串配置 Access。
|
||||||
|
/// </summary>
|
||||||
|
public static DataContextBuilder<TContext> UseAccessDatabase<TContext>(
|
||||||
|
this DataContextBuilder<TContext> builder,
|
||||||
|
string connectionString,
|
||||||
|
Action<JetDbContextOptionsBuilder>? configure = null)
|
||||||
|
where TContext : DbContext
|
||||||
|
{
|
||||||
|
if (builder == null) throw new ArgumentNullException(nameof(builder));
|
||||||
|
if (string.IsNullOrWhiteSpace(connectionString))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("连接字符串不能为空。", nameof(connectionString));
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.ConfigureDbContext(options => options.UseJet(connectionString, configure));
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 通过 AccessDatabaseOptions 配置 Access。
|
||||||
|
/// </summary>
|
||||||
|
public static DataContextBuilder<TContext> UseAccessDatabase<TContext>(
|
||||||
|
this DataContextBuilder<TContext> builder,
|
||||||
|
Action<AccessDatabaseOptions> configureOptions,
|
||||||
|
IServiceProvider? services = null,
|
||||||
|
IAccessPasswordDecryptor? decryptor = null,
|
||||||
|
Action<JetDbContextOptionsBuilder>? configure = null)
|
||||||
|
where TContext : DbContext
|
||||||
|
{
|
||||||
|
if (builder == null) throw new ArgumentNullException(nameof(builder));
|
||||||
|
if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions));
|
||||||
|
|
||||||
|
var options = new AccessDatabaseOptions();
|
||||||
|
configureOptions(options);
|
||||||
|
options.Validate();
|
||||||
|
|
||||||
|
var serviceProvider = services ?? NullServiceProvider.Instance;
|
||||||
|
var initializer = new AccessDatabaseInitializer();
|
||||||
|
var connectionFactory = new AccessConnectionStringFactory(decryptor ?? new PassthroughAccessPasswordDecryptor());
|
||||||
|
|
||||||
|
initializer.EnsureDatabase(options, serviceProvider);
|
||||||
|
var connectionString = connectionFactory.Create(options, serviceProvider);
|
||||||
|
builder.ConfigureDbContext(o => o.UseJet(connectionString, configure));
|
||||||
|
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Sharp.Data.Context;
|
||||||
|
|
||||||
|
namespace Sharp.Data.Access;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Access 数据上下文工厂,避免直接依赖具体实现。
|
||||||
|
/// </summary>
|
||||||
|
public interface IAccessDataContextFactory<TContext> where TContext : DbContext
|
||||||
|
{
|
||||||
|
IDataContext Create();
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
using System.Data.OleDb;
|
||||||
|
|
||||||
|
namespace Sharp.Data.Access;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Access 数据库事务接口
|
||||||
|
/// </summary>
|
||||||
|
public interface IAccessTransaction : IDisposable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 执行非查询命令(INSERT, UPDATE, DELETE)
|
||||||
|
/// </summary>
|
||||||
|
int Execute(string sql, params OleDbParameter[] parameters);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询多条记录
|
||||||
|
/// </summary>
|
||||||
|
List<T> Query<T>(string sql, params OleDbParameter[] parameters) where T : new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行标量查询(COUNT, SUM等)
|
||||||
|
/// </summary>
|
||||||
|
object? ExecuteScalar(string sql, params OleDbParameter[] parameters);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 提交事务
|
||||||
|
/// </summary>
|
||||||
|
void Commit();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 回滚事务
|
||||||
|
/// </summary>
|
||||||
|
void Rollback();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Access 数据服务接口(基于 System.Data.OleDb)
|
||||||
|
/// </summary>
|
||||||
|
public interface IAccessDataService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 查询多条记录
|
||||||
|
/// </summary>
|
||||||
|
List<T> Query<T>(string sql, params OleDbParameter[] parameters) where T : new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询单条记录
|
||||||
|
/// </summary>
|
||||||
|
T? QueryFirstOrDefault<T>(string sql, params OleDbParameter[] parameters) where T : new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行标量查询(COUNT, SUM等)
|
||||||
|
/// </summary>
|
||||||
|
object? ExecuteScalar(string sql, params OleDbParameter[] parameters);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行非查询命令(INSERT, UPDATE, DELETE)
|
||||||
|
/// </summary>
|
||||||
|
int Execute(string sql, params OleDbParameter[] parameters);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 开始事务
|
||||||
|
/// </summary>
|
||||||
|
IAccessTransaction BeginTransaction();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 在事务中执行操作(自动提交/回滚)
|
||||||
|
/// </summary>
|
||||||
|
void ExecuteInTransaction(Action<IAccessTransaction> action);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试数据库连接
|
||||||
|
/// </summary>
|
||||||
|
bool TestConnection();
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using Sharp.Data.Access.Internal;
|
||||||
|
using Sharp.Data.Access.Options;
|
||||||
|
|
||||||
|
namespace Sharp.Data.Access.Initialization;
|
||||||
|
|
||||||
|
internal sealed class AccessDatabaseInitializer : IAccessDatabaseInitializer
|
||||||
|
{
|
||||||
|
public void EnsureDatabase(AccessDatabaseOptions options, IServiceProvider services)
|
||||||
|
{
|
||||||
|
var configuration = AccessDatabaseConfiguration.FromOptions(options, services);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(configuration.ConnectionString) ||
|
||||||
|
string.IsNullOrWhiteSpace(configuration.DatabaseFile))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var databasePath = AccessPathResolver.Resolve(configuration.DatabaseFile, configuration.DataDirectory);
|
||||||
|
var directory = Path.GetDirectoryName(databasePath);
|
||||||
|
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(directory);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (File.Exists(databasePath))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configuration.CopyTemplateWhenMissing && !string.IsNullOrWhiteSpace(configuration.TemplateFile))
|
||||||
|
{
|
||||||
|
var templatePath = AccessPathResolver.Resolve(configuration.TemplateFile, configuration.DataDirectory);
|
||||||
|
if (!File.Exists(templatePath))
|
||||||
|
{
|
||||||
|
throw new FileNotFoundException($"Access 模板文件 \"{templatePath}\" 不存在。", templatePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
File.Copy(templatePath, databasePath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configuration.ThrowIfMissing)
|
||||||
|
{
|
||||||
|
throw new FileNotFoundException($"Access 数据库 \"{databasePath}\" 不存在。", databasePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using System;
|
||||||
|
using Sharp.Data.Access.Options;
|
||||||
|
|
||||||
|
namespace Sharp.Data.Access.Initialization;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Access 数据库初始化器(确保目录/数据库文件准备就绪)。
|
||||||
|
/// </summary>
|
||||||
|
public interface IAccessDatabaseInitializer
|
||||||
|
{
|
||||||
|
void EnsureDatabase(AccessDatabaseOptions options, IServiceProvider services);
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
using System;
|
||||||
|
using EntityFrameworkCore.Jet;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Sharp.Data;
|
||||||
|
using Sharp.Data.Access.Connections;
|
||||||
|
using Sharp.Data.Access.Initialization;
|
||||||
|
using Sharp.Data.Access.Options;
|
||||||
|
using Sharp.Data.Context;
|
||||||
|
|
||||||
|
namespace Sharp.Data.Access.Internal;
|
||||||
|
|
||||||
|
internal sealed class AccessDataContextFactory<TContext> : IAccessDataContextFactory<TContext>
|
||||||
|
where TContext : DbContext
|
||||||
|
{
|
||||||
|
private readonly IServiceProvider _services;
|
||||||
|
private readonly AccessContextOptions<TContext> _options;
|
||||||
|
private readonly IAccessConnectionStringFactory _connectionFactory;
|
||||||
|
private readonly IAccessDatabaseInitializer _initializer;
|
||||||
|
|
||||||
|
public AccessDataContextFactory(
|
||||||
|
IServiceProvider services,
|
||||||
|
AccessContextOptions<TContext> options,
|
||||||
|
IAccessConnectionStringFactory connectionFactory,
|
||||||
|
IAccessDatabaseInitializer initializer)
|
||||||
|
{
|
||||||
|
_services = services;
|
||||||
|
_options = options;
|
||||||
|
_connectionFactory = connectionFactory;
|
||||||
|
_initializer = initializer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IDataContext Create()
|
||||||
|
{
|
||||||
|
var builder = new DataContextBuilder<TContext>();
|
||||||
|
|
||||||
|
_initializer.EnsureDatabase(_options.Database, _services);
|
||||||
|
|
||||||
|
var connectionString = _connectionFactory.Create(_options.Database, _services);
|
||||||
|
builder.ConfigureDbContext(options =>
|
||||||
|
{
|
||||||
|
options.UseJet(connectionString, jet => _options.ConfigureJet?.Invoke(jet));
|
||||||
|
});
|
||||||
|
|
||||||
|
_options.ConfigureDataContext?.Invoke(_services, builder);
|
||||||
|
|
||||||
|
return builder.Build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Sharp.Data.Access.Options;
|
||||||
|
|
||||||
|
namespace Sharp.Data.Access.Internal;
|
||||||
|
|
||||||
|
internal sealed record AccessDatabaseConfiguration(
|
||||||
|
string? ConnectionString,
|
||||||
|
string? DatabaseFile,
|
||||||
|
string ProviderName,
|
||||||
|
string? Password,
|
||||||
|
string? EncryptedPassword,
|
||||||
|
string? TemplateFile,
|
||||||
|
string? DataDirectory,
|
||||||
|
int ConnectionTimeout,
|
||||||
|
int MaxRetryCount,
|
||||||
|
int RetryDelayMilliseconds,
|
||||||
|
IReadOnlyDictionary<string, string> AdditionalParameters,
|
||||||
|
bool CopyTemplateWhenMissing,
|
||||||
|
bool ThrowIfMissing)
|
||||||
|
{
|
||||||
|
public static AccessDatabaseConfiguration FromOptions(AccessDatabaseOptions options, IServiceProvider services)
|
||||||
|
{
|
||||||
|
if (options == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(options));
|
||||||
|
}
|
||||||
|
|
||||||
|
var connectionString = Resolve(options.ConnectionString, options.ConnectionStringFactory, services);
|
||||||
|
var databaseFile = Resolve(options.DatabaseFile, options.DatabaseFileFactory, services);
|
||||||
|
var templateFile = Resolve(options.TemplateFile, options.TemplateFileFactory, services);
|
||||||
|
var dataDirectory = Resolve(options.DataDirectory, options.DataDirectoryFactory, services);
|
||||||
|
var password = Resolve(options.Password, options.PasswordFactory, services);
|
||||||
|
var encryptedPassword = Resolve(options.EncryptedPassword, options.EncryptedPasswordFactory, services);
|
||||||
|
|
||||||
|
return new AccessDatabaseConfiguration(
|
||||||
|
connectionString,
|
||||||
|
databaseFile,
|
||||||
|
string.IsNullOrWhiteSpace(options.ProviderName) ? AccessProviderNames.AceOleDb16 : options.ProviderName,
|
||||||
|
password,
|
||||||
|
encryptedPassword,
|
||||||
|
templateFile,
|
||||||
|
dataDirectory,
|
||||||
|
options.ConnectionTimeout,
|
||||||
|
options.MaxRetryCount,
|
||||||
|
options.RetryDelayMilliseconds,
|
||||||
|
new Dictionary<string, string>(options.AdditionalParameters, StringComparer.OrdinalIgnoreCase),
|
||||||
|
options.CopyTemplateWhenMissing,
|
||||||
|
options.ThrowIfMissing);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? Resolve(string? value, Func<IServiceProvider, string?>? factory, IServiceProvider services)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return factory?.Invoke(services);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace Sharp.Data.Access.Internal;
|
||||||
|
|
||||||
|
internal static class AccessPathResolver
|
||||||
|
{
|
||||||
|
public static string Resolve(string path, string? baseDirectory)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(path))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Access 数据库路径不能为空。");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Path.IsPathFullyQualified(path))
|
||||||
|
{
|
||||||
|
return Path.GetFullPath(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
var root = string.IsNullOrWhiteSpace(baseDirectory)
|
||||||
|
? AppContext.BaseDirectory
|
||||||
|
: baseDirectory!;
|
||||||
|
|
||||||
|
return Path.GetFullPath(Path.Combine(root, path));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Sharp.Data.Access.Internal;
|
||||||
|
|
||||||
|
internal sealed class NullServiceProvider : IServiceProvider
|
||||||
|
{
|
||||||
|
public static readonly NullServiceProvider Instance = new();
|
||||||
|
|
||||||
|
private NullServiceProvider()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public object? GetService(Type serviceType) => null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using System;
|
||||||
|
using EntityFrameworkCore.Jet.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Sharp.Data;
|
||||||
|
|
||||||
|
namespace Sharp.Data.Access.Options;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Access 数据上下文注册配置。
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TContext">EF Core DbContext 类型。</typeparam>
|
||||||
|
public sealed class AccessContextOptions<TContext> where TContext : DbContext
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Access 数据库配置(连接方式、密码等)。
|
||||||
|
/// </summary>
|
||||||
|
public AccessDatabaseOptions Database { get; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 允许调用方在 Build 之前继续配置 DataContextBuilder(例如启用审计、软删除、当前用户等)。
|
||||||
|
/// </summary>
|
||||||
|
public Action<IServiceProvider, DataContextBuilder<TContext>>? ConfigureDataContext { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 细化 Jet Provider 选项。
|
||||||
|
/// </summary>
|
||||||
|
public Action<JetDbContextOptionsBuilder>? ConfigureJet { get; set; }
|
||||||
|
|
||||||
|
internal void Validate() => Database.Validate();
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Sharp.Data.Access.Options;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Access 数据库统一配置项,同时支持 OleDb 和 EF Core 两种方式。
|
||||||
|
/// </summary>
|
||||||
|
public class AccessDatabaseOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 完整的连接字符串,可选。如果提供则优先使用,不再重新构建。
|
||||||
|
/// </summary>
|
||||||
|
public string? ConnectionString { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 通过依赖注入动态生成连接字符串。
|
||||||
|
/// </summary>
|
||||||
|
public Func<IServiceProvider, string?>? ConnectionStringFactory { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Access 数据库文件路径(相对或绝对)。
|
||||||
|
/// </summary>
|
||||||
|
public string? DatabaseFile { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 通过依赖注入动态解析数据库文件路径。
|
||||||
|
/// </summary>
|
||||||
|
public Func<IServiceProvider, string?>? DatabaseFileFactory { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 数据文件的基础目录(如果数据库路径为相对路径则会拼接)。
|
||||||
|
/// 默认使用 <see cref="AppContext.BaseDirectory" />。
|
||||||
|
/// </summary>
|
||||||
|
public string? DataDirectory { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 通过依赖注入动态解析数据目录。
|
||||||
|
/// </summary>
|
||||||
|
public Func<IServiceProvider, string?>? DataDirectoryFactory { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 明文密码,可选。用于访问加密的 Access 数据库。
|
||||||
|
/// </summary>
|
||||||
|
public string? Password { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 通过依赖注入动态解析明文密码。
|
||||||
|
/// </summary>
|
||||||
|
public Func<IServiceProvider, string?>? PasswordFactory { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 加密后的密码,可选。将通过 <see cref="Security.IAccessPasswordDecryptor" /> 解密。
|
||||||
|
/// </summary>
|
||||||
|
public string? EncryptedPassword { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 通过依赖注入动态解析加密密码。
|
||||||
|
/// </summary>
|
||||||
|
public Func<IServiceProvider, string?>? EncryptedPasswordFactory { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 数据库模板(*.mdb / *.accdb),在文件不存在时复制一份。
|
||||||
|
/// </summary>
|
||||||
|
public string? TemplateFile { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 通过依赖注入动态解析模板文件路径。
|
||||||
|
/// </summary>
|
||||||
|
public Func<IServiceProvider, string?>? TemplateFileFactory { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 当数据库文件缺失时是否尝试从模板复制。
|
||||||
|
/// </summary>
|
||||||
|
public bool CopyTemplateWhenMissing { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 当数据库文件缺失(且无法复制)时是否抛出异常。
|
||||||
|
/// </summary>
|
||||||
|
public bool ThrowIfMissing { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provider 名称,默认为 ACE OLE DB 16.0(功能最完整)。
|
||||||
|
/// 可选值:
|
||||||
|
/// - AccessProviderNames.AceOleDb16 (Microsoft.ACE.OLEDB.16.0) - 推荐,需要 Office 2016+
|
||||||
|
/// - AccessProviderNames.AceOleDb12 (Microsoft.ACE.OLEDB.12.0) - 需要 Office 2010+,不支持 Connect Timeout
|
||||||
|
/// - AccessProviderNames.JetOleDb4 (Microsoft.Jet.OLEDB.4.0) - 仅支持 32 位和旧版 Access
|
||||||
|
/// </summary>
|
||||||
|
public string ProviderName { get; set; } = AccessProviderNames.AceOleDb16;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 连接超时时间(秒),默认 15 秒。
|
||||||
|
/// </summary>
|
||||||
|
public int ConnectionTimeout { get; set; } = 15;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 最大重试次数,默认 3 次。用于增强稳健性。
|
||||||
|
/// </summary>
|
||||||
|
public int MaxRetryCount { get; set; } = 3;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 重试延迟(毫秒),默认 100 毫秒。
|
||||||
|
/// </summary>
|
||||||
|
public int RetryDelayMilliseconds { get; set; } = 100;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 额外的 OLE DB 参数,按需添加。
|
||||||
|
/// </summary>
|
||||||
|
public IDictionary<string, string> AdditionalParameters { get; } =
|
||||||
|
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
internal void Validate()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(ConnectionString) &&
|
||||||
|
ConnectionStringFactory is null &&
|
||||||
|
string.IsNullOrWhiteSpace(DatabaseFile) &&
|
||||||
|
DatabaseFileFactory is null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"AccessDatabaseOptions 需要连接字符串或数据库文件路径。");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(ProviderName))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("必须指定 Access Provider。");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ConnectionTimeout <= 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("ConnectionTimeout 必须大于 0。");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MaxRetryCount < 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("MaxRetryCount 不能小于 0。");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (RetryDelayMilliseconds < 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("RetryDelayMilliseconds 不能小于 0。");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
namespace Sharp.Data.Access.Options;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 常用 Access 数据库 Provider 名称。
|
||||||
|
/// </summary>
|
||||||
|
public static class AccessProviderNames
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Microsoft Access Database Engine 2016 (推荐)。
|
||||||
|
/// </summary>
|
||||||
|
public const string AceOleDb16 = "Microsoft.ACE.OLEDB.16.0";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Microsoft Access Database Engine 2010。
|
||||||
|
/// </summary>
|
||||||
|
public const string AceOleDb12 = "Microsoft.ACE.OLEDB.12.0";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 老旧的 Jet 4.0 Provider(仅限 32 位进程)。
|
||||||
|
/// </summary>
|
||||||
|
public const string JetOleDb4 = "Microsoft.Jet.OLEDB.4.0";
|
||||||
|
}
|
||||||
@@ -0,0 +1,374 @@
|
|||||||
|
# AccessDataService 简化 API 使用指南
|
||||||
|
|
||||||
|
## 🎯 设计目标
|
||||||
|
|
||||||
|
Data.Access 模块的核心目标是**让调用者使用起来更简单**,将连接管理、SQL 执行、参数处理、事务管理等复杂度封装起来。
|
||||||
|
|
||||||
|
## 📊 代码对比
|
||||||
|
|
||||||
|
### ❌ 之前:直接使用 OleDb(复杂)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 每次都要手动创建连接
|
||||||
|
private OleDbConnection CreateConnection()
|
||||||
|
{
|
||||||
|
var dbPath = Path.GetFullPath("dbDwDmsSetup.accdb");
|
||||||
|
var connectionString = $"Provider=Microsoft.ACE.OLEDB.12.0;Data Source={dbPath}";
|
||||||
|
return new OleDbConnection(connectionString);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询用户 - 需要写大量样板代码
|
||||||
|
private async Task<List<UserInfoDto>> LoadUsersAsync()
|
||||||
|
{
|
||||||
|
using var connection = CreateConnection();
|
||||||
|
await connection.OpenAsync();
|
||||||
|
|
||||||
|
using var command = new OleDbCommand("SELECT UserName, Password FROM UserInfo", connection);
|
||||||
|
using var reader = await Task.Run(() => command.ExecuteReader());
|
||||||
|
|
||||||
|
var users = new List<UserInfoDto>();
|
||||||
|
while (await Task.Run(() => reader.Read()))
|
||||||
|
{
|
||||||
|
var user = new UserInfoDto
|
||||||
|
{
|
||||||
|
UserName = reader["UserName"]?.ToString() ?? "",
|
||||||
|
Password = reader["Password"]?.ToString() ?? ""
|
||||||
|
};
|
||||||
|
users.Add(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
return users;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除用户 - 需要手动管理事务
|
||||||
|
private async Task DeleteUserAsync(string userName)
|
||||||
|
{
|
||||||
|
using var connection = CreateConnection();
|
||||||
|
await connection.OpenAsync();
|
||||||
|
using var transaction = connection.BeginTransaction();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (var deletePermCmd = new OleDbCommand(
|
||||||
|
"DELETE FROM UserPermission WHERE UserInfo_UserName = ?", connection, transaction))
|
||||||
|
{
|
||||||
|
deletePermCmd.Parameters.AddWithValue("@UserName", userName);
|
||||||
|
await Task.Run(() => deletePermCmd.ExecuteNonQuery());
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var deleteUserCmd = new OleDbCommand(
|
||||||
|
"DELETE FROM UserInfo WHERE UserName = ?", connection, transaction))
|
||||||
|
{
|
||||||
|
deleteUserCmd.Parameters.AddWithValue("@UserName", userName);
|
||||||
|
await Task.Run(() => deleteUserCmd.ExecuteNonQuery());
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction.Commit();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
transaction.Rollback();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**问题**:
|
||||||
|
- 大量重复的连接管理代码
|
||||||
|
- 手动映射查询结果到对象
|
||||||
|
- 手动管理事务的提交和回滚
|
||||||
|
- 代码冗长,不易维护
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ✅ 现在:使用 AccessDataService(简洁)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 注入服务,无需手动创建连接
|
||||||
|
private readonly IAccessDataService _accessDataService;
|
||||||
|
|
||||||
|
// 查询用户 - 一行代码!自动映射!
|
||||||
|
private async Task<List<UserInfoDto>> LoadUsersAsync()
|
||||||
|
{
|
||||||
|
return await _accessDataService.QueryAsync<UserInfoDto>(
|
||||||
|
"SELECT UserName, [Password] FROM UserInfo");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除用户 - 自动管理事务!
|
||||||
|
private async Task DeleteUserAsync(string userName)
|
||||||
|
{
|
||||||
|
await _accessDataService.ExecuteInTransactionAsync(async transaction =>
|
||||||
|
{
|
||||||
|
await transaction.ExecuteAsync(
|
||||||
|
"DELETE FROM UserPermission WHERE UserInfo_UserName = ?",
|
||||||
|
new OleDbParameter("@UserName", userName));
|
||||||
|
|
||||||
|
await transaction.ExecuteAsync(
|
||||||
|
"DELETE FROM UserInfo WHERE UserName = ?",
|
||||||
|
new OleDbParameter("@UserName", userName));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**优势**:
|
||||||
|
- ✅ 无需手动管理连接
|
||||||
|
- ✅ 自动泛型映射查询结果
|
||||||
|
- ✅ 自动管理事务(提交/回滚)
|
||||||
|
- ✅ 代码简洁,易于维护
|
||||||
|
- ✅ 从 60+ 行减少到 10 行左右
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 快速开始
|
||||||
|
|
||||||
|
### 1. 初始化服务
|
||||||
|
|
||||||
|
在 `App.xaml.cs` 中初始化:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private void InitializeAccessDataService()
|
||||||
|
{
|
||||||
|
var databasePath = Path.GetFullPath("dbDwDmsSetup.accdb");
|
||||||
|
var options = new AccessDataServiceOptions
|
||||||
|
{
|
||||||
|
DatabasePath = databasePath
|
||||||
|
};
|
||||||
|
|
||||||
|
AccessDataService = new AccessDataService(options, LoggerFactory.CreateLogger<AccessDataService>());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 在窗口中使用
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public partial class AccessDatabaseWindowOleDb : Window
|
||||||
|
{
|
||||||
|
private readonly IAccessDataService _accessDataService;
|
||||||
|
|
||||||
|
public AccessDatabaseWindowOleDb()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
|
||||||
|
var app = (App)Application.Current;
|
||||||
|
_accessDataService = app.AccessDataService;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 API 使用示例
|
||||||
|
|
||||||
|
### 1. 查询多条记录
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 自动映射到对象
|
||||||
|
var users = await _accessDataService.QueryAsync<UserInfoDto>(
|
||||||
|
"SELECT UserName, [Password] FROM UserInfo");
|
||||||
|
|
||||||
|
// 带参数的查询
|
||||||
|
var users = await _accessDataService.QueryAsync<UserInfoDto>(
|
||||||
|
"SELECT UserName, [Password] FROM UserInfo WHERE UserName LIKE ?",
|
||||||
|
new OleDbParameter("@Pattern", "%admin%"));
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 查询单条记录
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var user = await _accessDataService.QueryFirstOrDefaultAsync<UserInfoDto>(
|
||||||
|
"SELECT UserName, [Password] FROM UserInfo WHERE UserName = ?",
|
||||||
|
new OleDbParameter("@UserName", "admin"));
|
||||||
|
|
||||||
|
if (user != null)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"找到用户: {user.UserName}");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 执行标量查询
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 查询数量
|
||||||
|
var count = await _accessDataService.ExecuteScalarAsync(
|
||||||
|
"SELECT COUNT(*) FROM UserInfo WHERE UserName = ?",
|
||||||
|
new OleDbParameter("@UserName", "admin"));
|
||||||
|
|
||||||
|
int userCount = Convert.ToInt32(count);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 插入数据
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var rowsAffected = await _accessDataService.ExecuteAsync(
|
||||||
|
"INSERT INTO UserInfo (UserName, [Password]) VALUES (?, ?)",
|
||||||
|
new OleDbParameter("@UserName", "newuser"),
|
||||||
|
new OleDbParameter("@Password", "password123"));
|
||||||
|
|
||||||
|
Console.WriteLine($"插入了 {rowsAffected} 行");
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 更新数据
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var rowsAffected = await _accessDataService.ExecuteAsync(
|
||||||
|
"UPDATE UserInfo SET [Password] = ? WHERE UserName = ?",
|
||||||
|
new OleDbParameter("@Password", "newpassword"),
|
||||||
|
new OleDbParameter("@UserName", "admin"));
|
||||||
|
|
||||||
|
Console.WriteLine($"更新了 {rowsAffected} 行");
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. 删除数据
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var rowsAffected = await _accessDataService.ExecuteAsync(
|
||||||
|
"DELETE FROM UserInfo WHERE UserName = ?",
|
||||||
|
new OleDbParameter("@UserName", "testuser"));
|
||||||
|
|
||||||
|
Console.WriteLine($"删除了 {rowsAffected} 行");
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. 使用事务(自动提交/回滚)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 推荐:使用 ExecuteInTransactionAsync(自动管理)
|
||||||
|
await _accessDataService.ExecuteInTransactionAsync(async transaction =>
|
||||||
|
{
|
||||||
|
// 删除权限
|
||||||
|
await transaction.ExecuteAsync(
|
||||||
|
"DELETE FROM UserPermission WHERE UserInfo_UserName = ?",
|
||||||
|
new OleDbParameter("@UserName", userName));
|
||||||
|
|
||||||
|
// 删除用户
|
||||||
|
await transaction.ExecuteAsync(
|
||||||
|
"DELETE FROM UserInfo WHERE UserName = ?",
|
||||||
|
new OleDbParameter("@UserName", userName));
|
||||||
|
|
||||||
|
// 自动提交!如果有异常,自动回滚!
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8. 手动管理事务
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 如果需要手动控制
|
||||||
|
using var transaction = _accessDataService.BeginTransaction();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await transaction.ExecuteAsync("INSERT INTO ...", ...);
|
||||||
|
await transaction.ExecuteAsync("UPDATE ...", ...);
|
||||||
|
|
||||||
|
transaction.Commit(); // 手动提交
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
transaction.Rollback(); // 手动回滚
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9. 测试连接
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var success = await _accessDataService.TestConnectionAsync();
|
||||||
|
if (success)
|
||||||
|
{
|
||||||
|
Console.WriteLine("数据库连接成功!");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 完整示例
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class UserService
|
||||||
|
{
|
||||||
|
private readonly IAccessDataService _accessDataService;
|
||||||
|
|
||||||
|
public UserService(IAccessDataService accessDataService)
|
||||||
|
{
|
||||||
|
_accessDataService = accessDataService;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取所有用户
|
||||||
|
public async Task<List<UserInfoDto>> GetAllUsersAsync()
|
||||||
|
{
|
||||||
|
return await _accessDataService.QueryAsync<UserInfoDto>(
|
||||||
|
"SELECT UserName, [Password] FROM UserInfo");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户权限数量
|
||||||
|
public async Task<int> GetUserPermissionCountAsync(string userName)
|
||||||
|
{
|
||||||
|
var count = await _accessDataService.ExecuteScalarAsync(
|
||||||
|
"SELECT COUNT(*) FROM UserPermission WHERE UserInfo_UserName = ?",
|
||||||
|
new OleDbParameter("@UserName", userName));
|
||||||
|
|
||||||
|
return Convert.ToInt32(count);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加用户
|
||||||
|
public async Task<bool> AddUserAsync(string userName, string password)
|
||||||
|
{
|
||||||
|
// 检查是否已存在
|
||||||
|
var exists = await _accessDataService.ExecuteScalarAsync(
|
||||||
|
"SELECT COUNT(*) FROM UserInfo WHERE UserName = ?",
|
||||||
|
new OleDbParameter("@UserName", userName));
|
||||||
|
|
||||||
|
if (Convert.ToInt32(exists) > 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// 插入用户
|
||||||
|
await _accessDataService.ExecuteAsync(
|
||||||
|
"INSERT INTO UserInfo (UserName, [Password]) VALUES (?, ?)",
|
||||||
|
new OleDbParameter("@UserName", userName),
|
||||||
|
new OleDbParameter("@Password", password));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除用户(包含权限)
|
||||||
|
public async Task DeleteUserAsync(string userName)
|
||||||
|
{
|
||||||
|
await _accessDataService.ExecuteInTransactionAsync(async transaction =>
|
||||||
|
{
|
||||||
|
// 先删除权限
|
||||||
|
await transaction.ExecuteAsync(
|
||||||
|
"DELETE FROM UserPermission WHERE UserInfo_UserName = ?",
|
||||||
|
new OleDbParameter("@UserName", userName));
|
||||||
|
|
||||||
|
// 再删除用户
|
||||||
|
await transaction.ExecuteAsync(
|
||||||
|
"DELETE FROM UserInfo WHERE UserName = ?",
|
||||||
|
new OleDbParameter("@UserName", userName));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 核心优势总结
|
||||||
|
|
||||||
|
| 特性 | 直接使用 OleDb | 使用 AccessDataService |
|
||||||
|
|------|---------------|----------------------|
|
||||||
|
| 连接管理 | 手动创建、打开、释放 | ✅ 自动管理 |
|
||||||
|
| 结果映射 | 手动读取 DataReader | ✅ 自动泛型映射 |
|
||||||
|
| 事务管理 | 手动 BeginTransaction/Commit/Rollback | ✅ 自动管理 |
|
||||||
|
| 代码量 | 60+ 行 | ✅ 10 行左右 |
|
||||||
|
| 可维护性 | 低(大量重复代码) | ✅ 高 |
|
||||||
|
| 易用性 | 低(需要理解 ADO.NET) | ✅ 高 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📖 参考文件
|
||||||
|
|
||||||
|
- **核心实现**: `Data.Access/AccessDataService.cs`
|
||||||
|
- **接口定义**: `Data.Access/IAccessDataService.cs`
|
||||||
|
- **配置选项**: `Data.Access/AccessDataServiceOptions.cs`
|
||||||
|
- **使用示例**: `Server/Views/AccessDatabaseWindowOleDb.xaml.cs`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**总结**:Data.Access 模块通过封装 OleDb 的复杂度,提供了简洁优雅的 API,让调用者可以用更少的代码完成相同的功能!
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
# Sharp.Data.Access
|
||||||
|
|
||||||
|
基于 EF Core + Access (Jet) 的数据库基础设施。遵循 “依赖大于配置” 的理念,提供易于集成的连接串构建、密码解密、数据库初始化以及依赖注入支持,让其他项目只专注于提供配置即可。
|
||||||
|
|
||||||
|
> **定位**:仅提供 Access 数据层能力(连接、仓储、上下文构建),不含任何业务逻辑。
|
||||||
|
|
||||||
|
## 特性
|
||||||
|
|
||||||
|
- ✅ **DI 集成** – 一行 `AddAccessDataContext<TContext>()` 注册,其他模块通过构造函数获取 `IDataContext`。
|
||||||
|
- ✅ **最小配置** – 只需要提供数据库文件或连接字符串即可完成集成。
|
||||||
|
- ✅ **密码解密** – 内置 `IAccessPasswordDecryptor`,可注入自定义实现来解密加密密码。
|
||||||
|
- ✅ **数据库初始化** – 自动创建目录、可选基于模板复制空库文件,避免运行期错误。
|
||||||
|
- ✅ **灵活扩展** – 支持通过委托访问 `IServiceProvider`,从任意配置中心或密钥管理系统读取参数。
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Sharp.Data.Access.DependencyInjection;
|
||||||
|
using Sharp.Data.Context;
|
||||||
|
|
||||||
|
builder.Services.AddAccessDataContext<AppDbContext>(options =>
|
||||||
|
{
|
||||||
|
options.Database.DatabaseFile = "data/app.accdb";
|
||||||
|
options.Database.EncryptedPassword = configuration["Access:EncryptedPassword"];
|
||||||
|
// 配置 Data 模块能力
|
||||||
|
options.ConfigureDataContext = (sp, ctx) =>
|
||||||
|
{
|
||||||
|
ctx.UseAudit()
|
||||||
|
.UseSoftDelete()
|
||||||
|
.UseCurrentUserProvider(() => sp.GetService<ICurrentUser>()?.Id);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
随后只需要注入 `IDataContext` 或 `IAccessDataContextFactory<AppDbContext>` 即可使用:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class UserService
|
||||||
|
{
|
||||||
|
private readonly IDataContext _db;
|
||||||
|
|
||||||
|
public UserService(IDataContext db) => _db = db;
|
||||||
|
|
||||||
|
public Task<List<User>> GetUsersAsync() => _db.Repository<User>().GetAllAsync();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 依赖注入示例(带模板/密码)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
|
||||||
|
services.AddSingleton<ISecretProvider, SecretProvider>(); // 自定义的密钥服务
|
||||||
|
|
||||||
|
services.AddAccessDataContext<AppDbContext>(options =>
|
||||||
|
{
|
||||||
|
options.Database.DatabaseFile = "data/app.accdb";
|
||||||
|
options.Database.TemplateFile = "data/template.accdb";
|
||||||
|
options.Database.CopyTemplateWhenMissing = true;
|
||||||
|
options.Database.EncryptedPasswordFactory = sp =>
|
||||||
|
sp.GetRequiredService<ISecretProvider>().Get("AccessDbPassword");
|
||||||
|
|
||||||
|
options.ConfigureDataContext = (sp, ctx) =>
|
||||||
|
{
|
||||||
|
ctx.UseAudit()
|
||||||
|
.UseSoftDelete()
|
||||||
|
.UseCurrentUserProvider(() => sp.GetRequiredService<ICurrentUserAccessor>().Id);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
await using var provider = services.BuildServiceProvider();
|
||||||
|
var factory = provider.GetRequiredService<IAccessDataContextFactory<AppDbContext>>();
|
||||||
|
await using var dataContext = factory.Create();
|
||||||
|
```
|
||||||
|
|
||||||
|
> 在实际业务项目中可以把 `IAccessDataContextFactory<TContext>` 注入到需要“短生命周期”数据库连接的服务中,或直接注入 `IDataContext`(作用域作用域内共享同一实例)。
|
||||||
|
|
||||||
|
## 处理加密密码
|
||||||
|
|
||||||
|
实现 `IAccessPasswordDecryptor` 并注册到 DI:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
builder.Services.AddSingleton<IAccessPasswordDecryptor, AesPasswordDecryptor>();
|
||||||
|
```
|
||||||
|
|
||||||
|
`AccessDatabaseOptions.EncryptedPassword` 会在构建连接字符串前自动调用该服务完成解密,也可以直接提供明文 `Password`。
|
||||||
|
|
||||||
|
## 模板与初始化
|
||||||
|
|
||||||
|
- `CopyTemplateWhenMissing = true`(默认)时,如果指定了 `TemplateFile`,将在首次运行时复制模板数据库。
|
||||||
|
- `ThrowIfMissing = true` 可在缺少数据库文件时立即抛出异常,便于早期发现部署问题。
|
||||||
|
- 所有路径都支持相对路径(基于 `AppContext.BaseDirectory`),也可以通过 `DataDirectoryFactory` 自定义。
|
||||||
|
|
||||||
|
## 手动使用(无依赖注入)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var builder = new DataContextBuilder<AppDbContext>();
|
||||||
|
builder.UseAccessDatabase(options =>
|
||||||
|
{
|
||||||
|
options.DatabaseFile = "data/app.accdb";
|
||||||
|
options.Password = "secret";
|
||||||
|
});
|
||||||
|
|
||||||
|
var db = builder.Build();
|
||||||
|
```
|
||||||
|
|
||||||
|
## 可扩展点
|
||||||
|
|
||||||
|
- `IAccessPasswordDecryptor`:自定义密码加解密。
|
||||||
|
- `IAccessConnectionStringFactory`:自定义连接串构建逻辑。
|
||||||
|
- `IAccessDatabaseInitializer`:自定义数据库初始化策略(检查锁、备份等)。
|
||||||
|
- `AccessContextOptions.ConfigureDataContext`:复用 Data 模块的审计、软删除、自动迁移等能力。
|
||||||
|
|
||||||
|
通过这些基础设施,Access 支持可在任何项目中复用,同时保持代码简洁、低耦合。
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
namespace Sharp.Data.Access.Security;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Access 密码解密接口,调用方可替换以实现自定义加解密策略。
|
||||||
|
/// </summary>
|
||||||
|
public interface IAccessPasswordDecryptor
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 解密 Access 数据库密码。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="encryptedPassword">加密后的密码。</param>
|
||||||
|
/// <returns>返回明文密码,如果无法解密则返回 null。</returns>
|
||||||
|
string? Decrypt(string? encryptedPassword);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Sharp.Data.Access.Security;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 默认的密码解密器,直接返回原始值。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PassthroughAccessPasswordDecryptor : IAccessPasswordDecryptor
|
||||||
|
{
|
||||||
|
public string? Decrypt(string? encryptedPassword) => encryptedPassword;
|
||||||
|
}
|
||||||
@@ -294,9 +294,20 @@ public class HttpServer
|
|||||||
var controller = _serviceProvider.GetService(controllerType);
|
var controller = _serviceProvider.GetService(controllerType);
|
||||||
if (controller != null)
|
if (controller != null)
|
||||||
return controller;
|
return controller;
|
||||||
|
|
||||||
|
// 使用 ActivatorUtilities 创建实例(可以解析构造函数参数)
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return ActivatorUtilities.CreateInstance(_serviceProvider, controllerType);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger?.LogError(ex, "Failed to create controller {ControllerType} with DI", controllerType.Name);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用激活器创建
|
// 没有 DI 容器时,使用默认激活器创建(需要无参构造)
|
||||||
return Activator.CreateInstance(controllerType)!;
|
return Activator.CreateInstance(controllerType)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
using System.Windows;
|
using System.IO;
|
||||||
|
using System.Windows;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Config;
|
using Config;
|
||||||
using Sharp.Log;
|
using Sharp.Log;
|
||||||
using Sharp.Data;
|
using Sharp.Data;
|
||||||
using Sharp.Data.Context;
|
using Sharp.Data.Context;
|
||||||
|
using Sharp.Data.Access;
|
||||||
|
using Sharp.Data.Access.Options;
|
||||||
using Server.Config;
|
using Server.Config;
|
||||||
using Server.Services;
|
using Server.Services;
|
||||||
using Server.Data;
|
using Server.Data;
|
||||||
@@ -20,6 +23,7 @@ namespace Server
|
|||||||
public ILoggerFactory? LoggerFactory { get; private set; }
|
public ILoggerFactory? LoggerFactory { get; private set; }
|
||||||
public ServerService? ServerService { get; private set; }
|
public ServerService? ServerService { get; private set; }
|
||||||
public IDataContext? DataContext { get; private set; }
|
public IDataContext? DataContext { get; private set; }
|
||||||
|
public IAccessDataService? AccessDataService { get; private set; }
|
||||||
|
|
||||||
protected override void OnStartup(StartupEventArgs e)
|
protected override void OnStartup(StartupEventArgs e)
|
||||||
{
|
{
|
||||||
@@ -36,7 +40,10 @@ namespace Server
|
|||||||
// 3. 初始化数据库
|
// 3. 初始化数据库
|
||||||
InitializeDatabase();
|
InitializeDatabase();
|
||||||
|
|
||||||
// 4. 初始化服务器服务
|
// 4. 初始化 Access 数据服务
|
||||||
|
InitializeAccessDataService();
|
||||||
|
|
||||||
|
// 5. 初始化服务器服务
|
||||||
InitializeServerService();
|
InitializeServerService();
|
||||||
|
|
||||||
var logger = LoggerFactory?.CreateLogger<App>();
|
var logger = LoggerFactory?.CreateLogger<App>();
|
||||||
@@ -105,6 +112,62 @@ namespace Server
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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()
|
private void InitializeServerService()
|
||||||
{
|
{
|
||||||
if (AppConfig == null || LoggerFactory == null)
|
if (AppConfig == null || LoggerFactory == null)
|
||||||
|
|||||||
@@ -23,13 +23,13 @@ namespace Server.Controllers
|
|||||||
public async Task GetAll(HttpContext context)
|
public async Task GetAll(HttpContext context)
|
||||||
{
|
{
|
||||||
var items = _dataStore.Values.ToList();
|
var items = _dataStore.Values.ToList();
|
||||||
var result = ResultExtensions.HttpSuccess(new
|
var data = new
|
||||||
{
|
{
|
||||||
Count = items.Count,
|
Count = items.Count,
|
||||||
Items = items
|
Items = items
|
||||||
});
|
};
|
||||||
|
|
||||||
await WriteSuccessResponseAsync(context, result);
|
await WriteSuccessResponseAsync(context, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -49,8 +49,7 @@ namespace Server.Controllers
|
|||||||
|
|
||||||
if (_dataStore.TryGetValue(id, out var item))
|
if (_dataStore.TryGetValue(id, out var item))
|
||||||
{
|
{
|
||||||
var result = ResultExtensions.HttpSuccess(item);
|
await WriteSuccessResponseAsync(context, item);
|
||||||
await WriteSuccessResponseAsync(context, result);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -87,8 +86,7 @@ namespace Server.Controllers
|
|||||||
|
|
||||||
_dataStore[id] = item;
|
_dataStore[id] = item;
|
||||||
|
|
||||||
var result = ResultExtensions.HttpSuccess(item, "Data created successfully");
|
await WriteSuccessResponseAsync(context, item, "Data created successfully");
|
||||||
await WriteSuccessResponseAsync(context, result);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -129,8 +127,7 @@ namespace Server.Controllers
|
|||||||
item.UpdatedAt = DateTime.UtcNow;
|
item.UpdatedAt = DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = ResultExtensions.HttpSuccess(item, "Data updated successfully");
|
await WriteSuccessResponseAsync(context, item, "Data updated successfully");
|
||||||
await WriteSuccessResponseAsync(context, result);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -150,8 +147,7 @@ namespace Server.Controllers
|
|||||||
|
|
||||||
if (_dataStore.TryRemove(id, out var item))
|
if (_dataStore.TryRemove(id, out var item))
|
||||||
{
|
{
|
||||||
var result = ResultExtensions.HttpSuccess(item, "Data deleted successfully");
|
await WriteSuccessResponseAsync(context, item, "Data deleted successfully");
|
||||||
await WriteSuccessResponseAsync(context, result);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -169,8 +165,8 @@ namespace Server.Controllers
|
|||||||
var count = _dataStore.Count;
|
var count = _dataStore.Count;
|
||||||
_dataStore.Clear();
|
_dataStore.Clear();
|
||||||
|
|
||||||
var result = ResultExtensions.HttpSuccess(new { DeletedCount = count }, "All data cleared");
|
var data = new { DeletedCount = count };
|
||||||
await WriteSuccessResponseAsync(context, result);
|
await WriteSuccessResponseAsync(context, data, "All data cleared");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,14 +19,14 @@ namespace Server.Controllers
|
|||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task Check(HttpContext context)
|
public async Task Check(HttpContext context)
|
||||||
{
|
{
|
||||||
var result = ResultExtensions.HttpSuccess(new
|
var data = new
|
||||||
{
|
{
|
||||||
Status = "Healthy",
|
Status = "Healthy",
|
||||||
Timestamp = DateTime.UtcNow,
|
Timestamp = DateTime.UtcNow,
|
||||||
Uptime = DateTime.UtcNow - Process.GetCurrentProcess().StartTime
|
Uptime = DateTime.UtcNow - Process.GetCurrentProcess().StartTime
|
||||||
});
|
};
|
||||||
|
|
||||||
await WriteSuccessResponseAsync(context, result);
|
await WriteSuccessResponseAsync(context, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -37,7 +37,7 @@ namespace Server.Controllers
|
|||||||
public async Task DetailedCheck(HttpContext context)
|
public async Task DetailedCheck(HttpContext context)
|
||||||
{
|
{
|
||||||
var process = Process.GetCurrentProcess();
|
var process = Process.GetCurrentProcess();
|
||||||
var result = ResultExtensions.HttpSuccess(new
|
var data = new
|
||||||
{
|
{
|
||||||
Status = "Healthy",
|
Status = "Healthy",
|
||||||
Timestamp = DateTime.UtcNow,
|
Timestamp = DateTime.UtcNow,
|
||||||
@@ -60,9 +60,9 @@ namespace Server.Controllers
|
|||||||
},
|
},
|
||||||
Threads = process.Threads.Count
|
Threads = process.Threads.Count
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
await WriteSuccessResponseAsync(context, result);
|
await WriteSuccessResponseAsync(context, data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ namespace Server.Controllers
|
|||||||
[HttpGet("/info")]
|
[HttpGet("/info")]
|
||||||
public async Task GetSystemInfo(HttpContext context)
|
public async Task GetSystemInfo(HttpContext context)
|
||||||
{
|
{
|
||||||
var result = ResultExtensions.HttpSuccess(new
|
var data = new
|
||||||
{
|
{
|
||||||
MachineName = Environment.MachineName,
|
MachineName = Environment.MachineName,
|
||||||
UserName = Environment.UserName,
|
UserName = Environment.UserName,
|
||||||
@@ -30,9 +30,9 @@ namespace Server.Controllers
|
|||||||
CurrentDirectory = Environment.CurrentDirectory,
|
CurrentDirectory = Environment.CurrentDirectory,
|
||||||
TickCount = Environment.TickCount64,
|
TickCount = Environment.TickCount64,
|
||||||
Version = Environment.Version.ToString()
|
Version = Environment.Version.ToString()
|
||||||
});
|
};
|
||||||
|
|
||||||
await WriteSuccessResponseAsync(context, result);
|
await WriteSuccessResponseAsync(context, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -59,7 +59,7 @@ namespace Server.Controllers
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var successResult = ResultExtensions.HttpSuccess(new { Name = name, Value = value });
|
var successResult = new { Name = name, Value = value };
|
||||||
await WriteSuccessResponseAsync(context, successResult);
|
await WriteSuccessResponseAsync(context, successResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ namespace Server.Controllers
|
|||||||
var now = DateTime.Now;
|
var now = DateTime.Now;
|
||||||
var utcNow = DateTime.UtcNow;
|
var utcNow = DateTime.UtcNow;
|
||||||
|
|
||||||
var result = ResultExtensions.HttpSuccess(new
|
var data = new
|
||||||
{
|
{
|
||||||
Local = new
|
Local = new
|
||||||
{
|
{
|
||||||
@@ -89,9 +89,9 @@ namespace Server.Controllers
|
|||||||
Time = utcNow.TimeOfDay,
|
Time = utcNow.TimeOfDay,
|
||||||
UnixTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
|
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; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,438 @@
|
|||||||
|
# Access 数据库 CRUD 完整示例
|
||||||
|
|
||||||
|
使用 **System.Data.OleDb** 对 Access 数据库进行增删改查操作。
|
||||||
|
|
||||||
|
## 📦 连接字符串
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var connectionString = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=dbDwDmsSetup.accdb";
|
||||||
|
using var connection = new OleDbConnection(connectionString);
|
||||||
|
connection.Open();
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1️⃣ 查询(SELECT)
|
||||||
|
|
||||||
|
### 查询所有用户
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using var connection = new OleDbConnection(connectionString);
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
var users = new List<UserInfo>();
|
||||||
|
using var command = new OleDbCommand("SELECT UserName, Password FROM UserInfo", connection);
|
||||||
|
using var reader = command.ExecuteReader();
|
||||||
|
|
||||||
|
while (reader.Read())
|
||||||
|
{
|
||||||
|
var user = new UserInfo
|
||||||
|
{
|
||||||
|
UserName = reader["UserName"]?.ToString() ?? "",
|
||||||
|
Password = reader["Password"]?.ToString() ?? ""
|
||||||
|
};
|
||||||
|
users.Add(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"查询到 {users.Count} 个用户");
|
||||||
|
```
|
||||||
|
|
||||||
|
### 条件查询
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 根据用户名查询
|
||||||
|
using var command = new OleDbCommand(
|
||||||
|
"SELECT UserName, Password FROM UserInfo WHERE UserName = ?", connection);
|
||||||
|
command.Parameters.AddWithValue("@UserName", "admin");
|
||||||
|
|
||||||
|
using var reader = command.ExecuteReader();
|
||||||
|
if (reader.Read())
|
||||||
|
{
|
||||||
|
var userName = reader["UserName"]?.ToString();
|
||||||
|
var password = reader["Password"]?.ToString();
|
||||||
|
Console.WriteLine($"找到用户: {userName}");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 统计数量
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using var command = new OleDbCommand(
|
||||||
|
"SELECT COUNT(*) FROM UserPermission WHERE UserInfo_UserName = ?", connection);
|
||||||
|
command.Parameters.AddWithValue("@UserName", "admin");
|
||||||
|
|
||||||
|
var count = Convert.ToInt32(command.ExecuteScalar());
|
||||||
|
Console.WriteLine($"该用户有 {count} 个权限");
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2️⃣ 添加(INSERT)
|
||||||
|
|
||||||
|
### 添加用户
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using var connection = new OleDbConnection(connectionString);
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
// 1. 先检查用户是否已存在
|
||||||
|
using (var checkCmd = new OleDbCommand(
|
||||||
|
"SELECT COUNT(*) FROM UserInfo WHERE UserName = ?", connection))
|
||||||
|
{
|
||||||
|
checkCmd.Parameters.AddWithValue("@UserName", "newuser");
|
||||||
|
var exists = Convert.ToInt32(checkCmd.ExecuteScalar()) > 0;
|
||||||
|
|
||||||
|
if (exists)
|
||||||
|
{
|
||||||
|
Console.WriteLine("用户已存在!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 插入新用户
|
||||||
|
using (var insertCmd = new OleDbCommand(
|
||||||
|
"INSERT INTO UserInfo (UserName, Password) VALUES (?, ?)", connection))
|
||||||
|
{
|
||||||
|
insertCmd.Parameters.AddWithValue("@UserName", "newuser");
|
||||||
|
insertCmd.Parameters.AddWithValue("@Password", "password123");
|
||||||
|
|
||||||
|
var rowsAffected = insertCmd.ExecuteNonQuery();
|
||||||
|
Console.WriteLine($"成功添加 {rowsAffected} 个用户");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 添加权限
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using var insertCmd = new OleDbCommand(
|
||||||
|
"INSERT INTO UserPermission (UserInfo_UserName, Permission_Code) VALUES (?, ?)", connection);
|
||||||
|
insertCmd.Parameters.AddWithValue("@UserName", "newuser");
|
||||||
|
insertCmd.Parameters.AddWithValue("@PermissionCode", "READ_DATA");
|
||||||
|
|
||||||
|
var rowsAffected = insertCmd.ExecuteNonQuery();
|
||||||
|
Console.WriteLine($"成功添加权限: {rowsAffected} 条");
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3️⃣ 修改(UPDATE)
|
||||||
|
|
||||||
|
### 修改用户密码
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using var connection = new OleDbConnection(connectionString);
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
using var updateCmd = new OleDbCommand(
|
||||||
|
"UPDATE UserInfo SET Password = ? WHERE UserName = ?", connection);
|
||||||
|
updateCmd.Parameters.AddWithValue("@Password", "newpassword");
|
||||||
|
updateCmd.Parameters.AddWithValue("@UserName", "admin");
|
||||||
|
|
||||||
|
var rowsAffected = updateCmd.ExecuteNonQuery();
|
||||||
|
|
||||||
|
if (rowsAffected > 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine("密码修改成功!");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine("用户不存在!");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 批量更新
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 将所有用户的密码更新为默认密码
|
||||||
|
using var updateCmd = new OleDbCommand(
|
||||||
|
"UPDATE UserInfo SET Password = ? WHERE Password IS NULL OR Password = ''", connection);
|
||||||
|
updateCmd.Parameters.AddWithValue("@Password", "default123");
|
||||||
|
|
||||||
|
var rowsAffected = updateCmd.ExecuteNonQuery();
|
||||||
|
Console.WriteLine($"更新了 {rowsAffected} 个用户的密码");
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4️⃣ 删除(DELETE)
|
||||||
|
|
||||||
|
### 删除用户(含事务)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using var connection = new OleDbConnection(connectionString);
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
// 使用事务确保数据一致性
|
||||||
|
using var transaction = connection.BeginTransaction();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 1. 先删除用户的所有权限
|
||||||
|
using (var deletePermCmd = new OleDbCommand(
|
||||||
|
"DELETE FROM UserPermission WHERE UserInfo_UserName = ?", connection, transaction))
|
||||||
|
{
|
||||||
|
deletePermCmd.Parameters.AddWithValue("@UserName", "testuser");
|
||||||
|
var permCount = deletePermCmd.ExecuteNonQuery();
|
||||||
|
Console.WriteLine($"删除了 {permCount} 个权限");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 再删除用户
|
||||||
|
using (var deleteUserCmd = new OleDbCommand(
|
||||||
|
"DELETE FROM UserInfo WHERE UserName = ?", connection, transaction))
|
||||||
|
{
|
||||||
|
deleteUserCmd.Parameters.AddWithValue("@UserName", "testuser");
|
||||||
|
var userCount = deleteUserCmd.ExecuteNonQuery();
|
||||||
|
|
||||||
|
if (userCount == 0)
|
||||||
|
{
|
||||||
|
transaction.Rollback();
|
||||||
|
Console.WriteLine("用户不存在!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"删除了 {userCount} 个用户");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 提交事务
|
||||||
|
transaction.Commit();
|
||||||
|
Console.WriteLine("删除成功!");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// 回滚事务
|
||||||
|
transaction.Rollback();
|
||||||
|
Console.WriteLine($"删除失败: {ex.Message}");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 删除权限
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using var deleteCmd = new OleDbCommand(
|
||||||
|
"DELETE FROM UserPermission WHERE UserInfo_UserName = ? AND Permission_Code = ?", connection);
|
||||||
|
deleteCmd.Parameters.AddWithValue("@UserName", "admin");
|
||||||
|
deleteCmd.Parameters.AddWithValue("@PermissionCode", "DELETE_DATA");
|
||||||
|
|
||||||
|
var rowsAffected = deleteCmd.ExecuteNonQuery();
|
||||||
|
Console.WriteLine($"删除了 {rowsAffected} 个权限");
|
||||||
|
```
|
||||||
|
|
||||||
|
### 批量删除
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 删除所有没有权限的用户
|
||||||
|
using var deleteCmd = new OleDbCommand(@"
|
||||||
|
DELETE FROM UserInfo
|
||||||
|
WHERE UserName NOT IN (
|
||||||
|
SELECT DISTINCT UserInfo_UserName FROM UserPermission
|
||||||
|
)", connection);
|
||||||
|
|
||||||
|
var rowsAffected = deleteCmd.ExecuteNonQuery();
|
||||||
|
Console.WriteLine($"删除了 {rowsAffected} 个无权限用户");
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔥 高级操作
|
||||||
|
|
||||||
|
### 1. JOIN 查询
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 查询用户及其所有权限
|
||||||
|
using var command = new OleDbCommand(@"
|
||||||
|
SELECT u.UserName, u.Password, p.Permission_Code
|
||||||
|
FROM UserInfo u
|
||||||
|
LEFT JOIN UserPermission p ON u.UserName = p.UserInfo_UserName
|
||||||
|
ORDER BY u.UserName", connection);
|
||||||
|
|
||||||
|
using var reader = command.ExecuteReader();
|
||||||
|
while (reader.Read())
|
||||||
|
{
|
||||||
|
var userName = reader["UserName"]?.ToString();
|
||||||
|
var permission = reader["Permission_Code"]?.ToString();
|
||||||
|
Console.WriteLine($"{userName}: {permission ?? "(无权限)"}");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 分组统计
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 统计每个用户的权限数量
|
||||||
|
using var command = new OleDbCommand(@"
|
||||||
|
SELECT u.UserName, COUNT(p.Permission_Code) AS PermCount
|
||||||
|
FROM UserInfo u
|
||||||
|
LEFT JOIN UserPermission p ON u.UserName = p.UserInfo_UserName
|
||||||
|
GROUP BY u.UserName
|
||||||
|
ORDER BY PermCount DESC", connection);
|
||||||
|
|
||||||
|
using var reader = command.ExecuteReader();
|
||||||
|
while (reader.Read())
|
||||||
|
{
|
||||||
|
var userName = reader["UserName"]?.ToString();
|
||||||
|
var count = Convert.ToInt32(reader["PermCount"]);
|
||||||
|
Console.WriteLine($"{userName}: {count} 个权限");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 参数化批量插入
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using var connection = new OleDbConnection(connectionString);
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
var users = new[]
|
||||||
|
{
|
||||||
|
("user1", "pass1"),
|
||||||
|
("user2", "pass2"),
|
||||||
|
("user3", "pass3")
|
||||||
|
};
|
||||||
|
|
||||||
|
using var transaction = connection.BeginTransaction();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var insertCmd = new OleDbCommand(
|
||||||
|
"INSERT INTO UserInfo (UserName, Password) VALUES (?, ?)", connection, transaction);
|
||||||
|
|
||||||
|
foreach (var (userName, password) in users)
|
||||||
|
{
|
||||||
|
insertCmd.Parameters.Clear();
|
||||||
|
insertCmd.Parameters.AddWithValue("@UserName", userName);
|
||||||
|
insertCmd.Parameters.AddWithValue("@Password", password);
|
||||||
|
insertCmd.ExecuteNonQuery();
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction.Commit();
|
||||||
|
Console.WriteLine($"成功添加 {users.Length} 个用户");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
transaction.Rollback();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 存在性检查
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 检查用户是否存在
|
||||||
|
public bool UserExists(string userName)
|
||||||
|
{
|
||||||
|
using var connection = new OleDbConnection(connectionString);
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
using var command = new OleDbCommand(
|
||||||
|
"SELECT COUNT(*) FROM UserInfo WHERE UserName = ?", connection);
|
||||||
|
command.Parameters.AddWithValue("@UserName", userName);
|
||||||
|
|
||||||
|
return Convert.ToInt32(command.ExecuteScalar()) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用
|
||||||
|
if (UserExists("admin"))
|
||||||
|
{
|
||||||
|
Console.WriteLine("用户存在");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 最佳实践
|
||||||
|
|
||||||
|
### 1. 使用 using 语句
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// ✅ 推荐:自动释放资源
|
||||||
|
using var connection = new OleDbConnection(connectionString);
|
||||||
|
using var command = new OleDbCommand(sql, connection);
|
||||||
|
using var reader = command.ExecuteReader();
|
||||||
|
|
||||||
|
// ❌ 不推荐:需要手动释放
|
||||||
|
var connection = new OleDbConnection(connectionString);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
connection.Dispose();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 使用参数化查询(防止 SQL 注入)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// ✅ 推荐:参数化查询
|
||||||
|
var sql = "SELECT * FROM UserInfo WHERE UserName = ?";
|
||||||
|
command.Parameters.AddWithValue("@UserName", userName);
|
||||||
|
|
||||||
|
// ❌ 危险:字符串拼接(SQL 注入风险)
|
||||||
|
var sql = $"SELECT * FROM UserInfo WHERE UserName = '{userName}'";
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 使用事务保证数据一致性
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// ✅ 推荐:使用事务
|
||||||
|
using var transaction = connection.BeginTransaction();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 多个操作
|
||||||
|
command1.ExecuteNonQuery();
|
||||||
|
command2.ExecuteNonQuery();
|
||||||
|
transaction.Commit();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
transaction.Rollback();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 异常处理
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var connection = new OleDbConnection(connectionString);
|
||||||
|
connection.Open();
|
||||||
|
// 数据库操作
|
||||||
|
}
|
||||||
|
catch (OleDbException ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"数据库错误: {ex.Message}");
|
||||||
|
// 记录日志
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"未知错误: {ex.Message}");
|
||||||
|
// 记录日志
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 完整示例项目
|
||||||
|
|
||||||
|
完整的 CRUD 操作已集成到 **Server 项目**中:
|
||||||
|
|
||||||
|
**文件位置**:
|
||||||
|
- UI: `Server/Views/AccessDatabaseWindowOleDb.xaml`
|
||||||
|
- 代码: `Server/Views/AccessDatabaseWindowOleDb.xaml.cs`
|
||||||
|
|
||||||
|
**功能列表**:
|
||||||
|
- ✅ 查询所有用户
|
||||||
|
- ✅ 添加新用户
|
||||||
|
- ✅ 修改用户密码
|
||||||
|
- ✅ 删除用户(含级联删除权限)
|
||||||
|
- ✅ 添加用户权限
|
||||||
|
- ✅ 删除用户权限
|
||||||
|
- ✅ 事务支持
|
||||||
|
- ✅ 异常处理
|
||||||
|
- ✅ 异步操作(不阻塞 UI)
|
||||||
|
|
||||||
|
**运行方式**:
|
||||||
|
1. 启动 Server 项目
|
||||||
|
2. 点击 "Access数据库" 按钮
|
||||||
|
3. 尽情测试增删改查功能!
|
||||||
|
|
||||||
|
数据库随意修改,不用担心!🎉
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
# Access 数据库访问方案对比
|
||||||
|
|
||||||
|
## 方案选择
|
||||||
|
|
||||||
|
### ✅ 方案 A:System.Data.OleDb(推荐)
|
||||||
|
|
||||||
|
**适用场景**:
|
||||||
|
- 简单的增删改查操作
|
||||||
|
- 性能要求高
|
||||||
|
- 需要稳定可靠的方案
|
||||||
|
- 不需要复杂的 LINQ 查询
|
||||||
|
|
||||||
|
**实现文件**:
|
||||||
|
- `Views/AccessDatabaseWindowOleDb.xaml`
|
||||||
|
- `Views/AccessDatabaseWindowOleDb.xaml.cs`
|
||||||
|
|
||||||
|
**示例代码**:
|
||||||
|
```csharp
|
||||||
|
// 1. 创建连接
|
||||||
|
var connectionString = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=dbDwDmsSetup.accdb";
|
||||||
|
using var connection = new OleDbConnection(connectionString);
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
// 2. 查询数据
|
||||||
|
using var command = new OleDbCommand("SELECT UserName, Password FROM UserInfo", connection);
|
||||||
|
using var reader = command.ExecuteReader();
|
||||||
|
while (reader.Read())
|
||||||
|
{
|
||||||
|
var userName = reader["UserName"]?.ToString();
|
||||||
|
var password = reader["Password"]?.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 插入数据
|
||||||
|
using var insertCmd = new OleDbCommand(
|
||||||
|
"INSERT INTO UserInfo (UserName, Password) VALUES (?, ?)", connection);
|
||||||
|
insertCmd.Parameters.AddWithValue("@UserName", "newuser");
|
||||||
|
insertCmd.Parameters.AddWithValue("@Password", "password");
|
||||||
|
insertCmd.ExecuteNonQuery();
|
||||||
|
|
||||||
|
// 4. 更新数据
|
||||||
|
using var updateCmd = new OleDbCommand(
|
||||||
|
"UPDATE UserInfo SET Password = ? WHERE UserName = ?", connection);
|
||||||
|
updateCmd.Parameters.AddWithValue("@Password", "newpassword");
|
||||||
|
updateCmd.Parameters.AddWithValue("@UserName", "username");
|
||||||
|
updateCmd.ExecuteNonQuery();
|
||||||
|
|
||||||
|
// 5. 删除数据
|
||||||
|
using var deleteCmd = new OleDbCommand(
|
||||||
|
"DELETE FROM UserInfo WHERE UserName = ?", connection);
|
||||||
|
deleteCmd.Parameters.AddWithValue("@UserName", "username");
|
||||||
|
deleteCmd.ExecuteNonQuery();
|
||||||
|
```
|
||||||
|
|
||||||
|
**优点**:
|
||||||
|
- ✅ 微软官方支持
|
||||||
|
- ✅ 无需额外 NuGet 包
|
||||||
|
- ✅ 性能最佳
|
||||||
|
- ✅ 稳定可靠
|
||||||
|
- ✅ 适用于所有 .NET 版本
|
||||||
|
|
||||||
|
**缺点**:
|
||||||
|
- ❌ 需要手写 SQL
|
||||||
|
- ❌ 没有 LINQ 支持
|
||||||
|
- ❌ 没有自动实体映射
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 方案 B:EntityFrameworkCore.Jet
|
||||||
|
|
||||||
|
**适用场景**:
|
||||||
|
- 需要复杂的 LINQ 查询
|
||||||
|
- 需要实体关系映射
|
||||||
|
- 代码优雅度要求高
|
||||||
|
- 可以接受第三方库
|
||||||
|
|
||||||
|
**实现文件**:
|
||||||
|
- `Views/AccessDatabaseWindow.xaml`
|
||||||
|
- `Views/AccessDatabaseWindow.xaml.cs`
|
||||||
|
- `Data/Access/AccessEntities.cs`
|
||||||
|
- `Data/Access/AccessDbContext.cs`
|
||||||
|
|
||||||
|
**示例代码**:
|
||||||
|
```csharp
|
||||||
|
// 1. 创建 DbContext
|
||||||
|
var optionsBuilder = new DbContextOptionsBuilder<AccessDbContext>();
|
||||||
|
optionsBuilder.UseJet("Provider=Microsoft.ACE.OLEDB.12.0;Data Source=dbDwDmsSetup.accdb");
|
||||||
|
using var dbContext = new AccessDbContext(optionsBuilder.Options);
|
||||||
|
|
||||||
|
// 2. LINQ 查询
|
||||||
|
var users = await dbContext.UserInfos
|
||||||
|
.Include(u => u.Permissions)
|
||||||
|
.Where(u => u.UserName.Contains("admin"))
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
// 3. 插入数据
|
||||||
|
var newUser = new UserInfo { UserName = "newuser", Password = "password" };
|
||||||
|
dbContext.UserInfos.Add(newUser);
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
// 4. 更新数据
|
||||||
|
var user = await dbContext.UserInfos.FindAsync("username");
|
||||||
|
if (user != null)
|
||||||
|
{
|
||||||
|
user.Password = "newpassword";
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 删除数据
|
||||||
|
var userToDelete = await dbContext.UserInfos.FindAsync("username");
|
||||||
|
if (userToDelete != null)
|
||||||
|
{
|
||||||
|
dbContext.UserInfos.Remove(userToDelete);
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**优点**:
|
||||||
|
- ✅ LINQ 查询支持
|
||||||
|
- ✅ 自动实体映射
|
||||||
|
- ✅ 代码更优雅
|
||||||
|
- ✅ 支持导航属性
|
||||||
|
|
||||||
|
**缺点**:
|
||||||
|
- ❌ 第三方库(EntityFrameworkCore.Jet)
|
||||||
|
- ❌ 版本兼容性问题
|
||||||
|
- ❌ 性能开销
|
||||||
|
- ❌ 可能与其他 EF Core 提供程序冲突
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 推荐方案
|
||||||
|
|
||||||
|
### 对于简单的增删改查:
|
||||||
|
|
||||||
|
**推荐使用方案 A(System.Data.OleDb)**
|
||||||
|
|
||||||
|
理由:
|
||||||
|
1. 稳定可靠,微软官方支持
|
||||||
|
2. 性能最佳
|
||||||
|
3. 无依赖问题
|
||||||
|
4. 代码简单直接
|
||||||
|
|
||||||
|
### 对于复杂的业务逻辑:
|
||||||
|
|
||||||
|
可以考虑方案 B(EntityFrameworkCore.Jet),但需要:
|
||||||
|
1. 接受第三方库的风险
|
||||||
|
2. 处理版本兼容性问题
|
||||||
|
3. 确保不与其他数据库提供程序冲突
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 当前 Server 项目使用的方案
|
||||||
|
|
||||||
|
当前使用:**方案 A(System.Data.OleDb)** ✅
|
||||||
|
|
||||||
|
主窗口按钮 "Access数据库" 打开的是 `AccessDatabaseWindowOleDb` 窗口。
|
||||||
|
|
||||||
|
如需切换到方案 B,修改 `MainWindow.xaml.cs` 中的 `AccessDbButton_Click` 方法:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 方案 A(当前)
|
||||||
|
var window = new AccessDatabaseWindowOleDb { Owner = this };
|
||||||
|
|
||||||
|
// 方案 B
|
||||||
|
var window = new AccessDatabaseWindow { Owner = this };
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 性能对比
|
||||||
|
|
||||||
|
| 操作 | OleDb | EF Core Jet |
|
||||||
|
|------|-------|-------------|
|
||||||
|
| 简单查询 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
|
||||||
|
| 复杂查询 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
|
||||||
|
| 插入操作 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
|
||||||
|
| 批量操作 | ⭐⭐⭐⭐⭐ | ⭐⭐ |
|
||||||
|
| 内存占用 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
对于你的需求(简单的增删改查),**强烈推荐使用 System.Data.OleDb(方案 A)**。
|
||||||
|
|
||||||
|
它是最稳定、性能最好、最简单的方案。
|
||||||
@@ -55,6 +55,8 @@
|
|||||||
Background="#FF9800" Foreground="White" BorderThickness="0" FontWeight="Bold" Cursor="Hand" IsEnabled="False"/>
|
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"
|
<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"/>
|
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"
|
<Button x:Name="ClearLogButton" Content="清空日志" Width="100" Height="32" Click="ClearLogButton_Click"
|
||||||
Background="#9E9E9E" Foreground="White" BorderThickness="0" FontWeight="Bold" Cursor="Hand"/>
|
Background="#9E9E9E" Foreground="White" BorderThickness="0" FontWeight="Bold" Cursor="Hand"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|||||||
@@ -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)
|
private void OnServerStatusChanged(object? sender, string status)
|
||||||
{
|
{
|
||||||
Dispatcher.Invoke(() =>
|
Dispatcher.Invoke(() =>
|
||||||
|
|||||||
@@ -7,6 +7,9 @@
|
|||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<UseWPF>true</UseWPF>
|
<UseWPF>true</UseWPF>
|
||||||
|
|
||||||
|
<!-- 强制使用 x64 架构,与已安装的 64 位 ACE 驱动匹配 -->
|
||||||
|
<PlatformTarget>x64</PlatformTarget>
|
||||||
|
|
||||||
<PublishSingleFile>true</PublishSingleFile>
|
<PublishSingleFile>true</PublishSingleFile>
|
||||||
<SelfContained>true</SelfContained>
|
<SelfContained>true</SelfContained>
|
||||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||||
@@ -22,6 +25,7 @@
|
|||||||
<ProjectReference Include="..\Wpf\Wpf.csproj" />
|
<ProjectReference Include="..\Wpf\Wpf.csproj" />
|
||||||
<ProjectReference Include="..\Common\Common.csproj" />
|
<ProjectReference Include="..\Common\Common.csproj" />
|
||||||
<ProjectReference Include="..\Data\Data.csproj" />
|
<ProjectReference Include="..\Data\Data.csproj" />
|
||||||
|
<ProjectReference Include="..\Data.Access\Data.Access.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -34,6 +38,9 @@
|
|||||||
<None Update="appsettings.json">
|
<None Update="appsettings.json">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</None>
|
</None>
|
||||||
|
<None Update="dbDwDmsSetup.accdb">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</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; } = "";
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
},
|
},
|
||||||
"SharpLog": {
|
"SharpLog": {
|
||||||
"UseConsole": true,
|
"UseConsole": true,
|
||||||
"MinLevel": "Information",
|
"MinLevel": "Debug",
|
||||||
"File": {
|
"File": {
|
||||||
"Enabled": true,
|
"Enabled": true,
|
||||||
"Path": "logs/server-.log",
|
"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"}}
|
||||||
@@ -39,6 +39,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Data", "Data\Data.csproj",
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Data.Test", "..\..\test\Data.Test\Data.Test.csproj", "{8EEC8BF4-CF9A-473D-B7A0-005E151C645D}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Data.Test", "..\..\test\Data.Test\Data.Test.csproj", "{8EEC8BF4-CF9A-473D-B7A0-005E151C645D}"
|
||||||
EndProject
|
EndProject
|
||||||
|
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}") = "Data.Access.Test", "..\..\test\Data.Access.Test\Data.Access.Test.csproj", "{48A70F46-8764-4FEA-BAFA-42776B0B526F}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -117,6 +121,14 @@ Global
|
|||||||
{8EEC8BF4-CF9A-473D-B7A0-005E151C645D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{8EEC8BF4-CF9A-473D-B7A0-005E151C645D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{8EEC8BF4-CF9A-473D-B7A0-005E151C645D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{8EEC8BF4-CF9A-473D-B7A0-005E151C645D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{8EEC8BF4-CF9A-473D-B7A0-005E151C645D}.Release|Any CPU.Build.0 = Release|Any CPU
|
{8EEC8BF4-CF9A-473D-B7A0-005E151C645D}.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
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ public sealed class ExceptionHandlingOptions
|
|||||||
public bool RethrowAfterHandle { get; set; } = false;
|
public bool RethrowAfterHandle { get; set; } = false;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Custom logger hook.
|
/// 日志钩子.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Action<Exception>? Log { get; set; }
|
public Action<Exception>? Log { get; set; }
|
||||||
= ex => System.Diagnostics.Debug.WriteLine(ex.ToString());
|
= ex => System.Diagnostics.Debug.WriteLine(ex.ToString());
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Net.Http;
|
||||||
|
|
||||||
namespace Sharp.Wpf.Exceptions;
|
namespace Sharp.Wpf.Exceptions;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Net.Http;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using WpfMessageBox = System.Windows.MessageBox;
|
using WpfMessageBox = System.Windows.MessageBox;
|
||||||
|
|
||||||
@@ -35,6 +36,10 @@ public sealed class UiExceptionHandler : IUiExceptionHandler
|
|||||||
private void OnDispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e)
|
private void OnDispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e)
|
||||||
{
|
{
|
||||||
Handle(e.Exception);
|
Handle(e.Exception);
|
||||||
|
if (CanRecover(e.Exception))
|
||||||
|
{
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
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>
|
// <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>
|
// </auto-generated>
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
@@ -14,7 +13,7 @@ using System.Reflection;
|
|||||||
[assembly: System.Reflection.AssemblyCompanyAttribute("Sharp.Wpf")]
|
[assembly: System.Reflection.AssemblyCompanyAttribute("Sharp.Wpf")]
|
||||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
[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.AssemblyProductAttribute("Sharp.Wpf")]
|
||||||
[assembly: System.Reflection.AssemblyTitleAttribute("Sharp.Wpf")]
|
[assembly: System.Reflection.AssemblyTitleAttribute("Sharp.Wpf")]
|
||||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
[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"
|
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.203/PortableRuntimeIdentifierGraph.json"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"runtimes": {
|
|
||||||
"win-x64": {
|
|
||||||
"#import": []
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"targets": {
|
"targets": {
|
||||||
"net8.0-windows7.0": {},
|
"net8.0-windows7.0": {}
|
||||||
"net8.0-windows7.0/win-x64": {}
|
|
||||||
},
|
},
|
||||||
"libraries": {},
|
"libraries": {},
|
||||||
"projectFileDependencyGroups": {
|
"projectFileDependencyGroups": {
|
||||||
@@ -78,11 +77,6 @@
|
|||||||
},
|
},
|
||||||
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.203/PortableRuntimeIdentifierGraph.json"
|
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.203/PortableRuntimeIdentifierGraph.json"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"runtimes": {
|
|
||||||
"win-x64": {
|
|
||||||
"#import": []
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"dgSpecHash": "9ZityDkLrvk=",
|
"dgSpecHash": "EQ+I1wXrehg=",
|
||||||
"success": true,
|
"success": true,
|
||||||
"projectFilePath": "D:\\yuanll\\sharp-basic\\src\\Sharp\\Wpf\\Wpf.csproj",
|
"projectFilePath": "D:\\yuanll\\sharp-basic\\src\\Sharp\\Wpf\\Wpf.csproj",
|
||||||
"expectedPackageFiles": [],
|
"expectedPackageFiles": [],
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
17634268840015055
|
17634526945569668
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -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 模式");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
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.Usage;
|
||||||
|
|
||||||
|
[TestClass]
|
||||||
|
public sealed class AccessUsageSampleTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 演示如何在无 DI 场景下使用 DataContextBuilder 配置 Access。
|
||||||
|
/// 这里只验证 API 能够被调用(不真正连接数据库)。
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void ManualBuilder_AllowsAccessConfigurationWithoutExceptions()
|
||||||
|
{
|
||||||
|
var builder = new DataContextBuilder<FakeContext>();
|
||||||
|
|
||||||
|
builder.UseAccessDatabase(options =>
|
||||||
|
{
|
||||||
|
options.ConnectionString = $"Provider=Microsoft.ACE.OLEDB.16.0;Data Source=fake.accdb;";
|
||||||
|
options.CopyTemplateWhenMissing = false;
|
||||||
|
options.ThrowIfMissing = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 不调用 Build(),避免实际访问文件;只要不抛出异常即为通过。
|
||||||
|
Assert.IsTrue(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class FakeContext : DbContext
|
||||||
|
{
|
||||||
|
public FakeContext(DbContextOptions<FakeContext> options) : base(options)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
Reference in new issue
Block a user