A:Access数据库功能
This commit is contained in:
1 parent
f7660c49e8
commit
1d412bba33
37 files changed
+990
-23
No files matched your search
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,9 +1,10 @@
|
|||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// <auto-generated>
|
// <auto-generated>
|
||||||
// This code was generated by a tool.
|
// 此代码由工具生成。
|
||||||
|
// 运行时版本:4.0.30319.42000
|
||||||
//
|
//
|
||||||
// Changes to this file may cause incorrect behavior and will be lost if
|
// 对此文件的更改可能会导致不正确的行为,并且如果
|
||||||
// the code is regenerated.
|
// 重新生成代码,这些更改将会丢失。
|
||||||
// </auto-generated>
|
// </auto-generated>
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
@@ -13,7 +14,7 @@ using System.Reflection;
|
|||||||
[assembly: System.Reflection.AssemblyCompanyAttribute("Client")]
|
[assembly: System.Reflection.AssemblyCompanyAttribute("Client")]
|
||||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+53d1dd2dbf05d818528814223b58610a790fcb26")]
|
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+f7660c49e81d5fe2747f6514545ceb961c2066d3")]
|
||||||
[assembly: System.Reflection.AssemblyProductAttribute("Client")]
|
[assembly: System.Reflection.AssemblyProductAttribute("Client")]
|
||||||
[assembly: System.Reflection.AssemblyTitleAttribute("Client")]
|
[assembly: System.Reflection.AssemblyTitleAttribute("Client")]
|
||||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
f8321da7c6a777fc7770c7234548af806317c95cd5f1beb1c1743888aa1bde11
|
eeea53cf7db257ee716efdd2fdcdb5f1a810b49927811d410a05e1ff19899369
|
||||||
@@ -0,0 +1,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";
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -74,7 +74,7 @@ namespace Server
|
|||||||
options.Enrich.Application ??= "SharpServer";
|
options.Enrich.Application ??= "SharpServer";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InitializeDatabase()
|
private void InitializeDatabase()
|
||||||
{
|
{
|
||||||
if (LoggerFactory == null)
|
if (LoggerFactory == null)
|
||||||
|
|||||||
@@ -39,6 +39,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Data", "Data\Data.csproj",
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Data.Test", "..\..\test\Data.Test\Data.Test.csproj", "{8EEC8BF4-CF9A-473D-B7A0-005E151C645D}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Data.Test", "..\..\test\Data.Test\Data.Test.csproj", "{8EEC8BF4-CF9A-473D-B7A0-005E151C645D}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Data.Access", "Data.Access\Data.Access.csproj", "{944051DB-AAB1-4502-9AF6-B496EA8FD737}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Data.Access.Test", "..\..\test\Data.Access.Test\Data.Access.Test.csproj", "{48A70F46-8764-4FEA-BAFA-42776B0B526F}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -117,6 +121,14 @@ Global
|
|||||||
{8EEC8BF4-CF9A-473D-B7A0-005E151C645D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{8EEC8BF4-CF9A-473D-B7A0-005E151C645D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{8EEC8BF4-CF9A-473D-B7A0-005E151C645D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{8EEC8BF4-CF9A-473D-B7A0-005E151C645D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{8EEC8BF4-CF9A-473D-B7A0-005E151C645D}.Release|Any CPU.Build.0 = Release|Any CPU
|
{8EEC8BF4-CF9A-473D-B7A0-005E151C645D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{944051DB-AAB1-4502-9AF6-B496EA8FD737}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{944051DB-AAB1-4502-9AF6-B496EA8FD737}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{944051DB-AAB1-4502-9AF6-B496EA8FD737}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{944051DB-AAB1-4502-9AF6-B496EA8FD737}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{48A70F46-8764-4FEA-BAFA-42776B0B526F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{48A70F46-8764-4FEA-BAFA-42776B0B526F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{48A70F46-8764-4FEA-BAFA-42776B0B526F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{48A70F46-8764-4FEA-BAFA-42776B0B526F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ using System.Reflection;
|
|||||||
[assembly: System.Reflection.AssemblyCompanyAttribute("Sharp.Wpf")]
|
[assembly: System.Reflection.AssemblyCompanyAttribute("Sharp.Wpf")]
|
||||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+53d1dd2dbf05d818528814223b58610a790fcb26")]
|
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+f7660c49e81d5fe2747f6514545ceb961c2066d3")]
|
||||||
[assembly: System.Reflection.AssemblyProductAttribute("Sharp.Wpf")]
|
[assembly: System.Reflection.AssemblyProductAttribute("Sharp.Wpf")]
|
||||||
[assembly: System.Reflection.AssemblyTitleAttribute("Sharp.Wpf")]
|
[assembly: System.Reflection.AssemblyTitleAttribute("Sharp.Wpf")]
|
||||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
fbdba11393faed768ef4c9b8f4eccef99aed87397a79576b900953e4aad05633
|
7ce8133edb934c064ba59400ce54843707360487a12d558ab17d6af09c0febb5
|
||||||
@@ -70,11 +70,6 @@
|
|||||||
},
|
},
|
||||||
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.203/PortableRuntimeIdentifierGraph.json"
|
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.203/PortableRuntimeIdentifierGraph.json"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"runtimes": {
|
|
||||||
"win-x64": {
|
|
||||||
"#import": []
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"targets": {
|
"targets": {
|
||||||
"net8.0-windows7.0": {},
|
"net8.0-windows7.0": {}
|
||||||
"net8.0-windows7.0/win-x64": {}
|
|
||||||
},
|
},
|
||||||
"libraries": {},
|
"libraries": {},
|
||||||
"projectFileDependencyGroups": {
|
"projectFileDependencyGroups": {
|
||||||
@@ -78,11 +77,6 @@
|
|||||||
},
|
},
|
||||||
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.203/PortableRuntimeIdentifierGraph.json"
|
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.203/PortableRuntimeIdentifierGraph.json"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"runtimes": {
|
|
||||||
"win-x64": {
|
|
||||||
"#import": []
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"dgSpecHash": "9ZityDkLrvk=",
|
"dgSpecHash": "EQ+I1wXrehg=",
|
||||||
"success": true,
|
"success": true,
|
||||||
"projectFilePath": "D:\\yuanll\\sharp-basic\\src\\Sharp\\Wpf\\Wpf.csproj",
|
"projectFilePath": "D:\\yuanll\\sharp-basic\\src\\Sharp\\Wpf\\Wpf.csproj",
|
||||||
"expectedPackageFiles": [],
|
"expectedPackageFiles": [],
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
17634268840015055
|
17634526945569668
|
||||||
@@ -1 +1 @@
|
|||||||
17633637013370903
|
17634577083448171
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||||
|
using Sharp.Data.Access;
|
||||||
|
using Sharp.Data.Access.DependencyInjection;
|
||||||
|
using Sharp.Data.Access.Options;
|
||||||
|
using Sharp.Data.Access.Security;
|
||||||
|
using Sharp.Data.Access.Connections;
|
||||||
|
|
||||||
|
namespace Data.Access.Test;
|
||||||
|
|
||||||
|
[TestClass]
|
||||||
|
public sealed class AccessConnectionStringFactoryTests
|
||||||
|
{
|
||||||
|
[TestMethod]
|
||||||
|
public void Create_UsesDecryptedPasswordAndAdditionalParameters()
|
||||||
|
{
|
||||||
|
var tempDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
|
||||||
|
Directory.CreateDirectory(tempDirectory);
|
||||||
|
|
||||||
|
var options = new AccessDatabaseOptions
|
||||||
|
{
|
||||||
|
DatabaseFile = "data/app.accdb",
|
||||||
|
DataDirectory = tempDirectory,
|
||||||
|
EncryptedPassword = "encrypted-secret",
|
||||||
|
ProviderName = AccessProviderNames.AceOleDb12
|
||||||
|
};
|
||||||
|
options.AdditionalParameters["Jet OLEDB:Database Locking Mode"] = "1";
|
||||||
|
|
||||||
|
var decryptor = new FakeDecryptor("plain-text");
|
||||||
|
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
services.AddSingleton<IAccessPasswordDecryptor>(decryptor);
|
||||||
|
services.AddAccessDataContext<FakeContext>(ctx =>
|
||||||
|
{
|
||||||
|
ctx.Database.ConnectionString = $"Provider={AccessProviderNames.AceOleDb16};Data Source=fake;";
|
||||||
|
}, registerDataContext: false);
|
||||||
|
|
||||||
|
var provider = services.BuildServiceProvider();
|
||||||
|
var factory = provider.GetRequiredService<IAccessConnectionStringFactory>();
|
||||||
|
|
||||||
|
var connectionString = factory.Create(options, provider);
|
||||||
|
var expectedPath = Path.GetFullPath(Path.Combine(tempDirectory, "data", "app.accdb"));
|
||||||
|
|
||||||
|
StringAssert.Contains(connectionString, $"Provider={AccessProviderNames.AceOleDb12}", "Provider should be set.");
|
||||||
|
StringAssert.Contains(connectionString, expectedPath, "Database path should be resolved to absolute path.");
|
||||||
|
StringAssert.Contains(connectionString, "Jet OLEDB:Database Password=plain-text", "Decrypted password should be used.");
|
||||||
|
StringAssert.Contains(connectionString, "Jet OLEDB:Database Locking Mode=1", "Additional parameters should be appended.");
|
||||||
|
Assert.AreEqual("encrypted-secret", decryptor.LastValue, "Decryptor should receive encrypted value.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class FakeDecryptor : IAccessPasswordDecryptor
|
||||||
|
{
|
||||||
|
private readonly string? _result;
|
||||||
|
|
||||||
|
public FakeDecryptor(string? result) => _result = result;
|
||||||
|
|
||||||
|
public string? LastValue { get; private set; }
|
||||||
|
|
||||||
|
public string? Decrypt(string? encryptedPassword)
|
||||||
|
{
|
||||||
|
LastValue = encryptedPassword;
|
||||||
|
return _result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class FakeContext : Microsoft.EntityFrameworkCore.DbContext
|
||||||
|
{
|
||||||
|
public FakeContext(Microsoft.EntityFrameworkCore.DbContextOptions<FakeContext> options) : base(options)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||||
|
using Sharp.Data.Access;
|
||||||
|
using Sharp.Data.Access.DependencyInjection;
|
||||||
|
using Sharp.Data.Access.Initialization;
|
||||||
|
using Sharp.Data.Access.Options;
|
||||||
|
|
||||||
|
namespace Data.Access.Test;
|
||||||
|
|
||||||
|
[TestClass]
|
||||||
|
public sealed class AccessDatabaseInitializerTests
|
||||||
|
{
|
||||||
|
[TestMethod]
|
||||||
|
public void EnsureDatabase_CopiesTemplateWhenMissing()
|
||||||
|
{
|
||||||
|
var tempRoot = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
|
||||||
|
Directory.CreateDirectory(tempRoot);
|
||||||
|
|
||||||
|
var templateDir = Path.Combine(tempRoot, "template");
|
||||||
|
Directory.CreateDirectory(templateDir);
|
||||||
|
|
||||||
|
var templatePath = Path.Combine(templateDir, "seed.accdb");
|
||||||
|
File.WriteAllText(templatePath, "seed-data");
|
||||||
|
|
||||||
|
var options = new AccessDatabaseOptions
|
||||||
|
{
|
||||||
|
DatabaseFile = "db/app.accdb",
|
||||||
|
TemplateFile = "template/seed.accdb",
|
||||||
|
DataDirectory = tempRoot,
|
||||||
|
CopyTemplateWhenMissing = true,
|
||||||
|
ThrowIfMissing = true
|
||||||
|
};
|
||||||
|
|
||||||
|
var services = BuildServices();
|
||||||
|
var initializer = services.GetRequiredService<IAccessDatabaseInitializer>();
|
||||||
|
initializer.EnsureDatabase(options, services);
|
||||||
|
|
||||||
|
var targetPath = Path.Combine(tempRoot, "db", "app.accdb");
|
||||||
|
Assert.IsTrue(File.Exists(targetPath), "Database file should be created from template.");
|
||||||
|
Assert.AreEqual("seed-data", File.ReadAllText(targetPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void EnsureDatabase_ThrowsWhenMissingAndNoTemplate()
|
||||||
|
{
|
||||||
|
var tempRoot = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
|
||||||
|
Directory.CreateDirectory(tempRoot);
|
||||||
|
|
||||||
|
var options = new AccessDatabaseOptions
|
||||||
|
{
|
||||||
|
DatabaseFile = "missing/app.accdb",
|
||||||
|
DataDirectory = tempRoot,
|
||||||
|
CopyTemplateWhenMissing = false,
|
||||||
|
ThrowIfMissing = true
|
||||||
|
};
|
||||||
|
|
||||||
|
var services = BuildServices();
|
||||||
|
var initializer = services.GetRequiredService<IAccessDatabaseInitializer>();
|
||||||
|
Assert.ThrowsException<FileNotFoundException>(() =>
|
||||||
|
initializer.EnsureDatabase(options, services));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ServiceProvider BuildServices()
|
||||||
|
{
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
services.AddAccessDataContext<FakeContext>(ctx =>
|
||||||
|
{
|
||||||
|
ctx.Database.ConnectionString = $"Provider={AccessProviderNames.AceOleDb16};Data Source=fake;";
|
||||||
|
}, registerDataContext: false);
|
||||||
|
|
||||||
|
return services.BuildServiceProvider();
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class FakeContext : Microsoft.EntityFrameworkCore.DbContext
|
||||||
|
{
|
||||||
|
public FakeContext(Microsoft.EntityFrameworkCore.DbContextOptions<FakeContext> options) : base(options)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||||
|
using Sharp.Data;
|
||||||
|
using Sharp.Data.Access;
|
||||||
|
using Sharp.Data.Access.DependencyInjection;
|
||||||
|
using Sharp.Data.Access.Options;
|
||||||
|
|
||||||
|
namespace Data.Access.Test;
|
||||||
|
|
||||||
|
[TestClass]
|
||||||
|
public sealed class AccessServiceCollectionExtensionsTests
|
||||||
|
{
|
||||||
|
[TestMethod]
|
||||||
|
public void AddAccessDataContext_RegistersFactoryAndOptions()
|
||||||
|
{
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
services.AddAccessDataContext<FakeContext>(options =>
|
||||||
|
{
|
||||||
|
options.Database.ConnectionString = $"Provider={AccessProviderNames.AceOleDb16};Data Source=fake;";
|
||||||
|
options.ConfigureDataContext = (sp, builder) =>
|
||||||
|
{
|
||||||
|
builder.UseAudit(false)
|
||||||
|
.UseSoftDelete(false)
|
||||||
|
.UseAutoMigration(false)
|
||||||
|
.EnableDetailedErrors(false);
|
||||||
|
};
|
||||||
|
}, registerDataContext: false);
|
||||||
|
|
||||||
|
var provider = services.BuildServiceProvider();
|
||||||
|
|
||||||
|
var options = provider.GetRequiredService<AccessContextOptions<FakeContext>>();
|
||||||
|
Assert.AreEqual($"Provider={AccessProviderNames.AceOleDb16};Data Source=fake;", options.Database.ConnectionString);
|
||||||
|
|
||||||
|
var decryptor = provider.GetRequiredService<Sharp.Data.Access.Security.IAccessPasswordDecryptor>();
|
||||||
|
Assert.IsInstanceOfType(decryptor, typeof(Sharp.Data.Access.Security.PassthroughAccessPasswordDecryptor));
|
||||||
|
|
||||||
|
var factory = provider.GetRequiredService<IAccessDataContextFactory<FakeContext>>();
|
||||||
|
Assert.IsNotNull(factory);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class FakeContext : DbContext
|
||||||
|
{
|
||||||
|
public FakeContext(DbContextOptions<FakeContext> options) : base(options)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||||
|
using Sharp.Data;
|
||||||
|
using Sharp.Data.Access.Extensions;
|
||||||
|
using Sharp.Data.Context;
|
||||||
|
|
||||||
|
namespace Data.Access.Test.Usage;
|
||||||
|
|
||||||
|
[TestClass]
|
||||||
|
public sealed class AccessUsageSampleTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 演示如何在无 DI 场景下使用 DataContextBuilder 配置 Access。
|
||||||
|
/// 这里只验证 API 能够被调用(不真正连接数据库)。
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void ManualBuilder_AllowsAccessConfigurationWithoutExceptions()
|
||||||
|
{
|
||||||
|
var builder = new DataContextBuilder<FakeContext>();
|
||||||
|
|
||||||
|
builder.UseAccessDatabase(options =>
|
||||||
|
{
|
||||||
|
options.ConnectionString = $"Provider=Microsoft.ACE.OLEDB.16.0;Data Source=fake.accdb;";
|
||||||
|
options.CopyTemplateWhenMissing = false;
|
||||||
|
options.ThrowIfMissing = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 不调用 Build(),避免实际访问文件;只要不抛出异常即为通过。
|
||||||
|
Assert.IsTrue(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class FakeContext : DbContext
|
||||||
|
{
|
||||||
|
public FakeContext(DbContextOptions<FakeContext> options) : base(options)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
Reference in new issue
Block a user