A:Access数据库功能

This commit is contained in:
jimmy committed 2025-11-19 08:42:28 +08:00
1 parent f7660c49e8
commit 1d412bba33
37 files changed
+990 -23

No files matched your search

@@ -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
@@ -0,0 +1,56 @@
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。");
}
var builder = new OleDbConnectionStringBuilder
{
["Provider"] = configuration.ProviderName,
["Data Source"] = AccessPathResolver.Resolve(configuration.DatabaseFile, configuration.DataDirectory)
};
var password = configuration.Password;
if (string.IsNullOrEmpty(password) && !string.IsNullOrWhiteSpace(configuration.EncryptedPassword))
{
password = _decryptor.Decrypt(configuration.EncryptedPassword);
}
if (!string.IsNullOrEmpty(password))
{
builder["Jet OLEDB:Database Password"] = password;
}
foreach (var parameter in configuration.AdditionalParameters)
{
builder[parameter.Key] = parameter.Value;
}
return builder.ConnectionString;
}
}
@@ -0,0 +1,12 @@
using System;
using Sharp.Data.Access.Options;
namespace Sharp.Data.Access.Connections;
/// <summary>
/// 构建 Access 连接字符串的工厂。
/// </summary>
public interface IAccessConnectionStringFactory
{
string Create(AccessDatabaseOptions options, IServiceProvider services);
}
@@ -0,0 +1,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,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,55 @@
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,
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,
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,109 @@
using System;
using System.Collections.Generic;
namespace Sharp.Data.Access.Options;
/// <summary>
/// Access 数据库配置项,调用方负责提供所有需要的参数。
/// </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>
/// 明文密码,可选。
/// </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; }
/// <summary>
/// Provider 名称,默认为 ACE OLE DB 16。
/// </summary>
public string ProviderName { get; set; } = AccessProviderNames.AceOleDb16;
/// <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。");
}
}
}
@@ -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";
}
+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;
}
+1 -1
View File
@@ -74,7 +74,7 @@ namespace Server
options.Enrich.Application ??= "SharpServer";
});
}
private void InitializeDatabase()
{
if (LoggerFactory == null)
+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
@@ -14,7 +14,7 @@ using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("Sharp.Wpf")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+53d1dd2dbf05d818528814223b58610a790fcb26")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+f7660c49e81d5fe2747f6514545ceb961c2066d3")]
[assembly: System.Reflection.AssemblyProductAttribute("Sharp.Wpf")]
[assembly: System.Reflection.AssemblyTitleAttribute("Sharp.Wpf")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
@@ -1 +1 @@
fbdba11393faed768ef4c9b8f4eccef99aed87397a79576b900953e4aad05633
7ce8133edb934c064ba59400ce54843707360487a12d558ab17d6af09c0febb5
@@ -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,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.