Compare commits

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

No files matched your search

+4
View File
@@ -10,3 +10,7 @@
</ResourceDictionary>
</Application.Resources>
</Application>
+21 -4
View File
@@ -2,7 +2,9 @@ using System;
using System.Windows;
using Client.Services;
using Client.ViewModels;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using Common;
using Config;
@@ -10,6 +12,7 @@ using NetClient.DependencyInjection;
using Sharp.Infrastructure.Configuration;
using Sharp.Log.Extensions;
using Sharp.NetServer.DependencyInjection;
using Sharp.Wpf.Exceptions;
using Sharp.Wpf.Services;
namespace Client
@@ -24,7 +27,7 @@ namespace Client
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
// 1) Build configuration manager from appsettings.json (+ reload on change + env vars)
var cm = ConfigBuilder.BuildFromFiles(new[] { "appsettings.json" }, reloadOnChange: true, includeEnvironment: true, basePath: AppContext.BaseDirectory);
@@ -33,6 +36,7 @@ namespace Client
// ConfigManager and typed SystemConfig accessor
services.AddSingleton(cm);
services.TryAddSingleton<IConfiguration>(cm.Configuration); // Register IConfiguration for BindConfiguration
services.AddSystemConfig(cm);
// Logging (binds from section "SharpLog")
@@ -42,6 +46,9 @@ namespace Client
services.AddNetClient(cm);
services.AddNetServer(cm);
// WPF exception handler
services.AddSingleton<IUiExceptionHandler, UiExceptionHandler>();
// WPF app services
services.AddSingleton<IDialogService, DialogService>();
services.AddSingleton<IAuthenticationService, DemoAuthenticationService>();
@@ -54,22 +61,32 @@ namespace Client
_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(cm);
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 loginResult = loginWindow.ShowDialog();
logger.LogInformation("Login window closed with result: {LoginResult}", loginResult);
if (loginResult == true)
{
logger.LogInformation("Login successful, showing main window");
var main = _serviceProvider.GetRequiredService<MainWindow>();
main.Show();
}
else
{
logger.LogInformation("Login cancelled or failed, shutting down application");
Shutdown();
}
}
@@ -80,4 +97,4 @@ namespace Client
base.OnExit(e);
}
}
}
}
+5 -1
View File
@@ -1,23 +1,27 @@
using System;
using HandyControl.Controls;
using Client.ViewModels;
using Microsoft.Extensions.Logging;
namespace Client;
public partial class LoginWindow : GlowWindow
{
private readonly LoginViewModel _viewModel;
private readonly ILogger<LoginWindow>? _logger;
public LoginWindow(LoginViewModel viewModel)
public LoginWindow(LoginViewModel viewModel, ILogger<LoginWindow>? logger = null)
{
InitializeComponent();
_viewModel = viewModel;
_logger = logger;
DataContext = viewModel;
_viewModel.LoginSucceeded += HandleLoginSucceeded;
}
private void HandleLoginSucceeded(object? sender, EventArgs e)
{
_logger?.LogInformation("HandleLoginSucceeded called, setting DialogResult = true");
DialogResult = true;
}
+14 -2
View File
@@ -1,6 +1,7 @@
using System;
using System.Threading.Tasks;
using Client.Services;
using Microsoft.Extensions.Logging;
using Sharp.Wpf.Commands;
using Sharp.Wpf.Mvvm;
@@ -10,6 +11,7 @@ public sealed class LoginViewModel : ViewModelBase
{
private readonly IAuthenticationService _authenticationService;
private readonly IUserSession _userSession;
private readonly ILogger<LoginViewModel> _logger;
private readonly IAsyncCommand _loginCommand;
private string _userName = "admin";
@@ -18,10 +20,11 @@ public sealed class LoginViewModel : ViewModelBase
public event EventHandler? LoginSucceeded;
public LoginViewModel(IAuthenticationService authenticationService, IUserSession userSession)
public LoginViewModel(IAuthenticationService authenticationService, IUserSession userSession, ILogger<LoginViewModel> logger)
{
_authenticationService = authenticationService;
_userSession = userSession;
_logger = logger;
_loginCommand = new AsyncCommand(LoginAsync);
}
@@ -67,20 +70,29 @@ public sealed class LoginViewModel : ViewModelBase
{
try
{
_logger.LogInformation("Login attempt for user: {UserName}", UserName);
IsBusy = true;
ErrorMessage = null;
var result = await _authenticationService.LoginAsync(UserName, Password).ConfigureAwait(false);
var result = await _authenticationService.LoginAsync(UserName, Password);
if (!result.Succeeded)
{
_logger.LogWarning("Login failed for user: {UserName}, Error: {Error}", UserName, result.Error);
ErrorMessage = result.Error;
return;
}
_logger.LogInformation("Login succeeded for user: {UserName}", UserName);
_userSession.SignIn(UserName, result.DisplayName);
_logger.LogInformation("Invoking LoginSucceeded event");
LoginSucceeded?.Invoke(this, EventArgs.Empty);
}
catch (Exception ex)
{
_logger.LogError(ex, "Login failed for user: {UserName}", UserName);
ErrorMessage = "An unexpected error occurred.";
}
finally
{
IsBusy = false;
@@ -1,9 +1,10 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// 此代码由工具生成。
// 运行时版本:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// 对此文件的更改可能会导致不正确的行为,并且如果
// 重新生成代码,这些更改将会丢失。
// </auto-generated>
//------------------------------------------------------------------------------
@@ -13,7 +14,7 @@ using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("Client")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+53d1dd2dbf05d818528814223b58610a790fcb26")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+f7660c49e81d5fe2747f6514545ceb961c2066d3")]
[assembly: System.Reflection.AssemblyProductAttribute("Client")]
[assembly: System.Reflection.AssemblyTitleAttribute("Client")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
@@ -1 +1 @@
f8321da7c6a777fc7770c7234548af806317c95cd5f1beb1c1743888aa1bde11
eeea53cf7db257ee716efdd2fdcdb5f1a810b49927811d410a05e1ff19899369
+503
View File
@@ -0,0 +1,503 @@
using System.Data;
using System.Data.OleDb;
using System.Linq;
using System.Reflection;
using Microsoft.Extensions.Logging;
using Sharp.Data.Access.Connections;
using Sharp.Data.Access.Initialization;
using Sharp.Data.Access.Internal;
using Sharp.Data.Access.Options;
using Sharp.Data.Access.Security;
namespace Sharp.Data.Access;
/// <summary>
/// Access 数据库事务实现
/// </summary>
internal class AccessTransaction : IAccessTransaction
{
private readonly OleDbConnection _connection;
private readonly OleDbTransaction _transaction;
private readonly ILogger? _logger;
private readonly bool _ownsConnection;
private bool _disposed;
public AccessTransaction(OleDbConnection connection, bool ownsConnection, ILogger? logger)
{
_connection = connection;
_ownsConnection = ownsConnection;
_transaction = connection.BeginTransaction();
_logger = logger;
}
public int Execute(string sql, params OleDbParameter[] parameters)
{
using var command = new OleDbCommand(sql, _connection, _transaction);
if (parameters.Length > 0)
command.Parameters.AddRange(parameters);
return command.ExecuteNonQuery();
}
public List<T> Query<T>(string sql, params OleDbParameter[] parameters) where T : new()
{
using var command = new OleDbCommand(sql, _connection, _transaction);
if (parameters.Length > 0)
command.Parameters.AddRange(parameters);
var results = new List<T>();
using var reader = command.ExecuteReader();
while (reader.Read())
{
var item = MapReaderToObject<T>(reader);
results.Add(item);
}
return results;
}
public object? ExecuteScalar(string sql, params OleDbParameter[] parameters)
{
using var command = new OleDbCommand(sql, _connection, _transaction);
if (parameters.Length > 0)
command.Parameters.AddRange(parameters);
return command.ExecuteScalar();
}
public void Commit()
{
_transaction.Commit();
_logger?.LogDebug("事务已提交");
}
public void Rollback()
{
_transaction.Rollback();
_logger?.LogDebug("事务已回滚");
}
public void Dispose()
{
if (_disposed) return;
try
{
_transaction.Dispose();
}
finally
{
if (_ownsConnection)
{
_connection.Dispose();
}
}
_disposed = true;
}
private static T MapReaderToObject<T>(IDataReader reader) where T : new()
{
var obj = new T();
var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
// 创建字段名到索引的映射(大小写不敏感)
var fieldMap = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
for (int i = 0; i < reader.FieldCount; i++)
{
var fieldName = reader.GetName(i);
fieldMap[fieldName] = i;
// Access 列名有时包含末尾空格,提前做一次修剪以便映射
var trimmed = fieldName.Trim();
if (!fieldMap.ContainsKey(trimmed))
{
fieldMap[trimmed] = i;
}
}
foreach (var property in properties)
{
if (!property.CanWrite) continue;
try
{
// 尝试查找匹配的字段(大小写不敏感)
if (fieldMap.TryGetValue(property.Name, out int ordinal))
{
var value = reader.GetValue(ordinal);
if (value != DBNull.Value && value != null)
{
// 尝试类型转换
if (property.PropertyType == value.GetType())
{
property.SetValue(obj, value);
}
else if (property.PropertyType == typeof(string))
{
property.SetValue(obj, value.ToString());
}
else
{
// 尝试使用 Convert.ChangeType
var convertedValue = Convert.ChangeType(value, property.PropertyType);
property.SetValue(obj, convertedValue);
}
}
}
}
catch
{
// 字段不存在或类型转换失败,保持默认值
}
}
return obj;
}
}
/// <summary>
/// Access 数据服务实现(基于 System.Data.OleDb)
/// </summary>
public class AccessDataService : IAccessDataService
{
private readonly AccessDatabaseOptions _options;
private readonly IAccessConnectionStringFactory _connectionFactory;
private readonly IAccessDatabaseInitializer _initializer;
private readonly IAccessPasswordDecryptor _decryptor;
private readonly ILogger<AccessDataService>? _logger;
private readonly string _connectionString;
public AccessDataService(
AccessDatabaseOptions options,
IAccessConnectionStringFactory? connectionFactory = null,
IAccessDatabaseInitializer? initializer = null,
IAccessPasswordDecryptor? decryptor = null,
ILogger<AccessDataService>? logger = null)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
_options.Validate();
_connectionFactory = connectionFactory ?? new AccessConnectionStringFactory(decryptor ?? new PassthroughAccessPasswordDecryptor());
_initializer = initializer ?? new AccessDatabaseInitializer();
_decryptor = decryptor ?? new PassthroughAccessPasswordDecryptor();
_logger = logger;
try
{
// 确保数据库存在(如果配置了模板复制)
_initializer.EnsureDatabase(_options, NullServiceProvider.Instance);
// 构建连接字符串
_connectionString = _connectionFactory.Create(_options, NullServiceProvider.Instance);
_logger?.LogInformation("AccessDataService 已初始化,数据库: {DatabaseFile}, Provider: {Provider}",
_options.DatabaseFile, _options.ProviderName);
_logger?.LogDebug("实际连接字符串: {ConnectionString}", _connectionString);
}
catch (Exception ex)
{
_logger?.LogError(ex, "AccessDataService 初始化失败");
// 提供详细的错误诊断信息
if (ex.Message.Contains("找不到") || ex is FileNotFoundException)
{
throw new InvalidOperationException(
$"找不到 Access 数据库文件: {_options.DatabaseFile}。" +
$"请确保文件存在或配置 TemplateFile 以自动创建。", ex);
}
throw;
}
}
public List<T> Query<T>(string sql, params OleDbParameter[] parameters) where T : new()
{
_logger?.LogDebug("执行查询: {Sql}", sql);
return ExecuteWithRetry(() =>
{
using var connection = CreateConnection();
connection.Open();
using var command = new OleDbCommand(sql, connection);
if (parameters.Length > 0)
{
// 克隆参数以避免重试时的"已包含在另一个集合中"错误
var clonedParams = parameters.Select(p => CloneParameter(p)).ToArray();
command.Parameters.AddRange(clonedParams);
}
var results = new List<T>();
using var reader = command.ExecuteReader();
while (reader.Read())
{
var item = MapReaderToObject<T>(reader);
results.Add(item);
}
_logger?.LogDebug("查询返回 {Count} 条记录", results.Count);
return results;
});
}
public T? QueryFirstOrDefault<T>(string sql, params OleDbParameter[] parameters) where T : new()
{
_logger?.LogDebug("执行单条查询: {Sql}", sql);
return ExecuteWithRetry(() =>
{
using var connection = CreateConnection();
connection.Open();
using var command = new OleDbCommand(sql, connection);
if (parameters.Length > 0)
{
// 克隆参数以避免重试时的"已包含在另一个集合中"错误
var clonedParams = parameters.Select(p => CloneParameter(p)).ToArray();
command.Parameters.AddRange(clonedParams);
}
using var reader = command.ExecuteReader();
if (reader.Read())
{
return MapReaderToObject<T>(reader);
}
return default;
});
}
public object? ExecuteScalar(string sql, params OleDbParameter[] parameters)
{
_logger?.LogDebug("执行标量查询: {Sql}", sql);
return ExecuteWithRetry(() =>
{
using var connection = CreateConnection();
connection.Open();
using var command = new OleDbCommand(sql, connection);
if (parameters.Length > 0)
{
// 克隆参数以避免重试时的"已包含在另一个集合中"错误
var clonedParams = parameters.Select(p => CloneParameter(p)).ToArray();
command.Parameters.AddRange(clonedParams);
}
var result = command.ExecuteScalar();
_logger?.LogDebug("标量查询返回: {Result}", result);
return result;
});
}
public int Execute(string sql, params OleDbParameter[] parameters)
{
_logger?.LogDebug("执行命令: {Sql}", sql);
return ExecuteWithRetry(() =>
{
using var connection = CreateConnection();
connection.Open();
using var command = new OleDbCommand(sql, connection);
if (parameters.Length > 0)
{
// 克隆参数以避免重试时的"已包含在另一个集合中"错误
var clonedParams = parameters.Select(p => CloneParameter(p)).ToArray();
command.Parameters.AddRange(clonedParams);
}
var rowsAffected = command.ExecuteNonQuery();
_logger?.LogDebug("命令影响了 {RowsAffected} 行", rowsAffected);
return rowsAffected;
});
}
public IAccessTransaction BeginTransaction()
{
_logger?.LogDebug("开始事务");
var connection = CreateConnection();
connection.Open();
return new AccessTransaction(connection, ownsConnection: true, _logger);
}
public void ExecuteInTransaction(Action<IAccessTransaction> action)
{
_logger?.LogDebug("在事务中执行操作");
using var transaction = BeginTransaction();
try
{
action(transaction);
transaction.Commit();
_logger?.LogDebug("事务执行成功");
}
catch (Exception ex)
{
_logger?.LogError(ex, "事务执行失败,正在回滚");
transaction.Rollback();
throw;
}
}
public bool TestConnection()
{
try
{
_logger?.LogDebug("测试数据库连接");
using var connection = CreateConnection();
connection.Open();
_logger?.LogDebug("数据库连接成功");
return true;
}
catch (Exception ex)
{
_logger?.LogError(ex, "数据库连接失败");
return false;
}
}
private OleDbConnection CreateConnection()
{
return new OleDbConnection(_connectionString);
}
private T ExecuteWithRetry<T>(Func<T> operation)
{
var maxRetries = _options.MaxRetryCount;
var retryDelay = _options.RetryDelayMilliseconds;
for (int attempt = 0; attempt <= maxRetries; attempt++)
{
try
{
return operation();
}
catch (OleDbException ex) when (attempt < maxRetries && IsRetriableError(ex))
{
_logger?.LogWarning(ex, "数据库操作失败,正在重试 ({Attempt}/{MaxRetries})...", attempt + 1, maxRetries);
Thread.Sleep(retryDelay);
}
catch (OleDbException ex)
{
// 提供详细的错误诊断信息
var errorMessage = $"Access 数据库操作失败: {ex.Message}";
// 检查是否是 Provider 相关的错误
if (ex.Message.Contains("找不到") || ex.Message.Contains("未注册") ||
ex.Message.Contains("OLE DB") || ex.Message.Contains("多步"))
{
errorMessage += $"\n\n可能的原因:" +
$"\n1. 未安装 Access Database Engine:当前使用 Provider = {_options.ProviderName}" +
$"\n - 如果使用 16.0,需要安装 Access Database Engine 2016 或 Office 2016+" +
$"\n - 如果使用 12.0,需要安装 Access Database Engine 2010 或 Office 2010+" +
$"\n - 建议:优先使用 12.0 版本,兼容性最好" +
$"\n2. 32位/64位不匹配:进程架构必须与 Access Driver 架构一致" +
$"\n3. 数据库文件损坏或被其他程序锁定" +
$"\n4. 数据库文件路径不正确:{_options.DatabaseFile}";
_logger?.LogError(ex, "数据库连接失败,Provider: {Provider}", _options.ProviderName);
}
throw new InvalidOperationException(errorMessage, ex);
}
}
// 不会到这里,因为最后一次尝试会抛出异常
throw new InvalidOperationException("重试逻辑错误");
}
private static bool IsRetriableError(OleDbException ex)
{
// Access 数据库常见的可重试错误
// -2147467259: 数据库被锁定
// -2147217865: 无法打开数据库(可能正在被其他进程使用)
return ex.ErrorCode == -2147467259 || ex.ErrorCode == -2147217865;
}
/// <summary>
/// 克隆 OleDbParameter 以避免重试时参数被重复添加的问题
/// </summary>
private static OleDbParameter CloneParameter(OleDbParameter original)
{
var clone = new OleDbParameter
{
ParameterName = original.ParameterName,
Value = original.Value,
OleDbType = original.OleDbType,
Size = original.Size,
Direction = original.Direction,
IsNullable = original.IsNullable,
Precision = original.Precision,
Scale = original.Scale,
SourceColumn = original.SourceColumn,
SourceVersion = original.SourceVersion
};
return clone;
}
private static T MapReaderToObject<T>(IDataReader reader) where T : new()
{
var obj = new T();
var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
// 创建字段名到索引的映射(大小写不敏感)
var fieldMap = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
for (int i = 0; i < reader.FieldCount; i++)
{
var fieldName = reader.GetName(i);
fieldMap[fieldName] = i;
// Access 列名有时包含末尾空格,提前做一次修剪以便映射
var trimmed = fieldName.Trim();
if (!fieldMap.ContainsKey(trimmed))
{
fieldMap[trimmed] = i;
}
}
foreach (var property in properties)
{
if (!property.CanWrite) continue;
try
{
// 尝试查找匹配的字段(大小写不敏感)
if (fieldMap.TryGetValue(property.Name, out int ordinal))
{
var value = reader.GetValue(ordinal);
if (value != DBNull.Value && value != null)
{
// 尝试类型转换
if (property.PropertyType == value.GetType())
{
property.SetValue(obj, value);
}
else if (property.PropertyType == typeof(string))
{
property.SetValue(obj, value.ToString());
}
else
{
// 尝试使用 Convert.ChangeType
var convertedValue = Convert.ChangeType(value, property.PropertyType);
property.SetValue(obj, convertedValue);
}
}
}
}
catch
{
// 字段不存在或类型转换失败,保持默认值
}
}
return obj;
}
}
@@ -0,0 +1,118 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Sharp.Data.Access.Options;
using Sharp.Data.Access.Security;
using Sharp.Data.Access.Connections;
using Sharp.Data.Access.Initialization;
namespace Sharp.Data.Access;
/// <summary>
/// Access 数据服务的依赖注入扩展
/// </summary>
public static class AccessDataServiceExtensions
{
/// <summary>
/// 添加 Access 数据服务(最简单方式 - 仅指定数据库路径)
/// </summary>
/// <param name="services">服务集合</param>
/// <param name="databasePath">数据库文件路径(绝对或相对路径)</param>
/// <param name="password">数据库密码(可选)</param>
/// <param name="provider">OLE DB 提供程序,默认:Microsoft.ACE.OLEDB.16.0</param>
/// <returns>服务集合</returns>
public static IServiceCollection AddAccessDataService(
this IServiceCollection services,
string databasePath,
string? password = null,
string? provider = null)
{
return services.AddAccessDataService(options =>
{
options.DatabaseFile = databasePath;
if (!string.IsNullOrEmpty(password))
options.Password = password;
if (!string.IsNullOrEmpty(provider))
options.ProviderName = provider;
});
}
/// <summary>
/// 添加 Access 数据服务(使用配置委托)
/// </summary>
/// <param name="services">服务集合</param>
/// <param name="configureOptions">配置委托</param>
/// <returns>服务集合</returns>
public static IServiceCollection AddAccessDataService(
this IServiceCollection services,
Action<AccessDatabaseOptions> configureOptions)
{
if (services == null) throw new ArgumentNullException(nameof(services));
if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions));
var options = new AccessDatabaseOptions();
configureOptions(options);
options.Validate();
// 注册核心服务
services.TryAddSingleton<IAccessPasswordDecryptor, PassthroughAccessPasswordDecryptor>();
services.TryAddSingleton<IAccessConnectionStringFactory>(sp =>
new AccessConnectionStringFactory(sp.GetRequiredService<IAccessPasswordDecryptor>()));
services.TryAddSingleton<IAccessDatabaseInitializer, AccessDatabaseInitializer>();
// 注册配置
services.AddSingleton(options);
// 注册 AccessDataService
services.AddSingleton<IAccessDataService>(sp =>
new AccessDataService(
options,
sp.GetRequiredService<IAccessConnectionStringFactory>(),
sp.GetRequiredService<IAccessDatabaseInitializer>(),
sp.GetRequiredService<IAccessPasswordDecryptor>(),
sp.GetService<Microsoft.Extensions.Logging.ILogger<AccessDataService>>()));
return services;
}
/// <summary>
/// 添加 Access 数据服务(使用配置对象)
/// </summary>
/// <param name="services">服务集合</param>
/// <param name="options">配置选项</param>
/// <returns>服务集合</returns>
public static IServiceCollection AddAccessDataService(
this IServiceCollection services,
AccessDatabaseOptions options)
{
if (options == null) throw new ArgumentNullException(nameof(options));
return services.AddAccessDataService(_ => { });
}
/// <summary>
/// 注册自定义密码解密器
/// </summary>
/// <typeparam name="TDecryptor">解密器类型</typeparam>
/// <param name="services">服务集合</param>
/// <returns>服务集合</returns>
public static IServiceCollection AddAccessPasswordDecryptor<TDecryptor>(this IServiceCollection services)
where TDecryptor : class, IAccessPasswordDecryptor
{
services.AddSingleton<IAccessPasswordDecryptor, TDecryptor>();
return services;
}
/// <summary>
/// 注册自定义密码解密器(使用工厂方法)
/// </summary>
/// <param name="services">服务集合</param>
/// <param name="implementationFactory">解密器工厂</param>
/// <returns>服务集合</returns>
public static IServiceCollection AddAccessPasswordDecryptor(
this IServiceCollection services,
Func<IServiceProvider, IAccessPasswordDecryptor> implementationFactory)
{
services.AddSingleton(implementationFactory);
return services;
}
}
@@ -0,0 +1,58 @@
using System;
using System.Data.OleDb;
using Sharp.Data.Access.Internal;
using Sharp.Data.Access.Options;
using Sharp.Data.Access.Security;
namespace Sharp.Data.Access.Connections;
internal sealed class AccessConnectionStringFactory : IAccessConnectionStringFactory
{
private readonly IAccessPasswordDecryptor _decryptor;
public AccessConnectionStringFactory(IAccessPasswordDecryptor decryptor)
{
_decryptor = decryptor ?? throw new ArgumentNullException(nameof(decryptor));
}
public string Create(AccessDatabaseOptions options, IServiceProvider services)
{
var configuration = AccessDatabaseConfiguration.FromOptions(options, services);
if (!string.IsNullOrWhiteSpace(configuration.ConnectionString))
{
return configuration.ConnectionString;
}
if (string.IsNullOrWhiteSpace(configuration.DatabaseFile))
{
throw new InvalidOperationException("Access 数据库需要提供 DatabaseFile 或 ConnectionString。");
}
// 基础连接字符串
// 注意:Connect Timeout 参数在某些 ACE 版本中不稳定,已移除
var builder = new OleDbConnectionStringBuilder
{
["Provider"] = configuration.ProviderName,
["Data Source"] = AccessPathResolver.Resolve(configuration.DatabaseFile, configuration.DataDirectory)
};
var password = configuration.Password;
if (string.IsNullOrEmpty(password) && !string.IsNullOrWhiteSpace(configuration.EncryptedPassword))
{
password = _decryptor.Decrypt(configuration.EncryptedPassword);
}
if (!string.IsNullOrEmpty(password))
{
builder["Jet OLEDB:Database Password"] = password;
}
foreach (var parameter in configuration.AdditionalParameters)
{
builder[parameter.Key] = parameter.Value;
}
return builder.ConnectionString;
}
}
@@ -0,0 +1,12 @@
using System;
using Sharp.Data.Access.Options;
namespace Sharp.Data.Access.Connections;
/// <summary>
/// 构建 Access 连接字符串的工厂。
/// </summary>
public interface IAccessConnectionStringFactory
{
string Create(AccessDatabaseOptions options, IServiceProvider services);
}
+20
View File
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>Sharp.Data.Access</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="EntityFrameworkCore.Jet" Version="8.0.0-preview1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
<PackageReference Include="System.Data.OleDb" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Data\Data.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,72 @@
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Sharp.Data.Access.Connections;
using Sharp.Data.Access.Initialization;
using Sharp.Data.Access.Internal;
using Sharp.Data.Access.Options;
using Sharp.Data.Access.Security;
using Sharp.Data.Context;
namespace Sharp.Data.Access.DependencyInjection;
/// <summary>
/// Access 模块的依赖注入扩展。
/// </summary>
public static class AccessServiceCollectionExtensions
{
public static IServiceCollection AddAccessDataContext<TContext>(this IServiceCollection services,
Action<AccessContextOptions<TContext>> configure,
bool registerDataContext = true,
ServiceLifetime dataContextLifetime = ServiceLifetime.Scoped)
where TContext : DbContext
{
if (configure == null) throw new ArgumentNullException(nameof(configure));
return services.AddAccessDataContext(_ =>
{
var options = new AccessContextOptions<TContext>();
configure(options);
return options;
}, registerDataContext, dataContextLifetime);
}
public static IServiceCollection AddAccessDataContext<TContext>(this IServiceCollection services,
Func<IServiceProvider, AccessContextOptions<TContext>> configure,
bool registerDataContext = true,
ServiceLifetime dataContextLifetime = ServiceLifetime.Scoped)
where TContext : DbContext
{
if (services == null) throw new ArgumentNullException(nameof(services));
if (configure == null) throw new ArgumentNullException(nameof(configure));
services.TryAddSingleton<IAccessPasswordDecryptor, PassthroughAccessPasswordDecryptor>();
services.TryAddSingleton<IAccessConnectionStringFactory>(sp =>
new AccessConnectionStringFactory(sp.GetRequiredService<IAccessPasswordDecryptor>()));
services.TryAddSingleton<IAccessDatabaseInitializer, AccessDatabaseInitializer>();
services.TryAddSingleton(provider =>
{
var options = configure(provider) ?? throw new InvalidOperationException("AccessContextOptions 不能为空。");
options.Validate();
return options;
});
services.AddScoped<IAccessDataContextFactory<TContext>>(sp =>
new AccessDataContextFactory<TContext>(
sp,
sp.GetRequiredService<AccessContextOptions<TContext>>(),
sp.GetRequiredService<IAccessConnectionStringFactory>(),
sp.GetRequiredService<IAccessDatabaseInitializer>()));
if (registerDataContext)
{
services.Add(new ServiceDescriptor(typeof(IDataContext),
sp => sp.GetRequiredService<IAccessDataContextFactory<TContext>>().Create(),
dataContextLifetime));
}
return services;
}
}
@@ -0,0 +1,66 @@
using System;
using EntityFrameworkCore.Jet;
using EntityFrameworkCore.Jet.Infrastructure;
using Microsoft.EntityFrameworkCore;
using Sharp.Data.Access.Connections;
using Sharp.Data.Access.Initialization;
using Sharp.Data.Access.Internal;
using Sharp.Data.Access.Options;
using Sharp.Data.Access.Security;
using Sharp.Data;
namespace Sharp.Data.Access.Extensions;
/// <summary>
/// 手动使用 DataContextBuilder 时的 Access Helper。
/// </summary>
public static class AccessDataContextBuilderExtensions
{
/// <summary>
/// 通过现成的连接字符串配置 Access。
/// </summary>
public static DataContextBuilder<TContext> UseAccessDatabase<TContext>(
this DataContextBuilder<TContext> builder,
string connectionString,
Action<JetDbContextOptionsBuilder>? configure = null)
where TContext : DbContext
{
if (builder == null) throw new ArgumentNullException(nameof(builder));
if (string.IsNullOrWhiteSpace(connectionString))
{
throw new ArgumentException("连接字符串不能为空。", nameof(connectionString));
}
builder.ConfigureDbContext(options => options.UseJet(connectionString, configure));
return builder;
}
/// <summary>
/// 通过 AccessDatabaseOptions 配置 Access。
/// </summary>
public static DataContextBuilder<TContext> UseAccessDatabase<TContext>(
this DataContextBuilder<TContext> builder,
Action<AccessDatabaseOptions> configureOptions,
IServiceProvider? services = null,
IAccessPasswordDecryptor? decryptor = null,
Action<JetDbContextOptionsBuilder>? configure = null)
where TContext : DbContext
{
if (builder == null) throw new ArgumentNullException(nameof(builder));
if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions));
var options = new AccessDatabaseOptions();
configureOptions(options);
options.Validate();
var serviceProvider = services ?? NullServiceProvider.Instance;
var initializer = new AccessDatabaseInitializer();
var connectionFactory = new AccessConnectionStringFactory(decryptor ?? new PassthroughAccessPasswordDecryptor());
initializer.EnsureDatabase(options, serviceProvider);
var connectionString = connectionFactory.Create(options, serviceProvider);
builder.ConfigureDbContext(o => o.UseJet(connectionString, configure));
return builder;
}
}
@@ -0,0 +1,12 @@
using Microsoft.EntityFrameworkCore;
using Sharp.Data.Context;
namespace Sharp.Data.Access;
/// <summary>
/// Access 数据上下文工厂,避免直接依赖具体实现。
/// </summary>
public interface IAccessDataContextFactory<TContext> where TContext : DbContext
{
IDataContext Create();
}
@@ -0,0 +1,75 @@
using System.Data.OleDb;
namespace Sharp.Data.Access;
/// <summary>
/// Access 数据库事务接口
/// </summary>
public interface IAccessTransaction : IDisposable
{
/// <summary>
/// 执行非查询命令(INSERT, UPDATE, DELETE)
/// </summary>
int Execute(string sql, params OleDbParameter[] parameters);
/// <summary>
/// 查询多条记录
/// </summary>
List<T> Query<T>(string sql, params OleDbParameter[] parameters) where T : new();
/// <summary>
/// 执行标量查询(COUNT, SUM等)
/// </summary>
object? ExecuteScalar(string sql, params OleDbParameter[] parameters);
/// <summary>
/// 提交事务
/// </summary>
void Commit();
/// <summary>
/// 回滚事务
/// </summary>
void Rollback();
}
/// <summary>
/// Access 数据服务接口(基于 System.Data.OleDb)
/// </summary>
public interface IAccessDataService
{
/// <summary>
/// 查询多条记录
/// </summary>
List<T> Query<T>(string sql, params OleDbParameter[] parameters) where T : new();
/// <summary>
/// 查询单条记录
/// </summary>
T? QueryFirstOrDefault<T>(string sql, params OleDbParameter[] parameters) where T : new();
/// <summary>
/// 执行标量查询(COUNT, SUM等)
/// </summary>
object? ExecuteScalar(string sql, params OleDbParameter[] parameters);
/// <summary>
/// 执行非查询命令(INSERT, UPDATE, DELETE)
/// </summary>
int Execute(string sql, params OleDbParameter[] parameters);
/// <summary>
/// 开始事务
/// </summary>
IAccessTransaction BeginTransaction();
/// <summary>
/// 在事务中执行操作(自动提交/回滚)
/// </summary>
void ExecuteInTransaction(Action<IAccessTransaction> action);
/// <summary>
/// 测试数据库连接
/// </summary>
bool TestConnection();
}
@@ -0,0 +1,49 @@
using System;
using System.IO;
using Sharp.Data.Access.Internal;
using Sharp.Data.Access.Options;
namespace Sharp.Data.Access.Initialization;
internal sealed class AccessDatabaseInitializer : IAccessDatabaseInitializer
{
public void EnsureDatabase(AccessDatabaseOptions options, IServiceProvider services)
{
var configuration = AccessDatabaseConfiguration.FromOptions(options, services);
if (!string.IsNullOrWhiteSpace(configuration.ConnectionString) ||
string.IsNullOrWhiteSpace(configuration.DatabaseFile))
{
return;
}
var databasePath = AccessPathResolver.Resolve(configuration.DatabaseFile, configuration.DataDirectory);
var directory = Path.GetDirectoryName(databasePath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
if (File.Exists(databasePath))
{
return;
}
if (configuration.CopyTemplateWhenMissing && !string.IsNullOrWhiteSpace(configuration.TemplateFile))
{
var templatePath = AccessPathResolver.Resolve(configuration.TemplateFile, configuration.DataDirectory);
if (!File.Exists(templatePath))
{
throw new FileNotFoundException($"Access 模板文件 \"{templatePath}\" 不存在。", templatePath);
}
File.Copy(templatePath, databasePath);
return;
}
if (configuration.ThrowIfMissing)
{
throw new FileNotFoundException($"Access 数据库 \"{databasePath}\" 不存在。", databasePath);
}
}
}
@@ -0,0 +1,12 @@
using System;
using Sharp.Data.Access.Options;
namespace Sharp.Data.Access.Initialization;
/// <summary>
/// Access 数据库初始化器(确保目录/数据库文件准备就绪)。
/// </summary>
public interface IAccessDatabaseInitializer
{
void EnsureDatabase(AccessDatabaseOptions options, IServiceProvider services);
}
@@ -0,0 +1,48 @@
using System;
using EntityFrameworkCore.Jet;
using Microsoft.EntityFrameworkCore;
using Sharp.Data;
using Sharp.Data.Access.Connections;
using Sharp.Data.Access.Initialization;
using Sharp.Data.Access.Options;
using Sharp.Data.Context;
namespace Sharp.Data.Access.Internal;
internal sealed class AccessDataContextFactory<TContext> : IAccessDataContextFactory<TContext>
where TContext : DbContext
{
private readonly IServiceProvider _services;
private readonly AccessContextOptions<TContext> _options;
private readonly IAccessConnectionStringFactory _connectionFactory;
private readonly IAccessDatabaseInitializer _initializer;
public AccessDataContextFactory(
IServiceProvider services,
AccessContextOptions<TContext> options,
IAccessConnectionStringFactory connectionFactory,
IAccessDatabaseInitializer initializer)
{
_services = services;
_options = options;
_connectionFactory = connectionFactory;
_initializer = initializer;
}
public IDataContext Create()
{
var builder = new DataContextBuilder<TContext>();
_initializer.EnsureDatabase(_options.Database, _services);
var connectionString = _connectionFactory.Create(_options.Database, _services);
builder.ConfigureDbContext(options =>
{
options.UseJet(connectionString, jet => _options.ConfigureJet?.Invoke(jet));
});
_options.ConfigureDataContext?.Invoke(_services, builder);
return builder.Build();
}
}
@@ -0,0 +1,61 @@
using System;
using System.Collections.Generic;
using Sharp.Data.Access.Options;
namespace Sharp.Data.Access.Internal;
internal sealed record AccessDatabaseConfiguration(
string? ConnectionString,
string? DatabaseFile,
string ProviderName,
string? Password,
string? EncryptedPassword,
string? TemplateFile,
string? DataDirectory,
int ConnectionTimeout,
int MaxRetryCount,
int RetryDelayMilliseconds,
IReadOnlyDictionary<string, string> AdditionalParameters,
bool CopyTemplateWhenMissing,
bool ThrowIfMissing)
{
public static AccessDatabaseConfiguration FromOptions(AccessDatabaseOptions options, IServiceProvider services)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
var connectionString = Resolve(options.ConnectionString, options.ConnectionStringFactory, services);
var databaseFile = Resolve(options.DatabaseFile, options.DatabaseFileFactory, services);
var templateFile = Resolve(options.TemplateFile, options.TemplateFileFactory, services);
var dataDirectory = Resolve(options.DataDirectory, options.DataDirectoryFactory, services);
var password = Resolve(options.Password, options.PasswordFactory, services);
var encryptedPassword = Resolve(options.EncryptedPassword, options.EncryptedPasswordFactory, services);
return new AccessDatabaseConfiguration(
connectionString,
databaseFile,
string.IsNullOrWhiteSpace(options.ProviderName) ? AccessProviderNames.AceOleDb16 : options.ProviderName,
password,
encryptedPassword,
templateFile,
dataDirectory,
options.ConnectionTimeout,
options.MaxRetryCount,
options.RetryDelayMilliseconds,
new Dictionary<string, string>(options.AdditionalParameters, StringComparer.OrdinalIgnoreCase),
options.CopyTemplateWhenMissing,
options.ThrowIfMissing);
}
private static string? Resolve(string? value, Func<IServiceProvider, string?>? factory, IServiceProvider services)
{
if (!string.IsNullOrWhiteSpace(value))
{
return value;
}
return factory?.Invoke(services);
}
}
@@ -0,0 +1,26 @@
using System;
using System.IO;
namespace Sharp.Data.Access.Internal;
internal static class AccessPathResolver
{
public static string Resolve(string path, string? baseDirectory)
{
if (string.IsNullOrWhiteSpace(path))
{
throw new InvalidOperationException("Access 数据库路径不能为空。");
}
if (Path.IsPathFullyQualified(path))
{
return Path.GetFullPath(path);
}
var root = string.IsNullOrWhiteSpace(baseDirectory)
? AppContext.BaseDirectory
: baseDirectory!;
return Path.GetFullPath(Path.Combine(root, path));
}
}
@@ -0,0 +1,14 @@
using System;
namespace Sharp.Data.Access.Internal;
internal sealed class NullServiceProvider : IServiceProvider
{
public static readonly NullServiceProvider Instance = new();
private NullServiceProvider()
{
}
public object? GetService(Type serviceType) => null;
}
@@ -0,0 +1,30 @@
using System;
using EntityFrameworkCore.Jet.Infrastructure;
using Microsoft.EntityFrameworkCore;
using Sharp.Data;
namespace Sharp.Data.Access.Options;
/// <summary>
/// Access 数据上下文注册配置。
/// </summary>
/// <typeparam name="TContext">EF Core DbContext 类型。</typeparam>
public sealed class AccessContextOptions<TContext> where TContext : DbContext
{
/// <summary>
/// Access 数据库配置(连接方式、密码等)。
/// </summary>
public AccessDatabaseOptions Database { get; } = new();
/// <summary>
/// 允许调用方在 Build 之前继续配置 DataContextBuilder(例如启用审计、软删除、当前用户等)。
/// </summary>
public Action<IServiceProvider, DataContextBuilder<TContext>>? ConfigureDataContext { get; set; }
/// <summary>
/// 细化 Jet Provider 选项。
/// </summary>
public Action<JetDbContextOptionsBuilder>? ConfigureJet { get; set; }
internal void Validate() => Database.Validate();
}
@@ -0,0 +1,143 @@
using System;
using System.Collections.Generic;
namespace Sharp.Data.Access.Options;
/// <summary>
/// Access 数据库统一配置项,同时支持 OleDb 和 EF Core 两种方式。
/// </summary>
public class AccessDatabaseOptions
{
/// <summary>
/// 完整的连接字符串,可选。如果提供则优先使用,不再重新构建。
/// </summary>
public string? ConnectionString { get; set; }
/// <summary>
/// 通过依赖注入动态生成连接字符串。
/// </summary>
public Func<IServiceProvider, string?>? ConnectionStringFactory { get; set; }
/// <summary>
/// Access 数据库文件路径(相对或绝对)。
/// </summary>
public string? DatabaseFile { get; set; }
/// <summary>
/// 通过依赖注入动态解析数据库文件路径。
/// </summary>
public Func<IServiceProvider, string?>? DatabaseFileFactory { get; set; }
/// <summary>
/// 数据文件的基础目录(如果数据库路径为相对路径则会拼接)。
/// 默认使用 <see cref="AppContext.BaseDirectory" />。
/// </summary>
public string? DataDirectory { get; set; }
/// <summary>
/// 通过依赖注入动态解析数据目录。
/// </summary>
public Func<IServiceProvider, string?>? DataDirectoryFactory { get; set; }
/// <summary>
/// 明文密码,可选。用于访问加密的 Access 数据库。
/// </summary>
public string? Password { get; set; }
/// <summary>
/// 通过依赖注入动态解析明文密码。
/// </summary>
public Func<IServiceProvider, string?>? PasswordFactory { get; set; }
/// <summary>
/// 加密后的密码,可选。将通过 <see cref="Security.IAccessPasswordDecryptor" /> 解密。
/// </summary>
public string? EncryptedPassword { get; set; }
/// <summary>
/// 通过依赖注入动态解析加密密码。
/// </summary>
public Func<IServiceProvider, string?>? EncryptedPasswordFactory { get; set; }
/// <summary>
/// 数据库模板(*.mdb / *.accdb),在文件不存在时复制一份。
/// </summary>
public string? TemplateFile { get; set; }
/// <summary>
/// 通过依赖注入动态解析模板文件路径。
/// </summary>
public Func<IServiceProvider, string?>? TemplateFileFactory { get; set; }
/// <summary>
/// 当数据库文件缺失时是否尝试从模板复制。
/// </summary>
public bool CopyTemplateWhenMissing { get; set; } = true;
/// <summary>
/// 当数据库文件缺失(且无法复制)时是否抛出异常。
/// </summary>
public bool ThrowIfMissing { get; set; } = true;
/// <summary>
/// Provider 名称,默认为 ACE OLE DB 16.0(功能最完整)。
/// 可选值:
/// - AccessProviderNames.AceOleDb16 (Microsoft.ACE.OLEDB.16.0) - 推荐,需要 Office 2016+
/// - AccessProviderNames.AceOleDb12 (Microsoft.ACE.OLEDB.12.0) - 需要 Office 2010+,不支持 Connect Timeout
/// - AccessProviderNames.JetOleDb4 (Microsoft.Jet.OLEDB.4.0) - 仅支持 32 位和旧版 Access
/// </summary>
public string ProviderName { get; set; } = AccessProviderNames.AceOleDb16;
/// <summary>
/// 连接超时时间(秒),默认 15 秒。
/// </summary>
public int ConnectionTimeout { get; set; } = 15;
/// <summary>
/// 最大重试次数,默认 3 次。用于增强稳健性。
/// </summary>
public int MaxRetryCount { get; set; } = 3;
/// <summary>
/// 重试延迟(毫秒),默认 100 毫秒。
/// </summary>
public int RetryDelayMilliseconds { get; set; } = 100;
/// <summary>
/// 额外的 OLE DB 参数,按需添加。
/// </summary>
public IDictionary<string, string> AdditionalParameters { get; } =
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
internal void Validate()
{
if (string.IsNullOrWhiteSpace(ConnectionString) &&
ConnectionStringFactory is null &&
string.IsNullOrWhiteSpace(DatabaseFile) &&
DatabaseFileFactory is null)
{
throw new InvalidOperationException(
"AccessDatabaseOptions 需要连接字符串或数据库文件路径。");
}
if (string.IsNullOrWhiteSpace(ProviderName))
{
throw new InvalidOperationException("必须指定 Access Provider。");
}
if (ConnectionTimeout <= 0)
{
throw new InvalidOperationException("ConnectionTimeout 必须大于 0。");
}
if (MaxRetryCount < 0)
{
throw new InvalidOperationException("MaxRetryCount 不能小于 0。");
}
if (RetryDelayMilliseconds < 0)
{
throw new InvalidOperationException("RetryDelayMilliseconds 不能小于 0。");
}
}
}
@@ -0,0 +1,22 @@
namespace Sharp.Data.Access.Options;
/// <summary>
/// 常用 Access 数据库 Provider 名称。
/// </summary>
public static class AccessProviderNames
{
/// <summary>
/// Microsoft Access Database Engine 2016 (推荐)。
/// </summary>
public const string AceOleDb16 = "Microsoft.ACE.OLEDB.16.0";
/// <summary>
/// Microsoft Access Database Engine 2010。
/// </summary>
public const string AceOleDb12 = "Microsoft.ACE.OLEDB.12.0";
/// <summary>
/// 老旧的 Jet 4.0 Provider(仅限 32 位进程)。
/// </summary>
public const string JetOleDb4 = "Microsoft.Jet.OLEDB.4.0";
}
+374
View File
@@ -0,0 +1,374 @@
# AccessDataService 简化 API 使用指南
## 🎯 设计目标
Data.Access 模块的核心目标是**让调用者使用起来更简单**,将连接管理、SQL 执行、参数处理、事务管理等复杂度封装起来。
## 📊 代码对比
### ❌ 之前:直接使用 OleDb(复杂)
```csharp
// 每次都要手动创建连接
private OleDbConnection CreateConnection()
{
var dbPath = Path.GetFullPath("dbDwDmsSetup.accdb");
var connectionString = $"Provider=Microsoft.ACE.OLEDB.12.0;Data Source={dbPath}";
return new OleDbConnection(connectionString);
}
// 查询用户 - 需要写大量样板代码
private async Task<List<UserInfoDto>> LoadUsersAsync()
{
using var connection = CreateConnection();
await connection.OpenAsync();
using var command = new OleDbCommand("SELECT UserName, Password FROM UserInfo", connection);
using var reader = await Task.Run(() => command.ExecuteReader());
var users = new List<UserInfoDto>();
while (await Task.Run(() => reader.Read()))
{
var user = new UserInfoDto
{
UserName = reader["UserName"]?.ToString() ?? "",
Password = reader["Password"]?.ToString() ?? ""
};
users.Add(user);
}
return users;
}
// 删除用户 - 需要手动管理事务
private async Task DeleteUserAsync(string userName)
{
using var connection = CreateConnection();
await connection.OpenAsync();
using var transaction = connection.BeginTransaction();
try
{
using (var deletePermCmd = new OleDbCommand(
"DELETE FROM UserPermission WHERE UserInfo_UserName = ?", connection, transaction))
{
deletePermCmd.Parameters.AddWithValue("@UserName", userName);
await Task.Run(() => deletePermCmd.ExecuteNonQuery());
}
using (var deleteUserCmd = new OleDbCommand(
"DELETE FROM UserInfo WHERE UserName = ?", connection, transaction))
{
deleteUserCmd.Parameters.AddWithValue("@UserName", userName);
await Task.Run(() => deleteUserCmd.ExecuteNonQuery());
}
transaction.Commit();
}
catch
{
transaction.Rollback();
throw;
}
}
```
**问题**:
- 大量重复的连接管理代码
- 手动映射查询结果到对象
- 手动管理事务的提交和回滚
- 代码冗长,不易维护
---
### ✅ 现在:使用 AccessDataService(简洁)
```csharp
// 注入服务,无需手动创建连接
private readonly IAccessDataService _accessDataService;
// 查询用户 - 一行代码!自动映射!
private async Task<List<UserInfoDto>> LoadUsersAsync()
{
return await _accessDataService.QueryAsync<UserInfoDto>(
"SELECT UserName, [Password] FROM UserInfo");
}
// 删除用户 - 自动管理事务!
private async Task DeleteUserAsync(string userName)
{
await _accessDataService.ExecuteInTransactionAsync(async transaction =>
{
await transaction.ExecuteAsync(
"DELETE FROM UserPermission WHERE UserInfo_UserName = ?",
new OleDbParameter("@UserName", userName));
await transaction.ExecuteAsync(
"DELETE FROM UserInfo WHERE UserName = ?",
new OleDbParameter("@UserName", userName));
});
}
```
**优势**:
- ✅ 无需手动管理连接
- ✅ 自动泛型映射查询结果
- ✅ 自动管理事务(提交/回滚)
- ✅ 代码简洁,易于维护
- ✅ 从 60+ 行减少到 10 行左右
---
## 🚀 快速开始
### 1. 初始化服务
在 `App.xaml.cs` 中初始化:
```csharp
private void InitializeAccessDataService()
{
var databasePath = Path.GetFullPath("dbDwDmsSetup.accdb");
var options = new AccessDataServiceOptions
{
DatabasePath = databasePath
};
AccessDataService = new AccessDataService(options, LoggerFactory.CreateLogger<AccessDataService>());
}
```
### 2. 在窗口中使用
```csharp
public partial class AccessDatabaseWindowOleDb : Window
{
private readonly IAccessDataService _accessDataService;
public AccessDatabaseWindowOleDb()
{
InitializeComponent();
var app = (App)Application.Current;
_accessDataService = app.AccessDataService;
}
}
```
---
## 📚 API 使用示例
### 1. 查询多条记录
```csharp
// 自动映射到对象
var users = await _accessDataService.QueryAsync<UserInfoDto>(
"SELECT UserName, [Password] FROM UserInfo");
// 带参数的查询
var users = await _accessDataService.QueryAsync<UserInfoDto>(
"SELECT UserName, [Password] FROM UserInfo WHERE UserName LIKE ?",
new OleDbParameter("@Pattern", "%admin%"));
```
### 2. 查询单条记录
```csharp
var user = await _accessDataService.QueryFirstOrDefaultAsync<UserInfoDto>(
"SELECT UserName, [Password] FROM UserInfo WHERE UserName = ?",
new OleDbParameter("@UserName", "admin"));
if (user != null)
{
Console.WriteLine($"找到用户: {user.UserName}");
}
```
### 3. 执行标量查询
```csharp
// 查询数量
var count = await _accessDataService.ExecuteScalarAsync(
"SELECT COUNT(*) FROM UserInfo WHERE UserName = ?",
new OleDbParameter("@UserName", "admin"));
int userCount = Convert.ToInt32(count);
```
### 4. 插入数据
```csharp
var rowsAffected = await _accessDataService.ExecuteAsync(
"INSERT INTO UserInfo (UserName, [Password]) VALUES (?, ?)",
new OleDbParameter("@UserName", "newuser"),
new OleDbParameter("@Password", "password123"));
Console.WriteLine($"插入了 {rowsAffected} 行");
```
### 5. 更新数据
```csharp
var rowsAffected = await _accessDataService.ExecuteAsync(
"UPDATE UserInfo SET [Password] = ? WHERE UserName = ?",
new OleDbParameter("@Password", "newpassword"),
new OleDbParameter("@UserName", "admin"));
Console.WriteLine($"更新了 {rowsAffected} 行");
```
### 6. 删除数据
```csharp
var rowsAffected = await _accessDataService.ExecuteAsync(
"DELETE FROM UserInfo WHERE UserName = ?",
new OleDbParameter("@UserName", "testuser"));
Console.WriteLine($"删除了 {rowsAffected} 行");
```
### 7. 使用事务(自动提交/回滚)
```csharp
// 推荐:使用 ExecuteInTransactionAsync(自动管理)
await _accessDataService.ExecuteInTransactionAsync(async transaction =>
{
// 删除权限
await transaction.ExecuteAsync(
"DELETE FROM UserPermission WHERE UserInfo_UserName = ?",
new OleDbParameter("@UserName", userName));
// 删除用户
await transaction.ExecuteAsync(
"DELETE FROM UserInfo WHERE UserName = ?",
new OleDbParameter("@UserName", userName));
// 自动提交!如果有异常,自动回滚!
});
```
### 8. 手动管理事务
```csharp
// 如果需要手动控制
using var transaction = _accessDataService.BeginTransaction();
try
{
await transaction.ExecuteAsync("INSERT INTO ...", ...);
await transaction.ExecuteAsync("UPDATE ...", ...);
transaction.Commit(); // 手动提交
}
catch
{
transaction.Rollback(); // 手动回滚
throw;
}
```
### 9. 测试连接
```csharp
var success = await _accessDataService.TestConnectionAsync();
if (success)
{
Console.WriteLine("数据库连接成功!");
}
```
---
## 🎨 完整示例
```csharp
public class UserService
{
private readonly IAccessDataService _accessDataService;
public UserService(IAccessDataService accessDataService)
{
_accessDataService = accessDataService;
}
// 获取所有用户
public async Task<List<UserInfoDto>> GetAllUsersAsync()
{
return await _accessDataService.QueryAsync<UserInfoDto>(
"SELECT UserName, [Password] FROM UserInfo");
}
// 获取用户权限数量
public async Task<int> GetUserPermissionCountAsync(string userName)
{
var count = await _accessDataService.ExecuteScalarAsync(
"SELECT COUNT(*) FROM UserPermission WHERE UserInfo_UserName = ?",
new OleDbParameter("@UserName", userName));
return Convert.ToInt32(count);
}
// 添加用户
public async Task<bool> AddUserAsync(string userName, string password)
{
// 检查是否已存在
var exists = await _accessDataService.ExecuteScalarAsync(
"SELECT COUNT(*) FROM UserInfo WHERE UserName = ?",
new OleDbParameter("@UserName", userName));
if (Convert.ToInt32(exists) > 0)
return false;
// 插入用户
await _accessDataService.ExecuteAsync(
"INSERT INTO UserInfo (UserName, [Password]) VALUES (?, ?)",
new OleDbParameter("@UserName", userName),
new OleDbParameter("@Password", password));
return true;
}
// 删除用户(包含权限)
public async Task DeleteUserAsync(string userName)
{
await _accessDataService.ExecuteInTransactionAsync(async transaction =>
{
// 先删除权限
await transaction.ExecuteAsync(
"DELETE FROM UserPermission WHERE UserInfo_UserName = ?",
new OleDbParameter("@UserName", userName));
// 再删除用户
await transaction.ExecuteAsync(
"DELETE FROM UserInfo WHERE UserName = ?",
new OleDbParameter("@UserName", userName));
});
}
}
```
---
## 💡 核心优势总结
| 特性 | 直接使用 OleDb | 使用 AccessDataService |
|------|---------------|----------------------|
| 连接管理 | 手动创建、打开、释放 | ✅ 自动管理 |
| 结果映射 | 手动读取 DataReader | ✅ 自动泛型映射 |
| 事务管理 | 手动 BeginTransaction/Commit/Rollback | ✅ 自动管理 |
| 代码量 | 60+ 行 | ✅ 10 行左右 |
| 可维护性 | 低(大量重复代码) | ✅ 高 |
| 易用性 | 低(需要理解 ADO.NET) | ✅ 高 |
---
## 📖 参考文件
- **核心实现**: `Data.Access/AccessDataService.cs`
- **接口定义**: `Data.Access/IAccessDataService.cs`
- **配置选项**: `Data.Access/AccessDataServiceOptions.cs`
- **使用示例**: `Server/Views/AccessDatabaseWindowOleDb.xaml.cs`
---
**总结**:Data.Access 模块通过封装 OleDb 的复杂度,提供了简洁优雅的 API,让调用者可以用更少的代码完成相同的功能!
+115
View File
@@ -0,0 +1,115 @@
# Sharp.Data.Access
基于 EF Core + Access (Jet) 的数据库基础设施。遵循 “依赖大于配置” 的理念,提供易于集成的连接串构建、密码解密、数据库初始化以及依赖注入支持,让其他项目只专注于提供配置即可。
> **定位**:仅提供 Access 数据层能力(连接、仓储、上下文构建),不含任何业务逻辑。
## 特性
- ✅ **DI 集成** – 一行 `AddAccessDataContext<TContext>()` 注册,其他模块通过构造函数获取 `IDataContext`。
- ✅ **最小配置** – 只需要提供数据库文件或连接字符串即可完成集成。
- ✅ **密码解密** – 内置 `IAccessPasswordDecryptor`,可注入自定义实现来解密加密密码。
- ✅ **数据库初始化** – 自动创建目录、可选基于模板复制空库文件,避免运行期错误。
- ✅ **灵活扩展** – 支持通过委托访问 `IServiceProvider`,从任意配置中心或密钥管理系统读取参数。
## 快速开始
```csharp
using Microsoft.Extensions.DependencyInjection;
using Sharp.Data.Access.DependencyInjection;
using Sharp.Data.Context;
builder.Services.AddAccessDataContext<AppDbContext>(options =>
{
options.Database.DatabaseFile = "data/app.accdb";
options.Database.EncryptedPassword = configuration["Access:EncryptedPassword"];
// 配置 Data 模块能力
options.ConfigureDataContext = (sp, ctx) =>
{
ctx.UseAudit()
.UseSoftDelete()
.UseCurrentUserProvider(() => sp.GetService<ICurrentUser>()?.Id);
};
});
```
随后只需要注入 `IDataContext` 或 `IAccessDataContextFactory<AppDbContext>` 即可使用:
```csharp
public class UserService
{
private readonly IDataContext _db;
public UserService(IDataContext db) => _db = db;
public Task<List<User>> GetUsersAsync() => _db.Repository<User>().GetAllAsync();
}
```
## 依赖注入示例(带模板/密码)
```csharp
var services = new ServiceCollection();
services.AddSingleton<ISecretProvider, SecretProvider>(); // 自定义的密钥服务
services.AddAccessDataContext<AppDbContext>(options =>
{
options.Database.DatabaseFile = "data/app.accdb";
options.Database.TemplateFile = "data/template.accdb";
options.Database.CopyTemplateWhenMissing = true;
options.Database.EncryptedPasswordFactory = sp =>
sp.GetRequiredService<ISecretProvider>().Get("AccessDbPassword");
options.ConfigureDataContext = (sp, ctx) =>
{
ctx.UseAudit()
.UseSoftDelete()
.UseCurrentUserProvider(() => sp.GetRequiredService<ICurrentUserAccessor>().Id);
};
});
await using var provider = services.BuildServiceProvider();
var factory = provider.GetRequiredService<IAccessDataContextFactory<AppDbContext>>();
await using var dataContext = factory.Create();
```
> 在实际业务项目中可以把 `IAccessDataContextFactory<TContext>` 注入到需要“短生命周期”数据库连接的服务中,或直接注入 `IDataContext`(作用域作用域内共享同一实例)。
## 处理加密密码
实现 `IAccessPasswordDecryptor` 并注册到 DI:
```csharp
builder.Services.AddSingleton<IAccessPasswordDecryptor, AesPasswordDecryptor>();
```
`AccessDatabaseOptions.EncryptedPassword` 会在构建连接字符串前自动调用该服务完成解密,也可以直接提供明文 `Password`。
## 模板与初始化
- `CopyTemplateWhenMissing = true`(默认)时,如果指定了 `TemplateFile`,将在首次运行时复制模板数据库。
- `ThrowIfMissing = true` 可在缺少数据库文件时立即抛出异常,便于早期发现部署问题。
- 所有路径都支持相对路径(基于 `AppContext.BaseDirectory`),也可以通过 `DataDirectoryFactory` 自定义。
## 手动使用(无依赖注入)
```csharp
var builder = new DataContextBuilder<AppDbContext>();
builder.UseAccessDatabase(options =>
{
options.DatabaseFile = "data/app.accdb";
options.Password = "secret";
});
var db = builder.Build();
```
## 可扩展点
- `IAccessPasswordDecryptor`:自定义密码加解密。
- `IAccessConnectionStringFactory`:自定义连接串构建逻辑。
- `IAccessDatabaseInitializer`:自定义数据库初始化策略(检查锁、备份等)。
- `AccessContextOptions.ConfigureDataContext`:复用 Data 模块的审计、软删除、自动迁移等能力。
通过这些基础设施,Access 支持可在任何项目中复用,同时保持代码简洁、低耦合。
@@ -0,0 +1,14 @@
namespace Sharp.Data.Access.Security;
/// <summary>
/// Access 密码解密接口,调用方可替换以实现自定义加解密策略。
/// </summary>
public interface IAccessPasswordDecryptor
{
/// <summary>
/// 解密 Access 数据库密码。
/// </summary>
/// <param name="encryptedPassword">加密后的密码。</param>
/// <returns>返回明文密码,如果无法解密则返回 null。</returns>
string? Decrypt(string? encryptedPassword);
}
@@ -0,0 +1,9 @@
namespace Sharp.Data.Access.Security;
/// <summary>
/// 默认的密码解密器,直接返回原始值。
/// </summary>
public sealed class PassthroughAccessPasswordDecryptor : IAccessPasswordDecryptor
{
public string? Decrypt(string? encryptedPassword) => encryptedPassword;
}
+12 -1
View File
@@ -294,9 +294,20 @@ public class HttpServer
var controller = _serviceProvider.GetService(controllerType);
if (controller != null)
return controller;
// 使用 ActivatorUtilities 创建实例(可以解析构造函数参数)
try
{
return ActivatorUtilities.CreateInstance(_serviceProvider, controllerType);
}
catch (Exception ex)
{
_logger?.LogError(ex, "Failed to create controller {ControllerType} with DI", controllerType.Name);
throw;
}
}
// 使用激活器创建
// 没有 DI 容器时,使用默认激活器创建(需要无参构造)
return Activator.CreateInstance(controllerType)!;
}
+66 -3
View File
@@ -1,9 +1,12 @@
using System.Windows;
using System.IO;
using System.Windows;
using Microsoft.Extensions.Logging;
using Config;
using Sharp.Log;
using Sharp.Data;
using Sharp.Data.Context;
using Sharp.Data.Access;
using Sharp.Data.Access.Options;
using Server.Config;
using Server.Services;
using Server.Data;
@@ -20,6 +23,7 @@ namespace Server
public ILoggerFactory? LoggerFactory { get; private set; }
public ServerService? ServerService { get; private set; }
public IDataContext? DataContext { get; private set; }
public IAccessDataService? AccessDataService { get; private set; }
protected override void OnStartup(StartupEventArgs e)
{
@@ -36,7 +40,10 @@ namespace Server
// 3. 初始化数据库
InitializeDatabase();
// 4. 初始化服务器服务
// 4. 初始化 Access 数据服务
InitializeAccessDataService();
// 5. 初始化服务器服务
InitializeServerService();
var logger = LoggerFactory?.CreateLogger<App>();
@@ -74,7 +81,7 @@ namespace Server
options.Enrich.Application ??= "SharpServer";
});
}
private void InitializeDatabase()
{
if (LoggerFactory == null)
@@ -104,6 +111,62 @@ namespace Server
throw;
}
}
private void InitializeAccessDataService()
{
if (LoggerFactory == null)
{
throw new InvalidOperationException("日志系统未初始化");
}
var logger = LoggerFactory.CreateLogger<App>();
logger.LogInformation("正在初始化 Access 数据服务...");
try
{
// 诊断:输出当前目录信息
logger.LogInformation("AppContext.BaseDirectory: {BaseDirectory}", AppContext.BaseDirectory);
logger.LogInformation("Environment.CurrentDirectory: {CurrentDirectory}", Environment.CurrentDirectory);
logger.LogInformation("数据库文件在输出目录: {DbPath}",
Path.Combine(AppContext.BaseDirectory, "dbDwDmsSetup.accdb"));
logger.LogInformation("数据库文件是否存在: {Exists}",
File.Exists(Path.Combine(AppContext.BaseDirectory, "dbDwDmsSetup.accdb")));
// 配置 Access 数据库选项
var options = new AccessDatabaseOptions
{
DatabaseFile = "dbDwDmsSetup.accdb",
// 使用 ACE OLE DB 16.0(功能最完整,支持所有连接字符串参数)
// 注意:12.0 不支持 Connect Timeout 参数,会导致 0x80040E21 错误
ProviderName = AccessProviderNames.AceOleDb16,
// 如果数据库有密码,可以在这里设置:
// Password = "your_password",
// 或者使用加密密码(需要先注册自定义解密器):
// EncryptedPassword = "encrypted_password",
// 可选:调整稳健性配置
// MaxRetryCount = 3, // 最大重试次数
// RetryDelayMilliseconds = 100, // 重试延迟
// ConnectionTimeout = 15, // 连接超时(秒)
};
// 创建 Access 数据服务
AccessDataService = new AccessDataService(
options,
logger: LoggerFactory.CreateLogger<AccessDataService>());
logger.LogInformation("Access 数据服务初始化成功");
}
catch (Exception ex)
{
logger.LogError(ex, "Access 数据服务初始化失败");
// 不抛出异常,因为 Access 数据库是可选的
logger.LogWarning("Access 数据服务初始化失败,但应用程序将继续运行");
}
}
private void InitializeServerService()
{
+9 -13
View File
@@ -23,13 +23,13 @@ namespace Server.Controllers
public async Task GetAll(HttpContext context)
{
var items = _dataStore.Values.ToList();
var result = ResultExtensions.HttpSuccess(new
var data = new
{
Count = items.Count,
Items = items
});
};
await WriteSuccessResponseAsync(context, result);
await WriteSuccessResponseAsync(context, data);
}
/// <summary>
@@ -49,8 +49,7 @@ namespace Server.Controllers
if (_dataStore.TryGetValue(id, out var item))
{
var result = ResultExtensions.HttpSuccess(item);
await WriteSuccessResponseAsync(context, result);
await WriteSuccessResponseAsync(context, item);
}
else
{
@@ -87,8 +86,7 @@ namespace Server.Controllers
_dataStore[id] = item;
var result = ResultExtensions.HttpSuccess(item, "Data created successfully");
await WriteSuccessResponseAsync(context, result);
await WriteSuccessResponseAsync(context, item, "Data created successfully");
}
/// <summary>
@@ -129,8 +127,7 @@ namespace Server.Controllers
item.UpdatedAt = DateTime.UtcNow;
}
var result = ResultExtensions.HttpSuccess(item, "Data updated successfully");
await WriteSuccessResponseAsync(context, result);
await WriteSuccessResponseAsync(context, item, "Data updated successfully");
}
/// <summary>
@@ -150,8 +147,7 @@ namespace Server.Controllers
if (_dataStore.TryRemove(id, out var item))
{
var result = ResultExtensions.HttpSuccess(item, "Data deleted successfully");
await WriteSuccessResponseAsync(context, result);
await WriteSuccessResponseAsync(context, item, "Data deleted successfully");
}
else
{
@@ -169,8 +165,8 @@ namespace Server.Controllers
var count = _dataStore.Count;
_dataStore.Clear();
var result = ResultExtensions.HttpSuccess(new { DeletedCount = count }, "All data cleared");
await WriteSuccessResponseAsync(context, result);
var data = new { DeletedCount = count };
await WriteSuccessResponseAsync(context, data, "All data cleared");
}
}
@@ -19,14 +19,14 @@ namespace Server.Controllers
[HttpGet]
public async Task Check(HttpContext context)
{
var result = ResultExtensions.HttpSuccess(new
var data = new
{
Status = "Healthy",
Timestamp = DateTime.UtcNow,
Uptime = DateTime.UtcNow - Process.GetCurrentProcess().StartTime
});
};
await WriteSuccessResponseAsync(context, result);
await WriteSuccessResponseAsync(context, data);
}
/// <summary>
@@ -37,7 +37,7 @@ namespace Server.Controllers
public async Task DetailedCheck(HttpContext context)
{
var process = Process.GetCurrentProcess();
var result = ResultExtensions.HttpSuccess(new
var data = new
{
Status = "Healthy",
Timestamp = DateTime.UtcNow,
@@ -60,9 +60,9 @@ namespace Server.Controllers
},
Threads = process.Threads.Count
}
});
};
await WriteSuccessResponseAsync(context, result);
await WriteSuccessResponseAsync(context, data);
}
}
}
@@ -18,7 +18,7 @@ namespace Server.Controllers
[HttpGet("/info")]
public async Task GetSystemInfo(HttpContext context)
{
var result = ResultExtensions.HttpSuccess(new
var data = new
{
MachineName = Environment.MachineName,
UserName = Environment.UserName,
@@ -30,9 +30,9 @@ namespace Server.Controllers
CurrentDirectory = Environment.CurrentDirectory,
TickCount = Environment.TickCount64,
Version = Environment.Version.ToString()
});
};
await WriteSuccessResponseAsync(context, result);
await WriteSuccessResponseAsync(context, data);
}
/// <summary>
@@ -59,7 +59,7 @@ namespace Server.Controllers
return;
}
var successResult = ResultExtensions.HttpSuccess(new { Name = name, Value = value });
var successResult = new { Name = name, Value = value };
await WriteSuccessResponseAsync(context, successResult);
}
@@ -73,7 +73,7 @@ namespace Server.Controllers
var now = DateTime.Now;
var utcNow = DateTime.UtcNow;
var result = ResultExtensions.HttpSuccess(new
var data = new
{
Local = new
{
@@ -89,9 +89,9 @@ namespace Server.Controllers
Time = utcNow.TimeOfDay,
UnixTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
}
});
};
await WriteSuccessResponseAsync(context, result);
await WriteSuccessResponseAsync(context, data);
}
}
}
@@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore;
namespace Server.Data.Access;
/// <summary>
/// Access 数据库上下文
/// </summary>
public class AccessDbContext : DbContext
{
public AccessDbContext(DbContextOptions<AccessDbContext> options) : base(options)
{
}
public DbSet<UserInfo> UserInfos => Set<UserInfo>();
public DbSet<UserPermission> UserPermissions => Set<UserPermission>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<UserInfo>(entity =>
{
entity.HasKey(e => e.UserName);
entity.Property(e => e.UserName).IsRequired().HasMaxLength(255);
entity.Property(e => e.Password).IsRequired().HasMaxLength(255);
});
modelBuilder.Entity<UserPermission>(entity =>
{
entity.HasKey(e => new { e.UserInfo_UserName, e.Permission_Code });
entity.Property(e => e.UserInfo_UserName).IsRequired().HasMaxLength(255);
entity.Property(e => e.Permission_Code).IsRequired().HasMaxLength(255);
entity.HasOne(e => e.UserInfo)
.WithMany(u => u.Permissions)
.HasForeignKey(e => e.UserInfo_UserName)
.OnDelete(DeleteBehavior.Cascade);
});
}
}
@@ -0,0 +1,40 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Server.Data.Access;
/// <summary>
/// UserInfo 表实体
/// </summary>
[Table("UserInfo")]
public class UserInfo
{
[Key]
[Required]
[MaxLength(255)]
public string UserName { get; set; } = string.Empty;
[Required]
[MaxLength(255)]
public string Password { get; set; } = string.Empty;
public ICollection<UserPermission> Permissions { get; set; } = new List<UserPermission>();
}
/// <summary>
/// UserPermission 表实体
/// </summary>
[Table("UserPermission")]
public class UserPermission
{
[Required]
[MaxLength(255)]
public string UserInfo_UserName { get; set; } = string.Empty;
[Required]
[MaxLength(255)]
public string Permission_Code { get; set; } = string.Empty;
[ForeignKey(nameof(UserInfo_UserName))]
public UserInfo? UserInfo { get; set; }
}
+438
View File
@@ -0,0 +1,438 @@
# Access 数据库 CRUD 完整示例
使用 **System.Data.OleDb** 对 Access 数据库进行增删改查操作。
## 📦 连接字符串
```csharp
var connectionString = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=dbDwDmsSetup.accdb";
using var connection = new OleDbConnection(connectionString);
connection.Open();
```
---
## 1️⃣ 查询(SELECT)
### 查询所有用户
```csharp
using var connection = new OleDbConnection(connectionString);
connection.Open();
var users = new List<UserInfo>();
using var command = new OleDbCommand("SELECT UserName, Password FROM UserInfo", connection);
using var reader = command.ExecuteReader();
while (reader.Read())
{
var user = new UserInfo
{
UserName = reader["UserName"]?.ToString() ?? "",
Password = reader["Password"]?.ToString() ?? ""
};
users.Add(user);
}
Console.WriteLine($"查询到 {users.Count} 个用户");
```
### 条件查询
```csharp
// 根据用户名查询
using var command = new OleDbCommand(
"SELECT UserName, Password FROM UserInfo WHERE UserName = ?", connection);
command.Parameters.AddWithValue("@UserName", "admin");
using var reader = command.ExecuteReader();
if (reader.Read())
{
var userName = reader["UserName"]?.ToString();
var password = reader["Password"]?.ToString();
Console.WriteLine($"找到用户: {userName}");
}
```
### 统计数量
```csharp
using var command = new OleDbCommand(
"SELECT COUNT(*) FROM UserPermission WHERE UserInfo_UserName = ?", connection);
command.Parameters.AddWithValue("@UserName", "admin");
var count = Convert.ToInt32(command.ExecuteScalar());
Console.WriteLine($"该用户有 {count} 个权限");
```
---
## 2️⃣ 添加(INSERT)
### 添加用户
```csharp
using var connection = new OleDbConnection(connectionString);
connection.Open();
// 1. 先检查用户是否已存在
using (var checkCmd = new OleDbCommand(
"SELECT COUNT(*) FROM UserInfo WHERE UserName = ?", connection))
{
checkCmd.Parameters.AddWithValue("@UserName", "newuser");
var exists = Convert.ToInt32(checkCmd.ExecuteScalar()) > 0;
if (exists)
{
Console.WriteLine("用户已存在!");
return;
}
}
// 2. 插入新用户
using (var insertCmd = new OleDbCommand(
"INSERT INTO UserInfo (UserName, Password) VALUES (?, ?)", connection))
{
insertCmd.Parameters.AddWithValue("@UserName", "newuser");
insertCmd.Parameters.AddWithValue("@Password", "password123");
var rowsAffected = insertCmd.ExecuteNonQuery();
Console.WriteLine($"成功添加 {rowsAffected} 个用户");
}
```
### 添加权限
```csharp
using var insertCmd = new OleDbCommand(
"INSERT INTO UserPermission (UserInfo_UserName, Permission_Code) VALUES (?, ?)", connection);
insertCmd.Parameters.AddWithValue("@UserName", "newuser");
insertCmd.Parameters.AddWithValue("@PermissionCode", "READ_DATA");
var rowsAffected = insertCmd.ExecuteNonQuery();
Console.WriteLine($"成功添加权限: {rowsAffected} 条");
```
---
## 3️⃣ 修改(UPDATE)
### 修改用户密码
```csharp
using var connection = new OleDbConnection(connectionString);
connection.Open();
using var updateCmd = new OleDbCommand(
"UPDATE UserInfo SET Password = ? WHERE UserName = ?", connection);
updateCmd.Parameters.AddWithValue("@Password", "newpassword");
updateCmd.Parameters.AddWithValue("@UserName", "admin");
var rowsAffected = updateCmd.ExecuteNonQuery();
if (rowsAffected > 0)
{
Console.WriteLine("密码修改成功!");
}
else
{
Console.WriteLine("用户不存在!");
}
```
### 批量更新
```csharp
// 将所有用户的密码更新为默认密码
using var updateCmd = new OleDbCommand(
"UPDATE UserInfo SET Password = ? WHERE Password IS NULL OR Password = ''", connection);
updateCmd.Parameters.AddWithValue("@Password", "default123");
var rowsAffected = updateCmd.ExecuteNonQuery();
Console.WriteLine($"更新了 {rowsAffected} 个用户的密码");
```
---
## 4️⃣ 删除(DELETE)
### 删除用户(含事务)
```csharp
using var connection = new OleDbConnection(connectionString);
connection.Open();
// 使用事务确保数据一致性
using var transaction = connection.BeginTransaction();
try
{
// 1. 先删除用户的所有权限
using (var deletePermCmd = new OleDbCommand(
"DELETE FROM UserPermission WHERE UserInfo_UserName = ?", connection, transaction))
{
deletePermCmd.Parameters.AddWithValue("@UserName", "testuser");
var permCount = deletePermCmd.ExecuteNonQuery();
Console.WriteLine($"删除了 {permCount} 个权限");
}
// 2. 再删除用户
using (var deleteUserCmd = new OleDbCommand(
"DELETE FROM UserInfo WHERE UserName = ?", connection, transaction))
{
deleteUserCmd.Parameters.AddWithValue("@UserName", "testuser");
var userCount = deleteUserCmd.ExecuteNonQuery();
if (userCount == 0)
{
transaction.Rollback();
Console.WriteLine("用户不存在!");
return;
}
Console.WriteLine($"删除了 {userCount} 个用户");
}
// 3. 提交事务
transaction.Commit();
Console.WriteLine("删除成功!");
}
catch (Exception ex)
{
// 回滚事务
transaction.Rollback();
Console.WriteLine($"删除失败: {ex.Message}");
}
```
### 删除权限
```csharp
using var deleteCmd = new OleDbCommand(
"DELETE FROM UserPermission WHERE UserInfo_UserName = ? AND Permission_Code = ?", connection);
deleteCmd.Parameters.AddWithValue("@UserName", "admin");
deleteCmd.Parameters.AddWithValue("@PermissionCode", "DELETE_DATA");
var rowsAffected = deleteCmd.ExecuteNonQuery();
Console.WriteLine($"删除了 {rowsAffected} 个权限");
```
### 批量删除
```csharp
// 删除所有没有权限的用户
using var deleteCmd = new OleDbCommand(@"
DELETE FROM UserInfo
WHERE UserName NOT IN (
SELECT DISTINCT UserInfo_UserName FROM UserPermission
)", connection);
var rowsAffected = deleteCmd.ExecuteNonQuery();
Console.WriteLine($"删除了 {rowsAffected} 个无权限用户");
```
---
## 🔥 高级操作
### 1. JOIN 查询
```csharp
// 查询用户及其所有权限
using var command = new OleDbCommand(@"
SELECT u.UserName, u.Password, p.Permission_Code
FROM UserInfo u
LEFT JOIN UserPermission p ON u.UserName = p.UserInfo_UserName
ORDER BY u.UserName", connection);
using var reader = command.ExecuteReader();
while (reader.Read())
{
var userName = reader["UserName"]?.ToString();
var permission = reader["Permission_Code"]?.ToString();
Console.WriteLine($"{userName}: {permission ?? "(无权限)"}");
}
```
### 2. 分组统计
```csharp
// 统计每个用户的权限数量
using var command = new OleDbCommand(@"
SELECT u.UserName, COUNT(p.Permission_Code) AS PermCount
FROM UserInfo u
LEFT JOIN UserPermission p ON u.UserName = p.UserInfo_UserName
GROUP BY u.UserName
ORDER BY PermCount DESC", connection);
using var reader = command.ExecuteReader();
while (reader.Read())
{
var userName = reader["UserName"]?.ToString();
var count = Convert.ToInt32(reader["PermCount"]);
Console.WriteLine($"{userName}: {count} 个权限");
}
```
### 3. 参数化批量插入
```csharp
using var connection = new OleDbConnection(connectionString);
connection.Open();
var users = new[]
{
("user1", "pass1"),
("user2", "pass2"),
("user3", "pass3")
};
using var transaction = connection.BeginTransaction();
try
{
using var insertCmd = new OleDbCommand(
"INSERT INTO UserInfo (UserName, Password) VALUES (?, ?)", connection, transaction);
foreach (var (userName, password) in users)
{
insertCmd.Parameters.Clear();
insertCmd.Parameters.AddWithValue("@UserName", userName);
insertCmd.Parameters.AddWithValue("@Password", password);
insertCmd.ExecuteNonQuery();
}
transaction.Commit();
Console.WriteLine($"成功添加 {users.Length} 个用户");
}
catch
{
transaction.Rollback();
throw;
}
```
### 4. 存在性检查
```csharp
// 检查用户是否存在
public bool UserExists(string userName)
{
using var connection = new OleDbConnection(connectionString);
connection.Open();
using var command = new OleDbCommand(
"SELECT COUNT(*) FROM UserInfo WHERE UserName = ?", connection);
command.Parameters.AddWithValue("@UserName", userName);
return Convert.ToInt32(command.ExecuteScalar()) > 0;
}
// 使用
if (UserExists("admin"))
{
Console.WriteLine("用户存在");
}
```
---
## 📝 最佳实践
### 1. 使用 using 语句
```csharp
// ✅ 推荐:自动释放资源
using var connection = new OleDbConnection(connectionString);
using var command = new OleDbCommand(sql, connection);
using var reader = command.ExecuteReader();
// ❌ 不推荐:需要手动释放
var connection = new OleDbConnection(connectionString);
try
{
// ...
}
finally
{
connection.Dispose();
}
```
### 2. 使用参数化查询(防止 SQL 注入)
```csharp
// ✅ 推荐:参数化查询
var sql = "SELECT * FROM UserInfo WHERE UserName = ?";
command.Parameters.AddWithValue("@UserName", userName);
// ❌ 危险:字符串拼接(SQL 注入风险)
var sql = $"SELECT * FROM UserInfo WHERE UserName = '{userName}'";
```
### 3. 使用事务保证数据一致性
```csharp
// ✅ 推荐:使用事务
using var transaction = connection.BeginTransaction();
try
{
// 多个操作
command1.ExecuteNonQuery();
command2.ExecuteNonQuery();
transaction.Commit();
}
catch
{
transaction.Rollback();
throw;
}
```
### 4. 异常处理
```csharp
try
{
using var connection = new OleDbConnection(connectionString);
connection.Open();
// 数据库操作
}
catch (OleDbException ex)
{
Console.WriteLine($"数据库错误: {ex.Message}");
// 记录日志
}
catch (Exception ex)
{
Console.WriteLine($"未知错误: {ex.Message}");
// 记录日志
}
```
---
## 🎯 完整示例项目
完整的 CRUD 操作已集成到 **Server 项目**中:
**文件位置**:
- UI: `Server/Views/AccessDatabaseWindowOleDb.xaml`
- 代码: `Server/Views/AccessDatabaseWindowOleDb.xaml.cs`
**功能列表**:
- ✅ 查询所有用户
- ✅ 添加新用户
- ✅ 修改用户密码
- ✅ 删除用户(含级联删除权限)
- ✅ 添加用户权限
- ✅ 删除用户权限
- ✅ 事务支持
- ✅ 异常处理
- ✅ 异步操作(不阻塞 UI)
**运行方式**:
1. 启动 Server 项目
2. 点击 "Access数据库" 按钮
3. 尽情测试增删改查功能!
数据库随意修改,不用担心!🎉
+186
View File
@@ -0,0 +1,186 @@
# Access 数据库访问方案对比
## 方案选择
### ✅ 方案 A:System.Data.OleDb(推荐)
**适用场景**:
- 简单的增删改查操作
- 性能要求高
- 需要稳定可靠的方案
- 不需要复杂的 LINQ 查询
**实现文件**:
- `Views/AccessDatabaseWindowOleDb.xaml`
- `Views/AccessDatabaseWindowOleDb.xaml.cs`
**示例代码**:
```csharp
// 1. 创建连接
var connectionString = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=dbDwDmsSetup.accdb";
using var connection = new OleDbConnection(connectionString);
connection.Open();
// 2. 查询数据
using var command = new OleDbCommand("SELECT UserName, Password FROM UserInfo", connection);
using var reader = command.ExecuteReader();
while (reader.Read())
{
var userName = reader["UserName"]?.ToString();
var password = reader["Password"]?.ToString();
}
// 3. 插入数据
using var insertCmd = new OleDbCommand(
"INSERT INTO UserInfo (UserName, Password) VALUES (?, ?)", connection);
insertCmd.Parameters.AddWithValue("@UserName", "newuser");
insertCmd.Parameters.AddWithValue("@Password", "password");
insertCmd.ExecuteNonQuery();
// 4. 更新数据
using var updateCmd = new OleDbCommand(
"UPDATE UserInfo SET Password = ? WHERE UserName = ?", connection);
updateCmd.Parameters.AddWithValue("@Password", "newpassword");
updateCmd.Parameters.AddWithValue("@UserName", "username");
updateCmd.ExecuteNonQuery();
// 5. 删除数据
using var deleteCmd = new OleDbCommand(
"DELETE FROM UserInfo WHERE UserName = ?", connection);
deleteCmd.Parameters.AddWithValue("@UserName", "username");
deleteCmd.ExecuteNonQuery();
```
**优点**:
- ✅ 微软官方支持
- ✅ 无需额外 NuGet 包
- ✅ 性能最佳
- ✅ 稳定可靠
- ✅ 适用于所有 .NET 版本
**缺点**:
- ❌ 需要手写 SQL
- ❌ 没有 LINQ 支持
- ❌ 没有自动实体映射
---
### 方案 B:EntityFrameworkCore.Jet
**适用场景**:
- 需要复杂的 LINQ 查询
- 需要实体关系映射
- 代码优雅度要求高
- 可以接受第三方库
**实现文件**:
- `Views/AccessDatabaseWindow.xaml`
- `Views/AccessDatabaseWindow.xaml.cs`
- `Data/Access/AccessEntities.cs`
- `Data/Access/AccessDbContext.cs`
**示例代码**:
```csharp
// 1. 创建 DbContext
var optionsBuilder = new DbContextOptionsBuilder<AccessDbContext>();
optionsBuilder.UseJet("Provider=Microsoft.ACE.OLEDB.12.0;Data Source=dbDwDmsSetup.accdb");
using var dbContext = new AccessDbContext(optionsBuilder.Options);
// 2. LINQ 查询
var users = await dbContext.UserInfos
.Include(u => u.Permissions)
.Where(u => u.UserName.Contains("admin"))
.ToListAsync();
// 3. 插入数据
var newUser = new UserInfo { UserName = "newuser", Password = "password" };
dbContext.UserInfos.Add(newUser);
await dbContext.SaveChangesAsync();
// 4. 更新数据
var user = await dbContext.UserInfos.FindAsync("username");
if (user != null)
{
user.Password = "newpassword";
await dbContext.SaveChangesAsync();
}
// 5. 删除数据
var userToDelete = await dbContext.UserInfos.FindAsync("username");
if (userToDelete != null)
{
dbContext.UserInfos.Remove(userToDelete);
await dbContext.SaveChangesAsync();
}
```
**优点**:
- ✅ LINQ 查询支持
- ✅ 自动实体映射
- ✅ 代码更优雅
- ✅ 支持导航属性
**缺点**:
- ❌ 第三方库(EntityFrameworkCore.Jet)
- ❌ 版本兼容性问题
- ❌ 性能开销
- ❌ 可能与其他 EF Core 提供程序冲突
---
## 推荐方案
### 对于简单的增删改查:
**推荐使用方案 A(System.Data.OleDb)**
理由:
1. 稳定可靠,微软官方支持
2. 性能最佳
3. 无依赖问题
4. 代码简单直接
### 对于复杂的业务逻辑:
可以考虑方案 B(EntityFrameworkCore.Jet),但需要:
1. 接受第三方库的风险
2. 处理版本兼容性问题
3. 确保不与其他数据库提供程序冲突
---
## 当前 Server 项目使用的方案
当前使用:**方案 A(System.Data.OleDb)** ✅
主窗口按钮 "Access数据库" 打开的是 `AccessDatabaseWindowOleDb` 窗口。
如需切换到方案 B,修改 `MainWindow.xaml.cs` 中的 `AccessDbButton_Click` 方法:
```csharp
// 方案 A(当前)
var window = new AccessDatabaseWindowOleDb { Owner = this };
// 方案 B
var window = new AccessDatabaseWindow { Owner = this };
```
---
## 性能对比
| 操作 | OleDb | EF Core Jet |
|------|-------|-------------|
| 简单查询 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| 复杂查询 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 插入操作 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| 批量操作 | ⭐⭐⭐⭐⭐ | ⭐⭐ |
| 内存占用 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
---
## 总结
对于你的需求(简单的增删改查),**强烈推荐使用 System.Data.OleDb(方案 A)**。
它是最稳定、性能最好、最简单的方案。
+2
View File
@@ -55,6 +55,8 @@
Background="#FF9800" Foreground="White" BorderThickness="0" FontWeight="Bold" Cursor="Hand" IsEnabled="False"/>
<Button x:Name="DataManagementButton" Content="数据管理" Width="100" Height="32" Margin="0,0,10,0" Click="DataManagementButton_Click"
Background="#673AB7" Foreground="White" BorderThickness="0" FontWeight="Bold" Cursor="Hand"/>
<Button x:Name="AccessDbButton" Content="Access数据库" Width="110" Height="32" Margin="0,0,10,0" Click="AccessDbButton_Click"
Background="#009688" Foreground="White" BorderThickness="0" FontWeight="Bold" Cursor="Hand"/>
<Button x:Name="ClearLogButton" Content="清空日志" Width="100" Height="32" Click="ClearLogButton_Click"
Background="#9E9E9E" Foreground="White" BorderThickness="0" FontWeight="Bold" Cursor="Hand"/>
</StackPanel>
+20
View File
@@ -155,6 +155,26 @@ namespace Server
}
}
private void AccessDbButton_Click(object sender, RoutedEventArgs e)
{
try
{
_logger?.LogInformation("打开 Access 数据库管理窗口 (OleDb)");
var window = new AccessDatabaseWindowOleDb
{
Owner = this
};
window.ShowDialog();
_logger?.LogInformation("Access 数据库管理窗口已关闭");
}
catch (Exception ex)
{
_logger?.LogError(ex, "打开 Access 数据库管理窗口失败");
AppendLog($"打开 Access 数据库管理窗口失败: {ex.Message}");
MessageBox.Show($"打开 Access 数据库管理窗口失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void OnServerStatusChanged(object? sender, string status)
{
Dispatcher.Invoke(() =>
+7
View File
@@ -7,6 +7,9 @@
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
<!-- 强制使用 x64 架构,与已安装的 64 位 ACE 驱动匹配 -->
<PlatformTarget>x64</PlatformTarget>
<PublishSingleFile>true</PublishSingleFile>
<SelfContained>true</SelfContained>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
@@ -22,6 +25,7 @@
<ProjectReference Include="..\Wpf\Wpf.csproj" />
<ProjectReference Include="..\Common\Common.csproj" />
<ProjectReference Include="..\Data\Data.csproj" />
<ProjectReference Include="..\Data.Access\Data.Access.csproj" />
</ItemGroup>
<ItemGroup>
@@ -34,6 +38,9 @@
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="dbDwDmsSetup.accdb">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
@@ -0,0 +1,54 @@
<Window x:Class="Server.Views.AccessDatabaseWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Title="Access 数据库管理" Height="600" Width="900"
WindowStartupLocation="CenterScreen">
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="200"/>
</Grid.RowDefinitions>
<!-- 标题和操作栏 -->
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,10">
<TextBlock Text="Access 数据库管理" FontSize="20" FontWeight="Bold" VerticalAlignment="Center"/>
<TextBlock Text=" (dbDwDmsSetup.accdb)" FontSize="14" Foreground="Gray" VerticalAlignment="Center" Margin="5,0,0,0"/>
<Button Content="刷新数据" Click="RefreshButton_Click" Margin="20,0,0,0" Padding="10,5"/>
<Button Content="测试连接" Click="TestConnectionButton_Click" Margin="10,0,0,0" Padding="10,5"/>
</StackPanel>
<!-- 用户列表 -->
<GroupBox Grid.Row="1" Header="用户列表" Margin="0,0,0,10">
<DataGrid x:Name="UsersDataGrid"
AutoGenerateColumns="False"
IsReadOnly="True"
SelectionMode="Single"
SelectionChanged="UsersDataGrid_SelectionChanged">
<DataGrid.Columns>
<DataGridTextColumn Header="用户名" Binding="{Binding UserName}" Width="200"/>
<DataGridTextColumn Header="密码" Binding="{Binding Password}" Width="*"/>
<DataGridTextColumn Header="权限数量" Binding="{Binding Permissions.Count}" Width="100"/>
</DataGrid.Columns>
</DataGrid>
</GroupBox>
<!-- 分隔符 -->
<GridSplitter Grid.Row="2" Height="5" HorizontalAlignment="Stretch" Background="LightGray"/>
<!-- 权限列表 -->
<GroupBox Grid.Row="3" Header="选中用户的权限">
<DataGrid x:Name="PermissionsDataGrid"
AutoGenerateColumns="False"
IsReadOnly="True">
<DataGrid.Columns>
<DataGridTextColumn Header="权限代码" Binding="{Binding Permission_Code}" Width="*"/>
</DataGrid.Columns>
</DataGrid>
</GroupBox>
</Grid>
</Window>
@@ -0,0 +1,146 @@
using System;
using System.Data.OleDb;
using System.IO;
using System.Linq;
using System.Windows;
using EntityFrameworkCore.Jet;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Sharp.Data.Access.Options;
using Server.Data.Access;
namespace Server.Views;
public partial class AccessDatabaseWindow : Window
{
private readonly ILogger<AccessDatabaseWindow>? _logger;
private const string DatabaseFile = "dbDwDmsSetup.accdb";
public AccessDatabaseWindow()
{
InitializeComponent();
try
{
var app = (App)Application.Current;
_logger = app.LoggerFactory?.CreateLogger<AccessDatabaseWindow>();
Loaded += AccessDatabaseWindow_Loaded;
}
catch (Exception ex)
{
MessageBox.Show($"初始化失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private async void AccessDatabaseWindow_Loaded(object sender, RoutedEventArgs e)
{
await LoadDataAsync();
}
private async void RefreshButton_Click(object sender, RoutedEventArgs e)
{
await LoadDataAsync();
}
private async void TestConnectionButton_Click(object sender, RoutedEventArgs e)
{
try
{
_logger?.LogInformation("测试 Access 数据库连接...");
using var dbContext = CreateDbContext();
var canConnect = await dbContext.Database.CanConnectAsync();
if (canConnect)
{
_logger?.LogInformation("数据库连接成功!");
MessageBox.Show("数据库连接成功!", "成功", MessageBoxButton.OK, MessageBoxImage.Information);
}
else
{
_logger?.LogWarning("无法连接到数据库");
MessageBox.Show("无法连接到数据库", "警告", MessageBoxButton.OK, MessageBoxImage.Warning);
}
}
catch (Exception ex)
{
_logger?.LogError(ex, "测试连接失败");
MessageBox.Show($"测试连接失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private async System.Threading.Tasks.Task LoadDataAsync()
{
try
{
_logger?.LogInformation("开始加载 Access 数据库数据...");
using var dbContext = CreateDbContext();
// 查询所有用户及其权限
var users = await dbContext.UserInfos
.Include(u => u.Permissions)
.ToListAsync();
_logger?.LogInformation($"加载了 {users.Count} 个用户");
// 更新 UI
UsersDataGrid.ItemsSource = users;
PermissionsDataGrid.ItemsSource = null;
MessageBox.Show($"成功加载 {users.Count} 个用户!", "成功", MessageBoxButton.OK, MessageBoxImage.Information);
}
catch (Exception ex)
{
_logger?.LogError(ex, "加载数据失败");
MessageBox.Show($"加载数据失败: {ex.Message}\n\n详细信息:\n{ex.ToString()}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void UsersDataGrid_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
{
if (UsersDataGrid.SelectedItem is UserInfo selectedUser)
{
_logger?.LogInformation($"选中用户: {selectedUser.UserName}, 权限数: {selectedUser.Permissions.Count}");
PermissionsDataGrid.ItemsSource = selectedUser.Permissions;
}
else
{
PermissionsDataGrid.ItemsSource = null;
}
}
/// <summary>
/// 创建 Access 数据库上下文(使用新的统一配置体系)
/// </summary>
private AccessDbContext CreateDbContext()
{
// 获取数据库文件的完整路径
var dbPath = Path.GetFullPath(DatabaseFile);
if (!File.Exists(dbPath))
{
throw new FileNotFoundException($"找不到 Access 数据库文件: {dbPath}");
}
// 使用统一的配置方式构建连接字符串
var builder = new OleDbConnectionStringBuilder
{
["Provider"] = AccessProviderNames.AceOleDb16, // 使用 16.0
["Data Source"] = dbPath
// Connect Timeout 在某些情况下不稳定,已移除
};
// 如果数据库有密码,可以在这里设置:
// builder["Jet OLEDB:Database Password"] = "your_password";
var connectionString = builder.ConnectionString;
// 创建 DbContext 选项
var optionsBuilder = new DbContextOptionsBuilder<AccessDbContext>();
optionsBuilder.UseJet(connectionString);
return new AccessDbContext(optionsBuilder.Options);
}
}
@@ -0,0 +1,76 @@
<Window x:Class="Server.Views.AccessDatabaseWindowOleDb"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Access 数据库管理 (OleDb方式)" Height="700" Width="1000"
WindowStartupLocation="CenterScreen">
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="200"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 标题和操作栏 -->
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,10">
<TextBlock Text="Access 数据库管理" FontSize="20" FontWeight="Bold" VerticalAlignment="Center"/>
<TextBlock Text=" (System.Data.OleDb)" FontSize="14" Foreground="Green" VerticalAlignment="Center" Margin="5,0,0,0"/>
<Button Content="刷新数据" Click="RefreshButton_Click" Margin="20,0,0,0" Padding="10,5"/>
<Button Content="测试连接" Click="TestConnectionButton_Click" Margin="10,0,0,0" Padding="10,5"/>
</StackPanel>
<!-- 用户操作区 -->
<GroupBox Grid.Row="1" Header="用户操作" Margin="0,0,0,10">
<StackPanel Orientation="Horizontal" Margin="5">
<TextBlock Text="用户名:" VerticalAlignment="Center" Margin="0,0,5,0"/>
<TextBox x:Name="UserNameTextBox" Width="150" Margin="0,0,10,0"/>
<TextBlock Text="密码:" VerticalAlignment="Center" Margin="0,0,5,0"/>
<TextBox x:Name="PasswordTextBox" Width="150" Margin="0,0,20,0"/>
<Button Content="添加用户" Click="AddUserButton_Click" Padding="10,5" Margin="0,0,5,0" Background="#4CAF50" Foreground="White"/>
<Button Content="修改密码" Click="UpdatePasswordButton_Click" Padding="10,5" Margin="0,0,5,0" Background="#FF9800" Foreground="White"/>
<Button Content="删除用户" Click="DeleteUserButton_Click" Padding="10,5" Background="#F44336" Foreground="White"/>
</StackPanel>
</GroupBox>
<!-- 用户列表 -->
<GroupBox Grid.Row="2" Header="用户列表" Margin="0,0,0,10">
<DataGrid x:Name="UsersDataGrid"
AutoGenerateColumns="False"
IsReadOnly="True"
SelectionMode="Single"
SelectionChanged="UsersDataGrid_SelectionChanged">
<DataGrid.Columns>
<DataGridTextColumn Header="用户名" Binding="{Binding UserName}" Width="200"/>
<DataGridTextColumn Header="密码" Binding="{Binding Password}" Width="*"/>
<DataGridTextColumn Header="权限数量" Binding="{Binding PermissionCount}" Width="100"/>
</DataGrid.Columns>
</DataGrid>
</GroupBox>
<!-- 分隔符 -->
<GridSplitter Grid.Row="3" Height="5" HorizontalAlignment="Stretch" Background="LightGray"/>
<!-- 权限列表 -->
<GroupBox Grid.Row="4" Header="选中用户的权限">
<DataGrid x:Name="PermissionsDataGrid"
AutoGenerateColumns="False"
IsReadOnly="True">
<DataGrid.Columns>
<DataGridTextColumn Header="权限代码" Binding="{Binding Permission_Code}" Width="*"/>
</DataGrid.Columns>
</DataGrid>
</GroupBox>
<!-- 权限操作区 -->
<GroupBox Grid.Row="5" Header="权限操作" Margin="0,10,0,0">
<StackPanel Orientation="Horizontal" Margin="5">
<TextBlock Text="权限代码:" VerticalAlignment="Center" Margin="0,0,5,0"/>
<TextBox x:Name="PermissionCodeTextBox" Width="200" Margin="0,0,20,0"/>
<Button Content="添加权限" Click="AddPermissionButton_Click" Padding="10,5" Margin="0,0,5,0" Background="#4CAF50" Foreground="White"/>
<Button Content="删除权限" Click="DeletePermissionButton_Click" Padding="10,5" Background="#F44336" Foreground="White"/>
</StackPanel>
</GroupBox>
</Grid>
</Window>
@@ -0,0 +1,560 @@
using System;
using System.Collections.Generic;
using System.Data.OleDb;
using System.Linq;
using System.Windows;
using Microsoft.Extensions.Logging;
using Sharp.Data.Access;
namespace Server.Views;
/// <summary>
/// 使用 AccessDataService 访问 Access 数据库(推荐方式)
/// 包含完整的增删改查功能
/// </summary>
public partial class AccessDatabaseWindowOleDb : Window
{
private readonly IAccessDataService? _accessDataService;
private readonly ILogger<AccessDatabaseWindowOleDb>? _logger;
private string? _selectedUserName;
public AccessDatabaseWindowOleDb()
{
InitializeComponent();
try
{
var app = (App)Application.Current;
_logger = app.LoggerFactory?.CreateLogger<AccessDatabaseWindowOleDb>();
_accessDataService = app.AccessDataService;
if (_accessDataService == null)
{
_logger?.LogWarning("AccessDataService 未初始化");
MessageBox.Show("Access 数据服务未初始化,某些功能可能不可用", "警告",
MessageBoxButton.OK, MessageBoxImage.Warning);
}
Loaded += Window_Loaded;
}
catch (Exception ex)
{
MessageBox.Show($"初始化失败: {ex.Message}", "错误",
MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private async void Window_Loaded(object sender, RoutedEventArgs e)
{
await Task.Run(() => LoadData());
}
#region 基础操作
private async void RefreshButton_Click(object sender, RoutedEventArgs e)
{
await Task.Run(() => LoadData());
}
private async void TestConnectionButton_Click(object sender, RoutedEventArgs e)
{
if (_accessDataService == null)
{
MessageBox.Show("Access 数据服务未初始化", "错误",
MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
try
{
_logger?.LogInformation("测试 Access 数据库连接...");
var success = await Task.Run(() => _accessDataService.TestConnection());
if (success)
{
_logger?.LogInformation("数据库连接成功!");
MessageBox.Show("数据库连接成功!", "成功",
MessageBoxButton.OK, MessageBoxImage.Information);
}
else
{
MessageBox.Show("数据库连接失败", "错误",
MessageBoxButton.OK, MessageBoxImage.Error);
}
}
catch (Exception ex)
{
_logger?.LogError(ex, "测试连接失败");
MessageBox.Show($"测试连接失败: {ex.Message}", "错误",
MessageBoxButton.OK, MessageBoxImage.Error);
}
}
#endregion
#region 用户管理(增删改查)
/// <summary>
/// 添加用户
/// </summary>
private async void AddUserButton_Click(object sender, RoutedEventArgs e)
{
if (_accessDataService == null) return;
var userName = UserNameTextBox.Text.Trim();
var password = PasswordTextBox.Text.Trim();
if (string.IsNullOrEmpty(userName))
{
MessageBox.Show("请输入用户名", "提示",
MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
if (string.IsNullOrEmpty(password))
{
MessageBox.Show("请输入密码", "提示",
MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
try
{
await Task.Run(() =>
{
// 检查用户是否已存在
var count = _accessDataService.ExecuteScalar(
"SELECT COUNT(*) FROM UserInfo WHERE UserName = ?",
new OleDbParameter("@UserName", userName));
if (Convert.ToInt32(count) > 0)
{
Dispatcher.Invoke(() =>
{
MessageBox.Show($"用户 '{userName}' 已存在!", "警告",
MessageBoxButton.OK, MessageBoxImage.Warning);
});
return;
}
// 插入新用户
_accessDataService.Execute(
"INSERT INTO UserInfo (UserName, [Password]) VALUES (?, ?)",
new OleDbParameter("@UserName", userName),
new OleDbParameter("@Password", password));
_logger?.LogInformation($"成功添加用户: {userName}");
Dispatcher.Invoke(() =>
{
MessageBox.Show($"成功添加用户 '{userName}'!", "成功",
MessageBoxButton.OK, MessageBoxImage.Information);
UserNameTextBox.Clear();
PasswordTextBox.Clear();
});
LoadData();
});
}
catch (Exception ex)
{
_logger?.LogError(ex, "添加用户失败");
MessageBox.Show($"添加用户失败: {ex.Message}", "错误",
MessageBoxButton.OK, MessageBoxImage.Error);
}
}
/// <summary>
/// 修改用户密码
/// </summary>
private async void UpdatePasswordButton_Click(object sender, RoutedEventArgs e)
{
if (_accessDataService == null) return;
var userName = UserNameTextBox.Text.Trim();
var newPassword = PasswordTextBox.Text.Trim();
if (string.IsNullOrEmpty(userName))
{
MessageBox.Show("请输入用户名或从列表中选择用户", "提示",
MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
if (string.IsNullOrEmpty(newPassword))
{
MessageBox.Show("请输入新密码", "提示",
MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
var result = MessageBox.Show(
$"确定要修改用户 '{userName}' 的密码吗?",
"确认",
MessageBoxButton.YesNo,
MessageBoxImage.Question);
if (result != MessageBoxResult.Yes)
return;
try
{
await Task.Run(() =>
{
// 更新密码
var rowsAffected = _accessDataService.Execute(
"UPDATE UserInfo SET [Password] = ? WHERE UserName = ?",
new OleDbParameter("@Password", newPassword),
new OleDbParameter("@UserName", userName));
if (rowsAffected == 0)
{
Dispatcher.Invoke(() =>
{
MessageBox.Show($"用户 '{userName}' 不存在!", "警告",
MessageBoxButton.OK, MessageBoxImage.Warning);
});
return;
}
_logger?.LogInformation($"成功修改用户 {userName} 的密码");
Dispatcher.Invoke(() =>
{
MessageBox.Show($"成功修改用户 '{userName}' 的密码!", "成功",
MessageBoxButton.OK, MessageBoxImage.Information);
PasswordTextBox.Clear();
});
LoadData();
});
}
catch (Exception ex)
{
_logger?.LogError(ex, "修改密码失败");
MessageBox.Show($"修改密码失败: {ex.Message}", "错误",
MessageBoxButton.OK, MessageBoxImage.Error);
}
}
/// <summary>
/// 删除用户
/// </summary>
private async void DeleteUserButton_Click(object sender, RoutedEventArgs e)
{
if (_accessDataService == null) return;
var userName = UserNameTextBox.Text.Trim();
if (string.IsNullOrEmpty(userName))
{
MessageBox.Show("请输入用户名或从列表中选择用户", "提示",
MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
var result = MessageBox.Show(
$"确定要删除用户 '{userName}' 及其所有权限吗?\n此操作不可恢复!",
"确认删除",
MessageBoxButton.YesNo,
MessageBoxImage.Warning);
if (result != MessageBoxResult.Yes)
return;
try
{
// 使用事务删除用户及其权限
await Task.Run(() =>
{
_accessDataService.ExecuteInTransaction(transaction =>
{
// 先删除用户的所有权限
transaction.Execute(
"DELETE FROM UserPermission WHERE UserInfo_UserName = ?",
new OleDbParameter("@UserName", userName));
// 再删除用户
var rowsAffected = transaction.Execute(
"DELETE FROM UserInfo WHERE UserName = ?",
new OleDbParameter("@UserName", userName));
if (rowsAffected == 0)
{
throw new InvalidOperationException($"用户 '{userName}' 不存在!");
}
});
_logger?.LogInformation($"成功删除用户: {userName}");
Dispatcher.Invoke(() =>
{
MessageBox.Show($"成功删除用户 '{userName}'!", "成功",
MessageBoxButton.OK, MessageBoxImage.Information);
UserNameTextBox.Clear();
PasswordTextBox.Clear();
});
LoadData();
});
}
catch (InvalidOperationException ex)
{
MessageBox.Show(ex.Message, "警告",
MessageBoxButton.OK, MessageBoxImage.Warning);
}
catch (Exception ex)
{
_logger?.LogError(ex, "删除用户失败");
MessageBox.Show($"删除用户失败: {ex.Message}", "错误",
MessageBoxButton.OK, MessageBoxImage.Error);
}
}
#endregion
#region 权限管理
/// <summary>
/// 添加权限
/// </summary>
private async void AddPermissionButton_Click(object sender, RoutedEventArgs e)
{
if (_accessDataService == null) return;
if (string.IsNullOrEmpty(_selectedUserName))
{
MessageBox.Show("请先选择一个用户", "提示",
MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
var permissionCode = PermissionCodeTextBox.Text.Trim();
if (string.IsNullOrEmpty(permissionCode))
{
MessageBox.Show("请输入权限代码", "提示",
MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
try
{
await Task.Run(() =>
{
// 检查权限是否已存在
var count = _accessDataService.ExecuteScalar(
"SELECT COUNT(*) FROM UserPermission WHERE UserInfo_UserName = ? AND Permission_Code = ?",
new OleDbParameter("@UserName", _selectedUserName),
new OleDbParameter("@PermissionCode", permissionCode));
if (Convert.ToInt32(count) > 0)
{
Dispatcher.Invoke(() =>
{
MessageBox.Show($"用户 '{_selectedUserName}' 已拥有权限 '{permissionCode}'!", "警告",
MessageBoxButton.OK, MessageBoxImage.Warning);
});
return;
}
// 插入权限
_accessDataService.Execute(
"INSERT INTO UserPermission (UserInfo_UserName, Permission_Code) VALUES (?, ?)",
new OleDbParameter("@UserName", _selectedUserName),
new OleDbParameter("@PermissionCode", permissionCode));
_logger?.LogInformation($"成功为用户 {_selectedUserName} 添加权限: {permissionCode}");
Dispatcher.Invoke(() =>
{
MessageBox.Show($"成功添加权限 '{permissionCode}'!", "成功",
MessageBoxButton.OK, MessageBoxImage.Information);
PermissionCodeTextBox.Clear();
LoadUserPermissions(_selectedUserName);
});
});
}
catch (Exception ex)
{
_logger?.LogError(ex, "添加权限失败");
MessageBox.Show($"添加权限失败: {ex.Message}", "错误",
MessageBoxButton.OK, MessageBoxImage.Error);
}
}
/// <summary>
/// 删除权限
/// </summary>
private async void DeletePermissionButton_Click(object sender, RoutedEventArgs e)
{
if (_accessDataService == null) return;
if (string.IsNullOrEmpty(_selectedUserName))
{
MessageBox.Show("请先选择一个用户", "提示",
MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
if (PermissionsDataGrid.SelectedItem is not UserPermissionDto selectedPermission)
{
MessageBox.Show("请从列表中选择要删除的权限", "提示",
MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
var result = MessageBox.Show(
$"确定要删除用户 '{_selectedUserName}' 的权限 '{selectedPermission.Permission_Code}' 吗?",
"确认删除",
MessageBoxButton.YesNo,
MessageBoxImage.Question);
if (result != MessageBoxResult.Yes)
return;
try
{
await Task.Run(() =>
{
_accessDataService.Execute(
"DELETE FROM UserPermission WHERE UserInfo_UserName = ? AND Permission_Code = ?",
new OleDbParameter("@UserName", _selectedUserName),
new OleDbParameter("@PermissionCode", selectedPermission.Permission_Code));
_logger?.LogInformation($"成功删除用户 {_selectedUserName} 的权限: {selectedPermission.Permission_Code}");
Dispatcher.Invoke(() =>
{
MessageBox.Show($"成功删除权限 '{selectedPermission.Permission_Code}'!", "成功",
MessageBoxButton.OK, MessageBoxImage.Information);
LoadUserPermissions(_selectedUserName);
});
});
}
catch (Exception ex)
{
_logger?.LogError(ex, "删除权限失败");
MessageBox.Show($"删除权限失败: {ex.Message}", "错误",
MessageBoxButton.OK, MessageBoxImage.Error);
}
}
#endregion
#region 数据加载
private void LoadData()
{
if (_accessDataService == null) return;
try
{
_logger?.LogInformation("开始加载 Access 数据库数据...");
// 查询所有用户(使用泛型映射)
var users = _accessDataService.Query<UserInfoDto>(
"SELECT [UserName], [Password] AS [Password] FROM [UserInfo]");
var userDetails = string.Join(" | ",
users.Select(u => $"UserName='{u.UserName}', Password='{u.Password}'"));
_logger?.LogInformation("用户列表: {UserDetails}", userDetails);
// 为每个用户查询权限数量
foreach (var user in users)
{
var count = _accessDataService.ExecuteScalar(
"SELECT COUNT(*) FROM UserPermission WHERE UserInfo_UserName = ?",
new OleDbParameter("@UserName", user.UserName));
user.PermissionCount = Convert.ToInt32(count);
}
_logger?.LogInformation($"加载了 {users.Count} 个用户");
// 更新 UI(需要在 UI 线程上执行)
Dispatcher.Invoke(() =>
{
UsersDataGrid.ItemsSource = users;
PermissionsDataGrid.ItemsSource = null;
_selectedUserName = null;
});
}
catch (Exception ex)
{
_logger?.LogError(ex, "加载数据失败");
Dispatcher.Invoke(() =>
{
MessageBox.Show($"加载数据失败: {ex.Message}", "错误",
MessageBoxButton.OK, MessageBoxImage.Error);
});
}
}
private void UsersDataGrid_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
{
if (UsersDataGrid.SelectedItem is UserInfoDto selectedUser)
{
_selectedUserName = selectedUser.UserName;
UserNameTextBox.Text = selectedUser.UserName;
PasswordTextBox.Text = selectedUser.Password;
LoadUserPermissions(selectedUser.UserName);
}
else
{
_selectedUserName = null;
PermissionsDataGrid.ItemsSource = null;
}
}
private void LoadUserPermissions(string userName)
{
if (_accessDataService == null) return;
try
{
// 查询用户权限(使用泛型映射)
var permissions = _accessDataService.Query<UserPermissionDto>(
"SELECT Permission_Code FROM UserPermission WHERE UserInfo_UserName = ?",
new OleDbParameter("@UserName", userName));
Dispatcher.Invoke(() =>
{
PermissionsDataGrid.ItemsSource = permissions;
});
_logger?.LogInformation($"用户 {userName} 有 {permissions.Count} 个权限");
}
catch (Exception ex)
{
_logger?.LogError(ex, "加载权限失败");
Dispatcher.Invoke(() =>
{
MessageBox.Show($"加载权限失败: {ex.Message}", "错误",
MessageBoxButton.OK, MessageBoxImage.Error);
});
}
}
#endregion
}
/// <summary>
/// 用户信息 DTO
/// </summary>
public class UserInfoDto
{
public string UserName { get; set; } = "";
public string Password { get; set; } = "";
public int PermissionCount { get; set; }
}
/// <summary>
/// 用户权限 DTO
/// </summary>
public class UserPermissionDto
{
public string Permission_Code { get; set; } = "";
}
+1 -1
View File
@@ -6,7 +6,7 @@
},
"SharpLog": {
"UseConsole": true,
"MinLevel": "Information",
"MinLevel": "Debug",
"File": {
"Enabled": true,
"Path": "logs/server-.log",
Binary file not shown.
@@ -0,0 +1,4 @@
{"Timestamp":"2025-11-19T10:17:05.3241502+08:00","Level":"Information","MessageTemplate":"正在初始化数据库...","RenderedMessage":"正在初始化数据库...","Properties":{"SourceContext":"Server.App","MachineName":"WEIYP","ProcessId":51312,"ThreadId":1,"EnvironmentName":"Production","Application":"SharpServer","Environment":"Production"}}
{"Timestamp":"2025-11-19T10:17:06.0810621+08:00","Level":"Information","MessageTemplate":"数据库初始化成功","RenderedMessage":"数据库初始化成功","Properties":{"SourceContext":"Server.App","MachineName":"WEIYP","ProcessId":51312,"ThreadId":1,"EnvironmentName":"Production","Application":"SharpServer","Environment":"Production"}}
{"Timestamp":"2025-11-19T10:17:06.0815557+08:00","Level":"Information","MessageTemplate":"应用程序启动成功","RenderedMessage":"应用程序启动成功","Properties":{"SourceContext":"Server.App","MachineName":"WEIYP","ProcessId":51312,"ThreadId":1,"EnvironmentName":"Production","Application":"SharpServer","Environment":"Production"}}
{"Timestamp":"2025-11-19T10:21:42.6459518+08:00","Level":"Information","MessageTemplate":"应用程序正常退出","RenderedMessage":"应用程序正常退出","Properties":{"SourceContext":"Server.App","MachineName":"WEIYP","ProcessId":51312,"ThreadId":1,"EnvironmentName":"Production","Application":"SharpServer","Environment":"Production"}}
+12
View File
@@ -39,6 +39,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Data", "Data\Data.csproj",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Data.Test", "..\..\test\Data.Test\Data.Test.csproj", "{8EEC8BF4-CF9A-473D-B7A0-005E151C645D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "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
GlobalSection(SolutionConfigurationPlatforms) = preSolution
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}.Release|Any CPU.ActiveCfg = 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
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -15,7 +15,7 @@ public sealed class ExceptionHandlingOptions
public bool RethrowAfterHandle { get; set; } = false;
/// <summary>
/// Custom logger hook.
/// 日志钩子.
/// </summary>
public Action<Exception>? Log { get; set; }
= ex => System.Diagnostics.Debug.WriteLine(ex.ToString());
@@ -1,4 +1,5 @@
using System;
using System.Net.Http;
namespace Sharp.Wpf.Exceptions;
@@ -1,4 +1,5 @@
using System;
using System.Net.Http;
using System.Threading.Tasks;
using WpfMessageBox = System.Windows.MessageBox;
@@ -35,6 +36,10 @@ public sealed class UiExceptionHandler : IUiExceptionHandler
private void OnDispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e)
{
Handle(e.Exception);
if (CanRecover(e.Exception))
{
e.Handled = true;
}
e.Handled = true;
}
@@ -86,4 +91,20 @@ public sealed class UiExceptionHandler : IUiExceptionHandler
}
}
}
protected bool CanRecover(Exception exception)
{
return exception switch
{
ArgumentException => true,
InvalidOperationException => true,
HttpRequestException => true,
TimeoutException => true,
OutOfMemoryException => false,
StackOverflowException => false,
AccessViolationException => false,
_ => true
};
}
}
@@ -1,10 +1,9 @@
//------------------------------------------------------------------------------
// <auto-generated>
// 此代码由工具生成。
// 运行时版本:4.0.30319.42000
// This code was generated by a tool.
//
// 对此文件的更改可能会导致不正确的行为,并且如果
// 重新生成代码,这些更改将会丢失。
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
@@ -14,7 +13,7 @@ using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("Sharp.Wpf")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+53d1dd2dbf05d818528814223b58610a790fcb26")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+e334e65c2d8abbef0063f92b88e6672859b4ea19")]
[assembly: System.Reflection.AssemblyProductAttribute("Sharp.Wpf")]
[assembly: System.Reflection.AssemblyTitleAttribute("Sharp.Wpf")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
@@ -1 +1 @@
fbdba11393faed768ef4c9b8f4eccef99aed87397a79576b900953e4aad05633
b6ad12dd3a3fcd3e5222848b29876246dac97d6908271610ecbcd090928adcb7
@@ -70,11 +70,6 @@
},
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.203/PortableRuntimeIdentifierGraph.json"
}
},
"runtimes": {
"win-x64": {
"#import": []
}
}
}
}
+1 -7
View File
@@ -1,8 +1,7 @@
{
"version": 3,
"targets": {
"net8.0-windows7.0": {},
"net8.0-windows7.0/win-x64": {}
"net8.0-windows7.0": {}
},
"libraries": {},
"projectFileDependencyGroups": {
@@ -78,11 +77,6 @@
},
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.203/PortableRuntimeIdentifierGraph.json"
}
},
"runtimes": {
"win-x64": {
"#import": []
}
}
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"version": 2,
"dgSpecHash": "9ZityDkLrvk=",
"dgSpecHash": "EQ+I1wXrehg=",
"success": true,
"projectFilePath": "D:\\yuanll\\sharp-basic\\src\\Sharp\\Wpf\\Wpf.csproj",
"expectedPackageFiles": [],
@@ -1 +1 @@
17634268840015055
17634526945569668
+1 -1
View File
@@ -1 +1 @@
17633637013370903
17634577083448171
@@ -0,0 +1,74 @@
using System;
using System.IO;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Sharp.Data.Access;
using Sharp.Data.Access.DependencyInjection;
using Sharp.Data.Access.Options;
using Sharp.Data.Access.Security;
using Sharp.Data.Access.Connections;
namespace Data.Access.Test;
[TestClass]
public sealed class AccessConnectionStringFactoryTests
{
[TestMethod]
public void Create_UsesDecryptedPasswordAndAdditionalParameters()
{
var tempDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(tempDirectory);
var options = new AccessDatabaseOptions
{
DatabaseFile = "data/app.accdb",
DataDirectory = tempDirectory,
EncryptedPassword = "encrypted-secret",
ProviderName = AccessProviderNames.AceOleDb12
};
options.AdditionalParameters["Jet OLEDB:Database Locking Mode"] = "1";
var decryptor = new FakeDecryptor("plain-text");
var services = new ServiceCollection();
services.AddSingleton<IAccessPasswordDecryptor>(decryptor);
services.AddAccessDataContext<FakeContext>(ctx =>
{
ctx.Database.ConnectionString = $"Provider={AccessProviderNames.AceOleDb16};Data Source=fake;";
}, registerDataContext: false);
var provider = services.BuildServiceProvider();
var factory = provider.GetRequiredService<IAccessConnectionStringFactory>();
var connectionString = factory.Create(options, provider);
var expectedPath = Path.GetFullPath(Path.Combine(tempDirectory, "data", "app.accdb"));
StringAssert.Contains(connectionString, $"Provider={AccessProviderNames.AceOleDb12}", "Provider should be set.");
StringAssert.Contains(connectionString, expectedPath, "Database path should be resolved to absolute path.");
StringAssert.Contains(connectionString, "Jet OLEDB:Database Password=plain-text", "Decrypted password should be used.");
StringAssert.Contains(connectionString, "Jet OLEDB:Database Locking Mode=1", "Additional parameters should be appended.");
Assert.AreEqual("encrypted-secret", decryptor.LastValue, "Decryptor should receive encrypted value.");
}
private sealed class FakeDecryptor : IAccessPasswordDecryptor
{
private readonly string? _result;
public FakeDecryptor(string? result) => _result = result;
public string? LastValue { get; private set; }
public string? Decrypt(string? encryptedPassword)
{
LastValue = encryptedPassword;
return _result;
}
}
private sealed class FakeContext : Microsoft.EntityFrameworkCore.DbContext
{
public FakeContext(Microsoft.EntityFrameworkCore.DbContextOptions<FakeContext> options) : base(options)
{
}
}
}
@@ -0,0 +1,82 @@
using System;
using System.IO;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Sharp.Data.Access;
using Sharp.Data.Access.DependencyInjection;
using Sharp.Data.Access.Initialization;
using Sharp.Data.Access.Options;
namespace Data.Access.Test;
[TestClass]
public sealed class AccessDatabaseInitializerTests
{
[TestMethod]
public void EnsureDatabase_CopiesTemplateWhenMissing()
{
var tempRoot = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(tempRoot);
var templateDir = Path.Combine(tempRoot, "template");
Directory.CreateDirectory(templateDir);
var templatePath = Path.Combine(templateDir, "seed.accdb");
File.WriteAllText(templatePath, "seed-data");
var options = new AccessDatabaseOptions
{
DatabaseFile = "db/app.accdb",
TemplateFile = "template/seed.accdb",
DataDirectory = tempRoot,
CopyTemplateWhenMissing = true,
ThrowIfMissing = true
};
var services = BuildServices();
var initializer = services.GetRequiredService<IAccessDatabaseInitializer>();
initializer.EnsureDatabase(options, services);
var targetPath = Path.Combine(tempRoot, "db", "app.accdb");
Assert.IsTrue(File.Exists(targetPath), "Database file should be created from template.");
Assert.AreEqual("seed-data", File.ReadAllText(targetPath));
}
[TestMethod]
public void EnsureDatabase_ThrowsWhenMissingAndNoTemplate()
{
var tempRoot = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(tempRoot);
var options = new AccessDatabaseOptions
{
DatabaseFile = "missing/app.accdb",
DataDirectory = tempRoot,
CopyTemplateWhenMissing = false,
ThrowIfMissing = true
};
var services = BuildServices();
var initializer = services.GetRequiredService<IAccessDatabaseInitializer>();
Assert.ThrowsException<FileNotFoundException>(() =>
initializer.EnsureDatabase(options, services));
}
private static ServiceProvider BuildServices()
{
var services = new ServiceCollection();
services.AddAccessDataContext<FakeContext>(ctx =>
{
ctx.Database.ConnectionString = $"Provider={AccessProviderNames.AceOleDb16};Data Source=fake;";
}, registerDataContext: false);
return services.BuildServiceProvider();
}
private sealed class FakeContext : Microsoft.EntityFrameworkCore.DbContext
{
public FakeContext(Microsoft.EntityFrameworkCore.DbContextOptions<FakeContext> options) : base(options)
{
}
}
}
@@ -0,0 +1,49 @@
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Sharp.Data;
using Sharp.Data.Access;
using Sharp.Data.Access.DependencyInjection;
using Sharp.Data.Access.Options;
namespace Data.Access.Test;
[TestClass]
public sealed class AccessServiceCollectionExtensionsTests
{
[TestMethod]
public void AddAccessDataContext_RegistersFactoryAndOptions()
{
var services = new ServiceCollection();
services.AddAccessDataContext<FakeContext>(options =>
{
options.Database.ConnectionString = $"Provider={AccessProviderNames.AceOleDb16};Data Source=fake;";
options.ConfigureDataContext = (sp, builder) =>
{
builder.UseAudit(false)
.UseSoftDelete(false)
.UseAutoMigration(false)
.EnableDetailedErrors(false);
};
}, registerDataContext: false);
var provider = services.BuildServiceProvider();
var options = provider.GetRequiredService<AccessContextOptions<FakeContext>>();
Assert.AreEqual($"Provider={AccessProviderNames.AceOleDb16};Data Source=fake;", options.Database.ConnectionString);
var decryptor = provider.GetRequiredService<Sharp.Data.Access.Security.IAccessPasswordDecryptor>();
Assert.IsInstanceOfType(decryptor, typeof(Sharp.Data.Access.Security.PassthroughAccessPasswordDecryptor));
var factory = provider.GetRequiredService<IAccessDataContextFactory<FakeContext>>();
Assert.IsNotNull(factory);
}
private sealed class FakeContext : DbContext
{
public FakeContext(DbContextOptions<FakeContext> options) : base(options)
{
}
}
}
@@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0"/>
<PackageReference Include="MSTest" Version="3.6.4"/>
<PackageReference Include="EntityFrameworkCore.Jet" Version="8.0.0-preview1" />
<PackageReference Include="System.Data.OleDb" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<Using Include="Microsoft.VisualStudio.TestTools.UnitTesting"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Sharp\Data.Access\Data.Access.csproj" />
<ProjectReference Include="..\..\src\Sharp\Data\Data.csproj" />
<ProjectReference Include="..\..\src\Sharp\Common\Common.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="dbDwDmsSetup.accdb">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
@@ -0,0 +1,44 @@
using Microsoft.EntityFrameworkCore;
namespace Data.Access.Test.Entities;
/// <summary>
/// 真实 Access 数据库的 DbContext
/// </summary>
public class AccessDbContext : DbContext
{
public AccessDbContext(DbContextOptions<AccessDbContext> options) : base(options)
{
}
public DbSet<UserInfoEntity> UserInfos => Set<UserInfoEntity>();
public DbSet<UserPermissionEntity> UserPermissions => Set<UserPermissionEntity>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// 配置 UserInfo
modelBuilder.Entity<UserInfoEntity>(entity =>
{
entity.HasKey(e => e.UserName);
entity.Property(e => e.UserName).IsRequired().HasMaxLength(255);
entity.Property(e => e.Password).IsRequired().HasMaxLength(255);
});
// 配置 UserPermission
modelBuilder.Entity<UserPermissionEntity>(entity =>
{
// 复合主键
entity.HasKey(e => new { e.UserInfo_UserName, e.Permission_Code });
entity.Property(e => e.UserInfo_UserName).IsRequired().HasMaxLength(255);
entity.Property(e => e.Permission_Code).IsRequired().HasMaxLength(255);
// 配置关系
entity.HasOne(e => e.UserInfo)
.WithMany(u => u.Permissions)
.HasForeignKey(e => e.UserInfo_UserName)
.OnDelete(DeleteBehavior.Cascade);
});
}
}
@@ -0,0 +1,31 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Data.Access.Test.Entities;
/// <summary>
/// UserInfo 表的实体映射
/// </summary>
[Table("UserInfo")]
public class UserInfoEntity
{
/// <summary>
/// 用户名 (主键)
/// </summary>
[Key]
[Required]
[MaxLength(255)]
public string UserName { get; set; } = string.Empty;
/// <summary>
/// 密码
/// </summary>
[Required]
[MaxLength(255)]
public string Password { get; set; } = string.Empty;
/// <summary>
/// 用户权限 (导航属性)
/// </summary>
public ICollection<UserPermissionEntity> Permissions { get; set; } = new List<UserPermissionEntity>();
}
@@ -0,0 +1,31 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Data.Access.Test.Entities;
/// <summary>
/// UserPermission 表的实体映射
/// </summary>
[Table("UserPermission")]
public class UserPermissionEntity
{
/// <summary>
/// 用户名 (外键)
/// </summary>
[Required]
[MaxLength(255)]
public string UserInfo_UserName { get; set; } = string.Empty;
/// <summary>
/// 权限代码
/// </summary>
[Required]
[MaxLength(255)]
public string Permission_Code { get; set; } = string.Empty;
/// <summary>
/// 用户信息 (导航属性)
/// </summary>
[ForeignKey(nameof(UserInfo_UserName))]
public UserInfoEntity? UserInfo { get; set; }
}
+1
View File
@@ -0,0 +1 @@
[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)]
@@ -0,0 +1,193 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Sharp.Data;
using Sharp.Data.Access.DependencyInjection;
using Sharp.Data.Access.Extensions;
using Sharp.Data.Context;
using Data.Access.Test.Entities;
namespace Data.Access.Test;
/// <summary>
/// 使用真实 Access 数据库进行 CRUD 操作的完整测试
/// </summary>
[TestClass]
public class RealAccessDatabaseIntegrationTests
{
private const string DatabaseFile = "dbDwDmsSetup.accdb";
[TestMethod]
public async Task CanQueryUsersUsingDataContextBuilder()
{
// 方式1: 使用 DataContextBuilder (无 DI)
var builder = new DataContextBuilder<AccessDbContext>();
builder.UseAccessDatabase(options =>
{
options.DatabaseFile = DatabaseFile;
options.CopyTemplateWhenMissing = false;
options.ThrowIfMissing = true;
}, configure: jet =>
{
// 禁用自动迁移以避免冲突
});
builder.UseAutoMigration(false); // 禁用自动迁移
var dataContext = builder.Build();
// 查询所有用户
var users = await dataContext.Repository<UserInfoEntity>().GetAllAsync();
Console.WriteLine($"找到 {users.Count} 个用户:");
foreach (var user in users)
{
Console.WriteLine($" - {user.UserName}");
}
Assert.IsTrue(users.Count >= 0, "应该能够查询用户表");
}
[TestMethod]
public async Task CanQueryUsersUsingDependencyInjection()
{
// 方式2: 使用依赖注入
var services = new ServiceCollection();
services.AddAccessDataContext<AccessDbContext>(options =>
{
options.Database.DatabaseFile = DatabaseFile;
options.Database.CopyTemplateWhenMissing = false;
options.Database.ThrowIfMissing = true;
});
var provider = services.BuildServiceProvider();
var dataContext = provider.GetRequiredService<IDataContext>();
// 查询所有用户
var users = await dataContext.Repository<UserInfoEntity>().GetAllAsync();
Console.WriteLine($"\n通过 DI 找到 {users.Count} 个用户:");
foreach (var user in users)
{
Console.WriteLine($" - {user.UserName}");
}
Assert.IsTrue(users.Count >= 0, "应该能够通过 DI 查询用户表");
}
[TestMethod]
public async Task CanQueryUserPermissions()
{
var builder = new DataContextBuilder<AccessDbContext>();
builder.UseAccessDatabase(options =>
{
options.DatabaseFile = DatabaseFile;
});
var dataContext = builder.Build();
// 查询所有权限
var permissions = await dataContext.Repository<UserPermissionEntity>().GetAllAsync();
Console.WriteLine($"\n找到 {permissions.Count} 个权限记录:");
foreach (var perm in permissions.Take(10)) // 只显示前 10 条
{
Console.WriteLine($" - 用户: {perm.UserInfo_UserName}, 权限: {perm.Permission_Code}");
}
Assert.IsTrue(permissions.Count >= 0, "应该能够查询权限表");
}
[TestMethod]
public async Task CanQueryWithFilters()
{
var builder = new DataContextBuilder<AccessDbContext>();
builder.UseAccessDatabase(options =>
{
options.DatabaseFile = DatabaseFile;
});
var dataContext = builder.Build();
// 使用 LINQ 查询
var dbContext = dataContext.Context;
var adminUsers = await dbContext.Set<UserInfoEntity>()
.Where(u => u.UserName.Contains("admin") || u.UserName.Contains("Admin"))
.ToListAsync();
Console.WriteLine($"\n找到 {adminUsers.Count} 个管理员用户:");
foreach (var user in adminUsers)
{
Console.WriteLine($" - {user.UserName}");
}
}
[TestMethod]
public async Task CanQueryUserWithPermissions()
{
var builder = new DataContextBuilder<AccessDbContext>();
builder.UseAccessDatabase(options =>
{
options.DatabaseFile = DatabaseFile;
});
var dataContext = builder.Build();
var dbContext = dataContext.Context;
// 查询用户及其权限
var usersWithPermissions = await dbContext.Set<UserInfoEntity>()
.Include(u => u.Permissions)
.Take(5) // 只取前 5 个用户
.ToListAsync();
Console.WriteLine($"\n查询到 {usersWithPermissions.Count} 个用户及其权限:");
foreach (var user in usersWithPermissions)
{
Console.WriteLine($" 用户: {user.UserName}");
Console.WriteLine($" 权限数: {user.Permissions.Count}");
foreach (var perm in user.Permissions.Take(3)) // 每个用户只显示前 3 个权限
{
Console.WriteLine($" - {perm.Permission_Code}");
}
}
}
[TestMethod]
public async Task CanUseRepositoryPattern()
{
var builder = new DataContextBuilder<AccessDbContext>();
builder.UseAccessDatabase(options =>
{
options.DatabaseFile = DatabaseFile;
});
var dataContext = builder.Build();
// 使用 Repository 模式
var userRepo = dataContext.Repository<UserInfoEntity>();
// 获取总数
var totalUsers = await userRepo.CountAsync();
Console.WriteLine($"\n用户总数: {totalUsers}");
// 分页查询
var pagedResult = await userRepo.GetPagedAsync(1, 10);
Console.WriteLine($"第一页 (10条/页): {pagedResult.Items.Count} 条记录");
// 查询单个用户
if (totalUsers > 0)
{
var firstUser = await userRepo.FirstOrDefaultAsync(u => true);
if (firstUser != null)
{
Console.WriteLine($"第一个用户: {firstUser.UserName}");
}
}
Assert.IsTrue(totalUsers >= 0, "应该能够使用 Repository 模式");
}
}
+134
View File
@@ -0,0 +1,134 @@
using System;
using System.Data.OleDb;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Sharp.Data;
using Sharp.Data.Access.Extensions;
using Sharp.Data.Context;
namespace Data.Access.Test;
/// <summary>
/// 使用真实 Access 数据库的集成测试
/// </summary>
[TestClass]
public class RealDatabaseTests
{
private const string DatabaseFile = "dbDwDmsSetup.accdb";
[TestMethod]
public void CanReadDatabaseSchema()
{
// 读取数据库表结构
var connectionString = $"Provider=Microsoft.ACE.OLEDB.12.0;Data Source={DatabaseFile}";
using var connection = new OleDbConnection(connectionString);
connection.Open();
// 获取所有表
var tables = connection.GetSchema("Tables");
Assert.IsTrue(tables.Rows.Count > 0, "数据库应该包含表");
Console.WriteLine($"数据库包含 {tables.Rows.Count} 个对象:");
foreach (System.Data.DataRow row in tables.Rows)
{
var tableName = row["TABLE_NAME"]?.ToString();
var tableType = row["TABLE_TYPE"]?.ToString();
if (tableType == "TABLE")
{
Console.WriteLine($" - {tableName}");
}
}
}
[TestMethod]
public void CanListAllTables()
{
var connectionString = $"Provider=Microsoft.ACE.OLEDB.12.0;Data Source={DatabaseFile}";
using var connection = new OleDbConnection(connectionString);
connection.Open();
var restrictions = new string?[4];
restrictions[3] = "TABLE"; // 只获取表,不包括视图和系统表
var tables = connection.GetSchema("Tables", restrictions);
Console.WriteLine("\n=== 用户表列表 ===");
foreach (System.Data.DataRow row in tables.Rows)
{
var tableName = row["TABLE_NAME"]?.ToString();
if (!string.IsNullOrEmpty(tableName) && !tableName.StartsWith("MSys"))
{
Console.WriteLine($"表名: {tableName}");
// 获取表的列信息
var columnRestrictions = new string?[4];
columnRestrictions[2] = tableName;
var columns = connection.GetSchema("Columns", columnRestrictions);
Console.WriteLine($" 列数: {columns.Rows.Count}");
foreach (System.Data.DataRow colRow in columns.Rows)
{
var columnName = colRow["COLUMN_NAME"]?.ToString();
var dataType = colRow["DATA_TYPE"]?.ToString();
Console.WriteLine($" - {columnName} ({GetDataTypeName(dataType)})");
}
Console.WriteLine();
}
}
}
[TestMethod]
public async Task CanQueryDatabaseUsingDataContextBuilder()
{
var builder = new DataContextBuilder<TestDbContext>();
builder.UseAccessDatabase(options =>
{
options.DatabaseFile = DatabaseFile;
options.CopyTemplateWhenMissing = false;
options.ThrowIfMissing = true;
});
var dataContext = builder.Build();
// 尝试连接数据库
var dbContext = dataContext.Context;
var canConnect = await dbContext.Database.CanConnectAsync();
Assert.IsTrue(canConnect, "应该能够连接到数据库");
Console.WriteLine("成功使用 DataContextBuilder 连接到 Access 数据库!");
}
private static string GetDataTypeName(string? dataType)
{
return dataType switch
{
"130" => "nvarchar",
"3" => "int",
"7" => "datetime",
"11" => "bit",
"5" => "double",
"4" => "float",
"2" => "smallint",
"6" => "currency",
"128" => "binary",
"205" => "image",
_ => $"unknown({dataType})"
};
}
}
/// <summary>
/// 测试用的 DbContext
/// </summary>
public class TestDbContext : DbContext
{
public TestDbContext(DbContextOptions<TestDbContext> options) : base(options)
{
}
}
@@ -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.