Compare commits
28
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b89b2d722 | ||
|
|
2bd35348dc | ||
|
|
b738a2b166 | ||
|
|
5e9e9fa818 | ||
|
|
8ca0679fca | ||
|
|
1b5d57bd19 | ||
|
|
f610bf5e34 | ||
|
|
54545aad35 | ||
|
|
012c9522f8 | ||
|
|
99c2c59552 | ||
|
|
974d16833c | ||
|
|
2943485a2a | ||
|
|
30451cabbd | ||
|
|
37aa848b5f | ||
|
|
2563288afe | ||
|
|
2c83fc72e4 | ||
|
|
dd0681ba90 | ||
|
|
4ec76b7855 | ||
|
|
433e206d45 | ||
|
|
26a2661625 | ||
|
|
5b7f127bb5 | ||
|
|
ea6103a596 | ||
|
|
de1a29c522 | ||
|
|
6ed0d3f1c2 | ||
|
|
d9e5a5483c | ||
|
|
bc09280d4e | ||
|
|
2f3eb935dd | ||
|
|
bca001b90f |
No files matched your search
+29
@@ -0,0 +1,29 @@
|
|||||||
|
# Visual Studio
|
||||||
|
.vs/
|
||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
[Dd]ebug/
|
||||||
|
[Rr]elease/
|
||||||
|
*.user
|
||||||
|
*.suo
|
||||||
|
*.userprefs
|
||||||
|
*.usertasks
|
||||||
|
*.sln.docstates
|
||||||
|
*.dll
|
||||||
|
*.pdb
|
||||||
|
*.exe
|
||||||
|
!src/**/bin/**/*.dll
|
||||||
|
!src/**/bin/**/*.pdb
|
||||||
|
!src/**/bin/**/*.exe
|
||||||
|
|
||||||
|
# Rider
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# Dotnet
|
||||||
|
project.lock.json
|
||||||
|
project.fragment.lock.json
|
||||||
|
artifacts/
|
||||||
|
|
||||||
|
# Other
|
||||||
|
Thumbs.db
|
||||||
|
Desktop.ini
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Sharp-Basic Framework 贡献者
|
||||||
|
|
||||||
|
## 主要作者
|
||||||
|
|
||||||
|
**duyl328**
|
||||||
|
- 邮箱:通过 GitHub 联系
|
||||||
|
- 网站:https://seyl.me/
|
||||||
|
- 角色:项目创始人和主要维护者
|
||||||
|
|
||||||
|
## 贡献者
|
||||||
|
|
||||||
|
感谢所有为 Sharp-Basic 做出贡献的开发者!
|
||||||
|
|
||||||
|
如果您想为项目做出贡献,请查看 CONTRIBUTING.md 文件。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
本文件列出了对 Sharp-Basic Framework 有重大贡献的个人和组织。
|
||||||
|
如果您认为您的名字应该出现在这里,请提交 Pull Request。
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# 更新日志
|
||||||
|
|
||||||
|
本文档记录 Sharp-Basic Framework 的所有重要更改。
|
||||||
|
|
||||||
|
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/),
|
||||||
|
版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
|
||||||
|
|
||||||
|
## [未发布]
|
||||||
|
|
||||||
|
### 计划中
|
||||||
|
- Cache 模块完整实现
|
||||||
|
- EventBus 模块完整实现
|
||||||
|
- 性能监控和诊断工具
|
||||||
|
|
||||||
|
## [1.0.1] - 2025-01-XX
|
||||||
|
|
||||||
|
### 新增
|
||||||
|
- 完整的项目元数据和版权信息
|
||||||
|
- AUTHORS 文件列出贡献者
|
||||||
|
- CONTRIBUTING.md 贡献指南
|
||||||
|
- NOTICE 文件包含第三方依赖声明
|
||||||
|
- CHANGELOG.md 版本变更记录
|
||||||
|
- Directory.Build.props 统一管理项目元信息
|
||||||
|
|
||||||
|
### 变更
|
||||||
|
- 更新许可证为 AGPL-3.0-or-later
|
||||||
|
- 完善 README.md,添加项目徽章和详细信息
|
||||||
|
- 统一版权声明为 Copyright © 2024-2025 duyl328
|
||||||
|
|
||||||
|
### 文档
|
||||||
|
- 更新 CLAUDE.md 项目指南
|
||||||
|
- 添加快速开始指南
|
||||||
|
|
||||||
|
## [1.0.0] - 2024-XX-XX
|
||||||
|
|
||||||
|
### 新增
|
||||||
|
- ✨ 初始版本发布
|
||||||
|
- 核心模块实现:
|
||||||
|
- **Sharp.Common**: 基础工具库(Result<T>、Global DI 容器、扩展方法)
|
||||||
|
- **Sharp.Config**: 多格式配置管理(JSON/XML/INI/TOML)
|
||||||
|
- **Sharp.Data**: EF Core 数据访问层(仓储模式、审计、软删除)
|
||||||
|
- **Sharp.Log**: Serilog 日志集成
|
||||||
|
- **Sharp.Wpf**: WPF MVVM 基础库
|
||||||
|
- **Sharp.Wpf.HandyControl**: HandyControl UI 库集成
|
||||||
|
- **Sharp.Wpf.DevTools**: 开发者工具(DevTools 窗口、日志查看器)
|
||||||
|
- **Sharp.NetClient**: HTTP 客户端(Polly 重试)
|
||||||
|
- **Sharp.NetServer**: 轻量级 Web 服务器
|
||||||
|
- **Sharp.Ports**: 串口通信库
|
||||||
|
- **Sharp.Plc**: PLC 通信抽象层
|
||||||
|
- **Sharp.Plc.Siemens**: 西门子 S7 PLC 驱动
|
||||||
|
- **Sharp.Process**: 进程执行管理
|
||||||
|
- **Sharp.Python**: Python 脚本执行
|
||||||
|
- 📦 完整的单元测试覆盖
|
||||||
|
- 📚 各模块 README 文档
|
||||||
|
- 🎯 Sharp.Demo 综合示例应用
|
||||||
|
|
||||||
|
### 架构
|
||||||
|
- 模块化设计,引入即用
|
||||||
|
- 约定优于配置的理念
|
||||||
|
- 固执己见的技术栈(EF Core, Serilog, HandyControl)
|
||||||
|
- Result<T> 函数式错误处理
|
||||||
|
- 混合 DI 方式(标准 DI + Global 容器)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 版本说明
|
||||||
|
|
||||||
|
### 版本号格式: MAJOR.MINOR.PATCH
|
||||||
|
|
||||||
|
- **MAJOR**: 不兼容的 API 变更
|
||||||
|
- **MINOR**: 向后兼容的功能新增
|
||||||
|
- **PATCH**: 向后兼容的问题修正
|
||||||
|
|
||||||
|
### 变更类型
|
||||||
|
|
||||||
|
- `新增`: 新功能
|
||||||
|
- `变更`: 现有功能的变更
|
||||||
|
- `弃用`: 即将移除的功能
|
||||||
|
- `移除`: 已移除的功能
|
||||||
|
- `修复`: Bug 修复
|
||||||
|
- `安全`: 安全相关的修复
|
||||||
|
- `文档`: 文档相关变更
|
||||||
|
- `性能`: 性能改进
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[未发布]: https://gitea.duyl328.org/jimmy/sharp-basic/compare/v1.0.1...HEAD
|
||||||
|
[1.0.1]: https://gitea.duyl328.org/jimmy/sharp-basic/compare/v1.0.0...v1.0.1
|
||||||
|
[1.0.0]: https://gitea.duyl328.org/jimmy/sharp-basic/releases/tag/v1.0.0
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
Sharp-Basic is a modular C# .NET 8 framework for building enterprise applications with reusable components. It provides a comprehensive toolkit with emphasis on:
|
||||||
|
- **Modularity**: Independent, reusable libraries that can be used selectively
|
||||||
|
- **Desktop Applications**: WPF-based UI toolkit with MVVM infrastructure
|
||||||
|
- **Industrial IoT**: PLC integration (Siemens) and serial port communication
|
||||||
|
- **Configuration as Code**: Multi-format (JSON/XML/INI/TOML) with hot-reload
|
||||||
|
- **Functional Programming**: Result<T> pattern for railway-oriented error handling
|
||||||
|
|
||||||
|
**Language**: All comments and documentation are in Chinese (中文).
|
||||||
|
|
||||||
|
## Build Commands
|
||||||
|
|
||||||
|
### Build the entire solution
|
||||||
|
```bash
|
||||||
|
cd src/Sharp
|
||||||
|
dotnet build Sharp.sln
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build specific project
|
||||||
|
```bash
|
||||||
|
cd src/Sharp
|
||||||
|
dotnet build Sharp.Demo/Sharp.Demo.csproj # WPF demo application (最佳实践示例)
|
||||||
|
dotnet build Common/Common.csproj # Core utilities
|
||||||
|
dotnet build Config/Config.csproj # Configuration management
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run tests
|
||||||
|
```bash
|
||||||
|
cd src/Sharp
|
||||||
|
dotnet test Sharp.sln # All tests
|
||||||
|
dotnet test ../../test/Common.Test # Specific test project
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run the Sharp.Demo application
|
||||||
|
```bash
|
||||||
|
cd src/Sharp
|
||||||
|
dotnet run --project Sharp.Demo/Sharp.Demo.csproj
|
||||||
|
```
|
||||||
|
|
||||||
|
### Clean build artifacts
|
||||||
|
```bash
|
||||||
|
cd src/Sharp
|
||||||
|
dotnet clean Sharp.sln
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
### Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/Sharp/ # All source projects
|
||||||
|
├── Common/ # Core foundation (Result<T>, Global container, utilities, SystemConfig)
|
||||||
|
├── Config/ # Multi-format configuration with hot-reload
|
||||||
|
├── Wpf/ # WPF UI toolkit (MVVM, dialogs, converters) - NO UI library dependency
|
||||||
|
├── Wpf.HandyControl/ # HandyControl UI library integration (optional)
|
||||||
|
├── Wpf.DevTools/ # WPF Developer Tools (DevTools, LogViewer, Performance Monitor)
|
||||||
|
├── Data/ # EF Core wrapper with repository pattern
|
||||||
|
├── Log/ # Serilog-based structured logging
|
||||||
|
├── Cache/ # Memory cache (planned)
|
||||||
|
├── EventBus/ # Event pub/sub (planned)
|
||||||
|
├── NetClient/ # HTTP client with Polly resilience
|
||||||
|
├── NetServer/ # Lightweight HTTP server with middleware
|
||||||
|
├── Ports/ # Serial port abstraction (RS232/RS485)
|
||||||
|
├── Plc/ # PLC abstraction interface
|
||||||
|
├── Plc.Siemens/ # Siemens S7 PLC implementation (S7.NET+)
|
||||||
|
├── Process/ # Process execution
|
||||||
|
├── Python/ # Python script execution
|
||||||
|
└── Sharp.Demo/ # 综合演示应用(最佳实践示例,替代了 Client 和 Server)
|
||||||
|
|
||||||
|
test/ # MSTest test projects
|
||||||
|
├── Common.Test/
|
||||||
|
├── Config.Test/
|
||||||
|
├── Data.Test/
|
||||||
|
├── Log.Test/
|
||||||
|
├── NetClient.Test/
|
||||||
|
└── NetServer.Test/
|
||||||
|
|
||||||
|
appsettings.template.json # 完整的配置模板文件
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Architectural Patterns
|
||||||
|
|
||||||
|
#### 1. Result<T> Pattern (Railway-Oriented Programming)
|
||||||
|
- Eliminates exceptions as control flow
|
||||||
|
- Provides chainable operations: `Map`, `Bind`, `Match`, `Where`, `OnSuccess`, `OnFailure`
|
||||||
|
- Synchronous and async wrappers: `Result.Try()`, `Result.TryAsync()`
|
||||||
|
- Example usage:
|
||||||
|
```csharp
|
||||||
|
Result.Try(() => int.Parse(input))
|
||||||
|
.Map(x => x * 2)
|
||||||
|
.Where(v => v > 100, "Value too small")
|
||||||
|
.OnSuccess(v => Console.WriteLine(v))
|
||||||
|
.OnFailure((ex, msg) => logger.LogError(msg));
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Dependency Injection (Hybrid Approach)
|
||||||
|
- **Primary**: Microsoft.Extensions.DependencyInjection for standard DI
|
||||||
|
- **Fallback**: Global service locator (Common.Global) for legacy/convenience
|
||||||
|
- Services registered in `DependencyInjection` folders within each module
|
||||||
|
- Client app startup (App.xaml.cs) orchestrates all service registration
|
||||||
|
|
||||||
|
#### 3. Configuration Management
|
||||||
|
- Multi-source configuration builder supporting JSON/XML/INI/TOML
|
||||||
|
- Hot-reload with change notifications
|
||||||
|
- Strong typing with DataAnnotations validation
|
||||||
|
- IOptions<T> pattern integration
|
||||||
|
- Environment variable overrides with optional prefix
|
||||||
|
|
||||||
|
#### 4. Repository Pattern with EF Core
|
||||||
|
- Generic `IRepository<TEntity>` abstraction
|
||||||
|
- Built-in entity types: `Entity`, `AuditableEntity`, `ISoftDeletable`
|
||||||
|
- Auto-audit tracking (CreatedAt, CreatedBy, UpdatedAt, UpdatedBy)
|
||||||
|
- Multi-database support: SQL Server, SQLite, PostgreSQL, MySQL, In-Memory
|
||||||
|
|
||||||
|
#### 5. MVVM for WPF
|
||||||
|
- `ViewModelBase` with INotifyPropertyChanged and IsBusy tracking
|
||||||
|
- `RelayCommand` and `AsyncCommand` implementations
|
||||||
|
- Services: `IDialogService`, `IFileDialogService`, `IDispatcherService`
|
||||||
|
- Converters: BoolToVisibility, InverseBooleanConverter, EnumToDescriptionConverter
|
||||||
|
- Global exception handling for UI/Task/AppDomain exceptions
|
||||||
|
|
||||||
|
#### 6. Middleware Pattern
|
||||||
|
- Custom HTTP server (NetServer) with ASP.NET Core-style pipeline
|
||||||
|
- Middleware: Authentication, CORS, Rate Limiting, Caching, Logging, Exception Handling
|
||||||
|
- Attribute-based routing with `RouteAttribute`
|
||||||
|
- Custom `IMiddleware` interface for extensibility
|
||||||
|
|
||||||
|
## Module Dependencies
|
||||||
|
|
||||||
|
**Core Layer** (no external dependencies):
|
||||||
|
- Common → (no dependencies)
|
||||||
|
|
||||||
|
**Configuration Layer**:
|
||||||
|
- Config → Microsoft.Extensions.Configuration.*, Tomlyn (TOML support)
|
||||||
|
|
||||||
|
**Data Layer**:
|
||||||
|
- Data → EF Core, Common
|
||||||
|
- Repository → Data
|
||||||
|
|
||||||
|
**Network Layer**:
|
||||||
|
- NetClient → Polly, Microsoft.Extensions.Http, Common
|
||||||
|
- NetServer → Common, Log
|
||||||
|
|
||||||
|
**WPF Layer**:
|
||||||
|
- Wpf → HandyControl (optional UI controls)
|
||||||
|
- Client → Wpf, Config, NetClient, NetServer, Log, Plc.Siemens, Ports
|
||||||
|
|
||||||
|
**Industrial IoT**:
|
||||||
|
- Ports → (serial port communication)
|
||||||
|
- Plc → (abstraction only)
|
||||||
|
- Plc.Siemens → Plc, S7.NET+ library
|
||||||
|
|
||||||
|
**Logging**:
|
||||||
|
- Log → Serilog, Microsoft.Extensions.Logging
|
||||||
|
|
||||||
|
## Common Development Workflows
|
||||||
|
|
||||||
|
### Adding a new module/library
|
||||||
|
1. Create project under `src/Sharp/`
|
||||||
|
2. Target `net8.0` (or `net8.0-windows` for WPF)
|
||||||
|
3. Enable nullable reference types: `<Nullable>enable</Nullable>`
|
||||||
|
4. Create `DependencyInjection` folder for service registration extensions
|
||||||
|
5. Add test project under `test/` with MSTest
|
||||||
|
6. Add to Sharp.sln: `dotnet sln add <ProjectPath>`
|
||||||
|
|
||||||
|
### Working with Result<T>
|
||||||
|
- Use `Result.Try()` to wrap exception-prone code
|
||||||
|
- Chain operations with `Map()` for transformations
|
||||||
|
- Use `Bind()` for operations that return Result<T>
|
||||||
|
- Extract values with `GetOrDefault()`, `GetOrThrow()`, or `Match()`
|
||||||
|
- Always handle both success and failure cases
|
||||||
|
|
||||||
|
### Using the Global container
|
||||||
|
- Register singletons: `Global.RegisterSingleton<T>(instance)` or `Global.RegisterSingleton<T>(() => new T())`
|
||||||
|
- Register transients: `Global.RegisterTransient<T>(() => new T())`
|
||||||
|
- Subscribe to changes: `Global.Subscribe<T>(e => { /* handle change */ })`
|
||||||
|
- Retrieve services: `Global.Get<T>()`
|
||||||
|
- Prefer standard DI (IServiceCollection) for new code
|
||||||
|
|
||||||
|
### Configuration workflow
|
||||||
|
1. Add config files to project root (appsettings.json, etc.)
|
||||||
|
2. Create strongly-typed options class with DataAnnotations
|
||||||
|
3. Build ConfigManager in App.xaml.cs OnStartup
|
||||||
|
4. Register with DI: `services.AddSingleton(cfg)` and `services.AddSingleton(cfg.Configuration)`
|
||||||
|
5. Use hot-reload callbacks: `cfg.OnReload(() => { /* refresh */ })`
|
||||||
|
|
||||||
|
### WPF Client startup flow
|
||||||
|
The Client application (App.xaml.cs) orchestrates initialization:
|
||||||
|
1. Register global exception handlers (UI, Task, AppDomain)
|
||||||
|
2. Build ConfigManager from appsettings.json
|
||||||
|
3. Setup ServiceCollection and register all services
|
||||||
|
4. Create ServiceProvider
|
||||||
|
5. Show LoginWindow (modal)
|
||||||
|
6. On successful login, show MainWindow
|
||||||
|
|
||||||
|
### Working with PLC (Siemens)
|
||||||
|
- Interface: `IPLC` in Plc project
|
||||||
|
- Implementation: Plc.Siemens using S7.NET+ library
|
||||||
|
- Supports: S7-1200, S7-1500, S7-300, S7-400
|
||||||
|
- Register with DI: Add Siemens PLC service with connection options
|
||||||
|
- Methods: Read/Write memory areas, data blocks, etc.
|
||||||
|
|
||||||
|
### Serial Port Communication
|
||||||
|
- Service: `ISerialPortService` in Ports module
|
||||||
|
- Discovery: `SerialPortDiscovery` for auto-detection
|
||||||
|
- Event-based data reception: `DataReceivedEventArgs`
|
||||||
|
- Configured with: baud rate, parity, stop bits, data bits
|
||||||
|
|
||||||
|
## Code Style
|
||||||
|
|
||||||
|
- **Target Framework**: .NET 8 (net8.0 / net8.0-windows)
|
||||||
|
- **Nullable**: Enabled (`#nullable enable`)
|
||||||
|
- **Implicit Usings**: Enabled
|
||||||
|
- **Namespace Convention**: `Sharp.*` (Common will migrate to `Sharp.Common`)
|
||||||
|
- **Language**: All comments, docs, and strings in Chinese
|
||||||
|
- **Backwards Compatibility**: Maintain for minor versions; breaking changes only in major versions
|
||||||
|
- **Testing**: MSTest with parallel execution enabled
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
GNU AGPLv3 - Network services that use this framework must open source modifications. Contact author for alternative commercial licensing.
|
||||||
@@ -0,0 +1,486 @@
|
|||||||
|
# Sharp-Basic 配置指南
|
||||||
|
|
||||||
|
本文档说明 Sharp-Basic 框架的配置约定和最佳实践。
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
|
||||||
|
- [配置文件模板](#配置文件模板)
|
||||||
|
- [约定配置节名称](#约定配置节名称)
|
||||||
|
- [环境变量覆盖](#环境变量覆盖)
|
||||||
|
- [模块配置详解](#模块配置详解)
|
||||||
|
- [最佳实践](#最佳实践)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 配置文件模板
|
||||||
|
|
||||||
|
Sharp-Basic 提供了完整的配置文件模板:**`appsettings.template.json`**
|
||||||
|
|
||||||
|
### 使用方法
|
||||||
|
|
||||||
|
1. **复制模板文件**
|
||||||
|
```bash
|
||||||
|
cp appsettings.template.json appsettings.json
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **根据项目需要选择性保留配置**
|
||||||
|
- 只使用数据库?保留 `Database` 节
|
||||||
|
- 需要PLC通信?保留 `Plc:Siemens` 节
|
||||||
|
- 不需要的配置可以删除
|
||||||
|
|
||||||
|
3. **修改配置值**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Database": {
|
||||||
|
"Provider": "SqlServer",
|
||||||
|
"ConnectionString": "你的数据库连接字符串"
|
||||||
|
},
|
||||||
|
"Plc": {
|
||||||
|
"Siemens": {
|
||||||
|
"IpAddress": "你的PLC IP地址"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 约定配置节名称
|
||||||
|
|
||||||
|
Sharp-Basic 遵循 **"约定优于配置"** 原则,每个模块都有固定的配置节名称:
|
||||||
|
|
||||||
|
### 核心模块
|
||||||
|
|
||||||
|
| 模块 | 配置节名称 | 说明 | 必填字段 |
|
||||||
|
|------|------------|------|----------|
|
||||||
|
| **System** | `System` | 系统级HTTP配置 | `HTTPIp`, `HttpPort` |
|
||||||
|
| **Database** | `Database` | 数据库连接配置 | `Provider`, `ConnectionString` |
|
||||||
|
| **Logging** | `Logging` | 日志级别配置 | `LogLevel` |
|
||||||
|
| **SharpLog** | `SharpLog` | Sharp日志扩展 | 无(全部可选) |
|
||||||
|
|
||||||
|
### 网络模块
|
||||||
|
|
||||||
|
| 模块 | 配置节名称 | 说明 | 必填字段 |
|
||||||
|
|------|------------|------|----------|
|
||||||
|
| **NetClient** | `NetClient` | HTTP客户端配置 | 无(可使用System默认值) |
|
||||||
|
| **NetServer** | `NetServer` | HTTP服务端配置 | 无(可使用System默认值) |
|
||||||
|
|
||||||
|
### IoT模块
|
||||||
|
|
||||||
|
| 模块 | 配置节名称 | 说明 | 必填字段 |
|
||||||
|
|------|------------|------|----------|
|
||||||
|
| **Plc.Siemens** | `Plc:Siemens` | 西门子PLC配置 | `IpAddress`, `CpuType` |
|
||||||
|
| **SerialPort** | `SerialPort` | 串口通信配置 | `PortName`, `BaudRate` |
|
||||||
|
|
||||||
|
### 工具模块
|
||||||
|
|
||||||
|
| 模块 | 配置节名称 | 说明 | 必填字段 |
|
||||||
|
|------|------------|------|----------|
|
||||||
|
| **Process** | `Process` | 进程执行配置 | 无(全部可选) |
|
||||||
|
| **Python** | `Python` | Python脚本配置 | 无(全部可选) |
|
||||||
|
| **Cache** | `Cache` | 缓存配置 | 无(全部可选) |
|
||||||
|
| **EventBus** | `EventBus` | 事件总线配置 | 无(全部可选) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 环境变量覆盖
|
||||||
|
|
||||||
|
所有配置都可以通过环境变量覆盖,格式:`SHARP__{节名}__{键名}`
|
||||||
|
|
||||||
|
### 覆盖规则
|
||||||
|
|
||||||
|
- 使用双下划线 `__` 分隔层级
|
||||||
|
- 所有字母必须大写(Windows不区分大小写,Linux区分)
|
||||||
|
- 冒号 `:` 用双下划线 `__` 替代
|
||||||
|
|
||||||
|
### 示例
|
||||||
|
|
||||||
|
#### 覆盖数据库配置
|
||||||
|
```bash
|
||||||
|
# Windows (PowerShell)
|
||||||
|
$env:SHARP__Database__Provider="Sqlite"
|
||||||
|
$env:SHARP__Database__ConnectionString="Data Source=test.db"
|
||||||
|
|
||||||
|
# Linux/macOS
|
||||||
|
export SHARP__Database__Provider=Sqlite
|
||||||
|
export SHARP__Database__ConnectionString="Data Source=test.db"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 覆盖PLC配置
|
||||||
|
```bash
|
||||||
|
# Windows (PowerShell)
|
||||||
|
$env:SHARP__Plc__Siemens__IpAddress="192.168.1.200"
|
||||||
|
$env:SHARP__Plc__Siemens__CpuType="S71500"
|
||||||
|
|
||||||
|
# Linux/macOS
|
||||||
|
export SHARP__Plc__Siemens__IpAddress=192.168.1.200
|
||||||
|
export SHARP__Plc__Siemens__CpuType=S71500
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 覆盖System配置
|
||||||
|
```bash
|
||||||
|
# Windows (PowerShell)
|
||||||
|
$env:SHARP__System__HttpPort="9090"
|
||||||
|
|
||||||
|
# Linux/macOS
|
||||||
|
export SHARP__System__HttpPort=9090
|
||||||
|
```
|
||||||
|
|
||||||
|
### 优先级
|
||||||
|
|
||||||
|
配置优先级从高到低:
|
||||||
|
1. **环境变量** - 最高优先级
|
||||||
|
2. **appsettings.{Environment}.json** - 环境特定配置
|
||||||
|
3. **appsettings.json** - 默认配置
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 模块配置详解
|
||||||
|
|
||||||
|
### 1. 数据库配置 (Database)
|
||||||
|
|
||||||
|
#### 最简配置
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Database": {
|
||||||
|
"Provider": "Sqlite",
|
||||||
|
"ConnectionString": "Data Source=app.db"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 代码中使用
|
||||||
|
```csharp
|
||||||
|
var config = ConfigBuilder.CreateDefault().Configuration;
|
||||||
|
services.AddSharpDatabase<AppDbContext>(config); // 自动读取 Database 节
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 支持的数据库提供程序
|
||||||
|
- `SqlServer` - Microsoft SQL Server
|
||||||
|
- `Sqlite` - SQLite
|
||||||
|
- `PostgreSQL` - PostgreSQL
|
||||||
|
- `MySql` - MySQL/MariaDB
|
||||||
|
- `InMemory` - 内存数据库(仅用于测试)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. PLC配置 (Plc:Siemens)
|
||||||
|
|
||||||
|
#### 最简配置
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Plc": {
|
||||||
|
"Siemens": {
|
||||||
|
"IpAddress": "192.168.1.100",
|
||||||
|
"CpuType": "S71200"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 代码中使用
|
||||||
|
```csharp
|
||||||
|
var config = ConfigBuilder.CreateDefault().Configuration;
|
||||||
|
services.AddSiemensPlc(config.GetSection("Plc:Siemens"));
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 支持的CPU类型
|
||||||
|
- `S71200` - S7-1200 系列
|
||||||
|
- `S71500` - S7-1500 系列
|
||||||
|
- `S7300` - S7-300 系列
|
||||||
|
- `S7400` - S7-400 系列
|
||||||
|
- `S7200` - S7-200 系列
|
||||||
|
- `S7200Smart` - S7-200 Smart 系列
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. 日志配置 (Logging & SharpLog)
|
||||||
|
|
||||||
|
#### 最简配置
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"SharpLog": {
|
||||||
|
"LogFilePath": "logs/app-.log",
|
||||||
|
"RollingInterval": "Day"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 代码中使用
|
||||||
|
```csharp
|
||||||
|
services.AddLogging(b => b.AddSharpLog()); // 自动读取 SharpLog 节
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. HTTP配置 (System, NetClient, NetServer)
|
||||||
|
|
||||||
|
#### System配置(共享默认值)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"System": {
|
||||||
|
"HTTPIp": "127.0.0.1",
|
||||||
|
"HttpPort": 8080,
|
||||||
|
"TimeoutSeconds": 30,
|
||||||
|
"Scheme": "http"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### NetClient配置(可选,留空则使用System默认值)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"NetClient": {
|
||||||
|
"BaseAddress": "http://api.example.com",
|
||||||
|
"Timeout": "00:00:30",
|
||||||
|
"EnableLogging": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### NetServer配置(可选,留空则使用System默认值)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"NetServer": {
|
||||||
|
"ListenUrls": [
|
||||||
|
"http://localhost:8080/"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 最佳实践
|
||||||
|
|
||||||
|
### 1. 按需配置
|
||||||
|
|
||||||
|
**只保留需要的配置节**,不要保留所有模板内容:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
// ❌ 不好:保留了不使用的配置
|
||||||
|
"Database": { ... },
|
||||||
|
"Plc": { ... },
|
||||||
|
"SerialPort": { ... },
|
||||||
|
"Python": { ... }
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// ✅ 好:只保留需要的配置
|
||||||
|
"Database": { ... },
|
||||||
|
"Logging": { ... }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 使用环境特定配置
|
||||||
|
|
||||||
|
为不同环境创建不同的配置文件:
|
||||||
|
|
||||||
|
```
|
||||||
|
appsettings.json # 默认配置
|
||||||
|
appsettings.Development.json # 开发环境
|
||||||
|
appsettings.Production.json # 生产环境
|
||||||
|
appsettings.Testing.json # 测试环境
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 敏感信息使用环境变量
|
||||||
|
|
||||||
|
**不要在配置文件中存储敏感信息**(密码、密钥等):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# ✅ 好:使用环境变量
|
||||||
|
export SHARP__Database__ConnectionString="Server=prod;User=sa;Password=SecretPass123"
|
||||||
|
export SHARP__Plc__Siemens__IpAddress="192.168.1.100"
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
// ❌ 不好:敏感信息写在配置文件
|
||||||
|
{
|
||||||
|
"Database": {
|
||||||
|
"ConnectionString": "Server=prod;User=sa;Password=SecretPass123"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 添加注释说明
|
||||||
|
|
||||||
|
在配置文件中添加注释(JSON5格式,或删除注释后部署):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
// 数据库配置 - 生产环境使用SQL Server
|
||||||
|
"Database": {
|
||||||
|
"Provider": "SqlServer",
|
||||||
|
"ConnectionString": "Server=.;Database=MyApp;..."
|
||||||
|
},
|
||||||
|
|
||||||
|
// PLC配置 - 车间1号PLC
|
||||||
|
"Plc": {
|
||||||
|
"Siemens": {
|
||||||
|
"IpAddress": "192.168.1.100", // 车间1号PLC
|
||||||
|
"CpuType": "S71200"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 配置验证
|
||||||
|
|
||||||
|
Sharp-Basic 会在启动时验证必填配置项:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// ✅ 好:配置缺失时会抛出清晰的异常
|
||||||
|
services.AddSharpDatabase<AppDbContext>(config);
|
||||||
|
// 异常:未找到数据库连接字符串配置: Database:ConnectionString
|
||||||
|
|
||||||
|
services.AddSiemensPlc(config.GetSection("Plc:Siemens"));
|
||||||
|
// 异常:未配置PLC IP地址
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 快速参考
|
||||||
|
|
||||||
|
### 典型WPF应用配置
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"System": {
|
||||||
|
"HTTPIp": "127.0.0.1",
|
||||||
|
"HttpPort": 8080
|
||||||
|
},
|
||||||
|
"Database": {
|
||||||
|
"Provider": "Sqlite",
|
||||||
|
"ConnectionString": "Data Source=app.db"
|
||||||
|
},
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"SharpLog": {
|
||||||
|
"LogFilePath": "logs/app-.log",
|
||||||
|
"RollingInterval": "Day"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 工业IoT应用配置
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"System": {
|
||||||
|
"HTTPIp": "0.0.0.0",
|
||||||
|
"HttpPort": 8080
|
||||||
|
},
|
||||||
|
"Database": {
|
||||||
|
"Provider": "SqlServer",
|
||||||
|
"ConnectionString": "Server=.;Database=ScadaDB;Trusted_Connection=True;"
|
||||||
|
},
|
||||||
|
"Plc": {
|
||||||
|
"Siemens": {
|
||||||
|
"IpAddress": "192.168.1.100",
|
||||||
|
"CpuType": "S71200",
|
||||||
|
"MonitorInterval": 500,
|
||||||
|
"AutoReconnect": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"SerialPort": {
|
||||||
|
"PortName": "COM1",
|
||||||
|
"BaudRate": 9600
|
||||||
|
},
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Plc.Siemens": "Debug"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Web服务应用配置
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"System": {
|
||||||
|
"HTTPIp": "0.0.0.0",
|
||||||
|
"HttpPort": 5000,
|
||||||
|
"Scheme": "https"
|
||||||
|
},
|
||||||
|
"Database": {
|
||||||
|
"Provider": "PostgreSQL",
|
||||||
|
"ConnectionString": "Host=localhost;Database=myapi;Username=postgres;Password=***"
|
||||||
|
},
|
||||||
|
"NetServer": {
|
||||||
|
"ListenUrls": [
|
||||||
|
"https://0.0.0.0:5000/",
|
||||||
|
"http://0.0.0.0:5001/"
|
||||||
|
],
|
||||||
|
"ShowDetailedErrors": false
|
||||||
|
},
|
||||||
|
"Cache": {
|
||||||
|
"DefaultExpirationSeconds": 300,
|
||||||
|
"SizeLimit": 10000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 相关文档
|
||||||
|
|
||||||
|
- [appsettings.template.json](appsettings.template.json) - 完整配置模板
|
||||||
|
- [Config模块README](src/Sharp/Config/README.md) - 配置管理详解
|
||||||
|
- [Data模块README](src/Sharp/Data/README.md) - 数据库配置详解
|
||||||
|
- [Plc.Siemens模块README](src/Sharp/Plc.Siemens/README.md) - PLC配置详解
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 故障排查
|
||||||
|
|
||||||
|
### 问题:找不到配置节
|
||||||
|
|
||||||
|
**错误信息**:
|
||||||
|
```
|
||||||
|
未找到数据库连接字符串配置: Database:ConnectionString
|
||||||
|
```
|
||||||
|
|
||||||
|
**解决方法**:
|
||||||
|
1. 检查配置文件名是否为 `appsettings.json`
|
||||||
|
2. 检查配置节名称是否正确(区分大小写)
|
||||||
|
3. 检查配置文件是否在项目根目录
|
||||||
|
4. 检查配置文件的 "复制到输出目录" 设置
|
||||||
|
|
||||||
|
### 问题:环境变量不生效
|
||||||
|
|
||||||
|
**原因**:环境变量格式错误
|
||||||
|
|
||||||
|
**正确格式**:
|
||||||
|
```bash
|
||||||
|
# ✅ 正确:双下划线分隔
|
||||||
|
SHARP__Database__Provider=Sqlite
|
||||||
|
|
||||||
|
# ❌ 错误:单下划线
|
||||||
|
SHARP_Database_Provider=Sqlite
|
||||||
|
|
||||||
|
# ❌ 错误:使用冒号
|
||||||
|
SHARP:Database:Provider=Sqlite
|
||||||
|
```
|
||||||
|
|
||||||
|
### 问题:配置被覆盖
|
||||||
|
|
||||||
|
**检查优先级**:
|
||||||
|
1. 先检查环境变量
|
||||||
|
2. 再检查 appsettings.{Environment}.json
|
||||||
|
3. 最后检查 appsettings.json
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**版本**: 1.0.0
|
||||||
|
**更新日期**: 2025-12-22
|
||||||
+197
@@ -0,0 +1,197 @@
|
|||||||
|
# 贡献指南
|
||||||
|
|
||||||
|
感谢您考虑为 Sharp-Basic Framework 做出贡献!
|
||||||
|
|
||||||
|
## 📜 行为准则
|
||||||
|
|
||||||
|
请遵守以下基本原则:
|
||||||
|
- 尊重所有贡献者
|
||||||
|
- 保持友好和包容的态度
|
||||||
|
- 接受建设性的批评
|
||||||
|
- 专注于对社区最有利的事情
|
||||||
|
|
||||||
|
## 🚀 如何贡献
|
||||||
|
|
||||||
|
### 报告 Bug
|
||||||
|
|
||||||
|
如果您发现了 bug,请创建一个 Issue 并包含以下信息:
|
||||||
|
1. **标题**: 简洁描述问题
|
||||||
|
2. **环境**: 操作系统、.NET 版本、项目版本
|
||||||
|
3. **复现步骤**: 详细的步骤
|
||||||
|
4. **预期行为**: 应该发生什么
|
||||||
|
5. **实际行为**: 实际发生了什么
|
||||||
|
6. **日志/截图**: 如果有的话
|
||||||
|
|
||||||
|
### 提出新功能
|
||||||
|
|
||||||
|
在开始编码之前:
|
||||||
|
1. 先创建一个 Issue 讨论您的想法
|
||||||
|
2. 等待维护者的反馈
|
||||||
|
3. 确保功能符合项目的整体方向
|
||||||
|
|
||||||
|
### 提交代码
|
||||||
|
|
||||||
|
#### 准备工作
|
||||||
|
```bash
|
||||||
|
# Fork 项目到您的账号
|
||||||
|
# Clone 到本地
|
||||||
|
git clone https://gitea.duyl328.org/您的用户名/sharp-basic.git
|
||||||
|
cd sharp-basic
|
||||||
|
|
||||||
|
# 添加上游仓库
|
||||||
|
git remote add upstream https://gitea.duyl328.org/jimmy/sharp-basic.git
|
||||||
|
|
||||||
|
# 创建功能分支
|
||||||
|
git checkout -b feature/您的功能名称
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 代码规范
|
||||||
|
|
||||||
|
1. **命名约定**
|
||||||
|
- 类名:PascalCase(如 `ConfigManager`)
|
||||||
|
- 方法名:PascalCase(如 `BuildConfiguration`)
|
||||||
|
- 私有字段:_camelCase(如 `_logger`)
|
||||||
|
- 参数/局部变量:camelCase(如 `userName`)
|
||||||
|
|
||||||
|
2. **注释规范**
|
||||||
|
- 所有公共 API 必须有 XML 文档注释
|
||||||
|
- 使用中文编写注释和文档
|
||||||
|
- 复杂逻辑需要添加行内注释
|
||||||
|
|
||||||
|
3. **代码风格**
|
||||||
|
- 启用 nullable 引用类型
|
||||||
|
- 使用 `var` 当类型明显时
|
||||||
|
- 一行不超过 120 字符
|
||||||
|
- 使用 4 个空格缩进(不使用 Tab)
|
||||||
|
|
||||||
|
4. **测试要求**
|
||||||
|
- 新功能必须包含单元测试
|
||||||
|
- 测试覆盖率应 >= 80%
|
||||||
|
- 使用 MSTest 框架
|
||||||
|
- 测试方法命名: `方法名_场景_预期结果`
|
||||||
|
|
||||||
|
#### 提交信息规范
|
||||||
|
|
||||||
|
使用以下格式:
|
||||||
|
```
|
||||||
|
<类型>(<模块>): <简短描述>
|
||||||
|
|
||||||
|
<详细描述(可选)>
|
||||||
|
|
||||||
|
<关联 Issue(如果有)>
|
||||||
|
```
|
||||||
|
|
||||||
|
**类型**:
|
||||||
|
- `feat`: 新功能
|
||||||
|
- `fix`: Bug 修复
|
||||||
|
- `docs`: 文档更新
|
||||||
|
- `style`: 代码格式调整(不影响功能)
|
||||||
|
- `refactor`: 重构
|
||||||
|
- `test`: 测试相关
|
||||||
|
- `chore`: 构建/工具相关
|
||||||
|
|
||||||
|
**示例**:
|
||||||
|
```
|
||||||
|
feat(Config): 添加 TOML 格式支持
|
||||||
|
|
||||||
|
实现了基于 Tomlyn 的 TOML 配置文件解析功能
|
||||||
|
支持热重载和强类型绑定
|
||||||
|
|
||||||
|
Closes #123
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Pull Request 流程
|
||||||
|
|
||||||
|
1. **确保代码质量**
|
||||||
|
```bash
|
||||||
|
# 运行所有测试
|
||||||
|
dotnet test
|
||||||
|
|
||||||
|
# 构建项目
|
||||||
|
dotnet build -c Release
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **同步上游更改**
|
||||||
|
```bash
|
||||||
|
git fetch upstream
|
||||||
|
git rebase upstream/main
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **提交 PR**
|
||||||
|
- 填写清晰的 PR 标题和描述
|
||||||
|
- 关联相关的 Issue
|
||||||
|
- 确保 CI 通过(如果有)
|
||||||
|
- 等待代码审查
|
||||||
|
|
||||||
|
4. **代码审查**
|
||||||
|
- 维护者会审查您的代码
|
||||||
|
- 根据反馈进行修改
|
||||||
|
- 保持耐心和礼貌
|
||||||
|
|
||||||
|
## 📂 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
src/Sharp/ # 所有源代码项目
|
||||||
|
├── Common/ # 核心工具库
|
||||||
|
├── Config/ # 配置管理
|
||||||
|
├── Data/ # 数据访问层
|
||||||
|
├── Log/ # 日志模块
|
||||||
|
├── Wpf/ # WPF 基础库
|
||||||
|
└── ...
|
||||||
|
|
||||||
|
test/ # 测试项目
|
||||||
|
├── Common.Test/
|
||||||
|
├── Config.Test/
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 开发环境设置
|
||||||
|
|
||||||
|
### 必需工具
|
||||||
|
- [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0)
|
||||||
|
- [Git](https://git-scm.com/)
|
||||||
|
- 推荐 IDE:
|
||||||
|
- [Visual Studio 2022](https://visualstudio.microsoft.com/) (Windows)
|
||||||
|
- [Rider](https://www.jetbrains.com/rider/) (跨平台)
|
||||||
|
- [VS Code](https://code.visualstudio.com/) + C# 扩展
|
||||||
|
|
||||||
|
### 首次构建
|
||||||
|
```bash
|
||||||
|
# 还原依赖
|
||||||
|
dotnet restore
|
||||||
|
|
||||||
|
# 构建解决方案
|
||||||
|
dotnet build Sharp.sln
|
||||||
|
|
||||||
|
# 运行测试
|
||||||
|
dotnet test
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📋 待办事项
|
||||||
|
|
||||||
|
查看 [Issues](https://gitea.duyl328.org/jimmy/sharp-basic/issues) 页面了解:
|
||||||
|
- 标记为 `good first issue` 的任务适合新贡献者
|
||||||
|
- 标记为 `help wanted` 的任务需要帮助
|
||||||
|
|
||||||
|
## 📝 文档贡献
|
||||||
|
|
||||||
|
文档同样重要!您可以:
|
||||||
|
- 改进现有文档的清晰度
|
||||||
|
- 添加示例代码
|
||||||
|
- 修正错别字
|
||||||
|
- 翻译文档(如果需要)
|
||||||
|
|
||||||
|
## ⚖️ 许可证
|
||||||
|
|
||||||
|
提交代码即表示您同意将您的贡献以 [AGPL-3.0-or-later](LICENSE) 许可证发布。
|
||||||
|
|
||||||
|
## 💬 联系方式
|
||||||
|
|
||||||
|
如有任何问题:
|
||||||
|
- 创建 [Issue](https://gitea.duyl328.org/jimmy/sharp-basic/issues)
|
||||||
|
- 查看 [README](README.md) 获取更多信息
|
||||||
|
- 访问作者网站: [https://seyl.me/](https://seyl.me/)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
再次感谢您的贡献!🎉
|
||||||
@@ -219,8 +219,8 @@ If you develop a new program, and you want it to be of the greatest possible use
|
|||||||
|
|
||||||
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found.
|
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
sharp-basic
|
Sharp-Basic Framework
|
||||||
Copyright (C) 2025 jimmy
|
Copyright (C) 2024-2025 duyl328
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,216 @@
|
|||||||
|
# 项目元数据清理总结
|
||||||
|
|
||||||
|
## ✅ 已完成的任务
|
||||||
|
|
||||||
|
### 1. Git 清理
|
||||||
|
- ✅ 从 Git 中移除了 `src/Sharp/Wpf/obj/` 文件夹下的所有 22 个文件
|
||||||
|
- ✅ 确认其他模块没有 obj/bin 文件被错误追踪
|
||||||
|
- ✅ `.gitignore` 已正确配置 `obj/` 和 `bin/` 规则
|
||||||
|
|
||||||
|
### 2. 元数据统一管理
|
||||||
|
|
||||||
|
#### 创建的核心文件
|
||||||
|
- **`Directory.Build.props`** - 集中管理所有项目的元数据
|
||||||
|
- 版本:1.0.1
|
||||||
|
- 作者:duyl328
|
||||||
|
- 版权:Copyright © 2024-2025 duyl328
|
||||||
|
- 仓库:https://gitea.duyl328.org/jimmy/sharp-basic
|
||||||
|
- 许可证:AGPL-3.0-or-later
|
||||||
|
- 项目描述、标签等
|
||||||
|
|
||||||
|
#### 清理的项目文件
|
||||||
|
成功清理了 **17 个项目文件**,移除了冗余的元数据:
|
||||||
|
|
||||||
|
**已清理的项目**:
|
||||||
|
1. Cache.csproj
|
||||||
|
2. Common.csproj
|
||||||
|
3. Config.csproj
|
||||||
|
4. Data.csproj
|
||||||
|
5. EventBus.csproj
|
||||||
|
6. Log.csproj
|
||||||
|
7. NetClient.csproj
|
||||||
|
8. NetServer.csproj
|
||||||
|
9. Plc.csproj
|
||||||
|
10. Plc.Siemens.csproj
|
||||||
|
11. Ports.csproj
|
||||||
|
12. Process.csproj
|
||||||
|
13. Python.csproj
|
||||||
|
14. Sharp.Demo.csproj
|
||||||
|
15. Wpf.csproj
|
||||||
|
16. Wpf.DevTools.csproj
|
||||||
|
17. Wpf.HandyControl.csproj
|
||||||
|
|
||||||
|
**每个项目移除了 12 个冗余属性**:
|
||||||
|
- Version, AssemblyVersion, FileVersion
|
||||||
|
- Authors, Company, Product, Copyright
|
||||||
|
- PackageId, PackageProjectUrl, RepositoryUrl, RepositoryType, PackageLicenseExpression
|
||||||
|
|
||||||
|
**保留的项目特有属性**:
|
||||||
|
- `Description` - 每个项目的独特描述
|
||||||
|
- `PackageTags` - 每个项目的特定标签
|
||||||
|
|
||||||
|
### 3. 项目元数据验证
|
||||||
|
|
||||||
|
#### 清理前的项目文件(以 Common.csproj 为例)
|
||||||
|
```xml
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<!-- ... -->
|
||||||
|
<Version>1.0.0</Version>
|
||||||
|
<AssemblyVersion>1.0.0.0</AssemblyVersion>
|
||||||
|
<FileVersion>1.0.0.0</FileVersion>
|
||||||
|
<Authors>Sharp Team</Authors>
|
||||||
|
<Company>Sharp Team</Company>
|
||||||
|
<Product>Sharp Framework</Product>
|
||||||
|
<Copyright>Copyright © 2024 Sharp Team</Copyright>
|
||||||
|
<Description>...</Description>
|
||||||
|
<PackageId>Sharp.Common</PackageId>
|
||||||
|
<PackageProjectUrl>https://github.com/your-org/sharp-basic</PackageProjectUrl>
|
||||||
|
<RepositoryUrl>https://github.com/your-org/sharp-basic</RepositoryUrl>
|
||||||
|
<RepositoryType>git</RepositoryType>
|
||||||
|
<PackageTags>...</PackageTags>
|
||||||
|
<PackageLicenseExpression>AGPL-3.0-or-later</PackageLicenseExpression>
|
||||||
|
</PropertyGroup>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 清理后的项目文件
|
||||||
|
```xml
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
|
||||||
|
<Description>Sharp Framework 核心库 - 提供 Result<T> 模式、Global 容器、系统配置等基础功能</Description>
|
||||||
|
<PackageTags>sharp;common;result;railway-oriented;functional;utilities</PackageTags>
|
||||||
|
</PropertyGroup>
|
||||||
|
```
|
||||||
|
|
||||||
|
**优势**:
|
||||||
|
- 📉 代码大幅减少(从 ~25 行元数据减少到 2 行)
|
||||||
|
- 🔄 统一管理:版本、作者、许可证等在一处更新
|
||||||
|
- ✨ 保持灵活性:每个项目保留独特的描述和标签
|
||||||
|
|
||||||
|
### 4. 创建的工具脚本
|
||||||
|
|
||||||
|
**`update-metadata.ps1`**
|
||||||
|
- 自动清理所有项目文件的冗余元数据
|
||||||
|
- 可重复运行,安全无副作用
|
||||||
|
- 详细日志输出,显示每个项目的处理结果
|
||||||
|
|
||||||
|
使用方法:
|
||||||
|
```powershell
|
||||||
|
cd src/Sharp
|
||||||
|
powershell -ExecutionPolicy Bypass -File update-metadata.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📋 Git 变更概览
|
||||||
|
|
||||||
|
```
|
||||||
|
修改的文件 (M):
|
||||||
|
- 17 个 .csproj 项目文件(清理元数据)
|
||||||
|
- LICENSE(更新版权声明)
|
||||||
|
- README.md(添加项目信息和徽章)
|
||||||
|
|
||||||
|
删除的文件 (D):
|
||||||
|
- src/Sharp/Wpf/obj/** (22 个文件)
|
||||||
|
|
||||||
|
新增的文件 (??):
|
||||||
|
- AUTHORS
|
||||||
|
- CHANGELOG.md
|
||||||
|
- CONTRIBUTING.md
|
||||||
|
- NOTICE
|
||||||
|
- PROJECT_METADATA_SUMMARY.md
|
||||||
|
- METADATA_CLEANUP_SUMMARY.md
|
||||||
|
- src/Sharp/Directory.Build.props
|
||||||
|
- src/Sharp/update-metadata.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 元数据继承关系
|
||||||
|
|
||||||
|
```
|
||||||
|
Directory.Build.props (父级)
|
||||||
|
│
|
||||||
|
├─→ 所有项目自动继承:
|
||||||
|
│ • Version: 1.0.1
|
||||||
|
│ • Authors: duyl328
|
||||||
|
│ • Company: duyl328
|
||||||
|
│ • Product: Sharp-Basic Framework
|
||||||
|
│ • Copyright: Copyright © 2024-2025 duyl328
|
||||||
|
│ • Description: 模块化.NET 8企业应用开发框架
|
||||||
|
│ • RepositoryUrl: https://gitea.duyl328.org/jimmy/sharp-basic
|
||||||
|
│ • PackageLicenseExpression: AGPL-3.0-or-later
|
||||||
|
│ • PackageTags: wpf;mvvm;dotnet;framework;...
|
||||||
|
│ • ... 其他编译和包设置
|
||||||
|
│
|
||||||
|
└─→ 各项目.csproj (子级)
|
||||||
|
• 只定义项目特有的属性
|
||||||
|
• Description (覆盖父级的通用描述)
|
||||||
|
• PackageTags (覆盖父级的通用标签)
|
||||||
|
```
|
||||||
|
|
||||||
|
## ✨ 效果验证
|
||||||
|
|
||||||
|
### 构建测试
|
||||||
|
```bash
|
||||||
|
cd src/Sharp
|
||||||
|
dotnet build Sharp.sln -c Release
|
||||||
|
```
|
||||||
|
|
||||||
|
### 查看生成的程序集信息
|
||||||
|
```bash
|
||||||
|
# 所有程序集应显示:
|
||||||
|
# Version: 1.0.1.0
|
||||||
|
# FileVersion: 1.0.1.0
|
||||||
|
# Company: duyl328
|
||||||
|
# Product: Sharp-Basic Framework
|
||||||
|
# Copyright: Copyright © 2024-2025 duyl328. All rights reserved.
|
||||||
|
```
|
||||||
|
|
||||||
|
### 打包验证
|
||||||
|
```bash
|
||||||
|
dotnet pack Common/Common.csproj -c Release -o ./nupkg
|
||||||
|
|
||||||
|
# 检查生成的 .nupkg 文件元数据
|
||||||
|
# 应包含从 Directory.Build.props 继承的所有信息
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 下一步建议
|
||||||
|
|
||||||
|
1. **提交更改**
|
||||||
|
```bash
|
||||||
|
git add .
|
||||||
|
git commit -m "A:统一项目元数据管理
|
||||||
|
|
||||||
|
- 创建 Directory.Build.props 集中管理元数据
|
||||||
|
- 清理所有项目文件的冗余元数据(17个项目)
|
||||||
|
- 从 Git 中移除 Wpf/obj 文件夹
|
||||||
|
- 添加完整的项目文档(AUTHORS, CHANGELOG, CONTRIBUTING, NOTICE)
|
||||||
|
- 更新 LICENSE 和 README.md
|
||||||
|
|
||||||
|
版本: 1.0.1
|
||||||
|
作者: duyl328
|
||||||
|
许可证: AGPL-3.0-or-later"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **创建 Git 标签**
|
||||||
|
```bash
|
||||||
|
git tag -a v1.0.1 -m "Release version 1.0.1"
|
||||||
|
git push origin v1.0.1
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **发布 NuGet 包**(如果需要)
|
||||||
|
```bash
|
||||||
|
dotnet pack -c Release
|
||||||
|
# 将 nupkg 文件发布到 NuGet 或私有包源
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎉 完成状态
|
||||||
|
|
||||||
|
✅ Wpf/obj 文件夹已从 Git 中移除
|
||||||
|
✅ 所有 17 个模块的元数据已清理并统一
|
||||||
|
✅ Directory.Build.props 正确配置
|
||||||
|
✅ 项目文档完整(LICENSE, AUTHORS, CONTRIBUTING, CHANGELOG, NOTICE)
|
||||||
|
✅ README.md 包含徽章和项目信息
|
||||||
|
✅ 自动化清理脚本可供未来使用
|
||||||
|
|
||||||
|
**项目现在是一个标准的、专业的、元数据管理规范的开源项目!** 🎊
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
Sharp-Basic Framework
|
||||||
|
Copyright © 2024-2025 duyl328
|
||||||
|
https://gitea.duyl328.org/jimmy/sharp-basic
|
||||||
|
|
||||||
|
本软件采用 GNU Affero General Public License v3.0 或更高版本 (AGPL-3.0-or-later) 许可。
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
本软件使用了以下第三方开源组件:
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
Microsoft.Extensions.* 系列
|
||||||
|
版权所有 © .NET Foundation and Contributors
|
||||||
|
许可证: MIT
|
||||||
|
来源: https://github.com/dotnet/runtime
|
||||||
|
|
||||||
|
包括但不限于:
|
||||||
|
- Microsoft.Extensions.DependencyInjection
|
||||||
|
- Microsoft.Extensions.Configuration
|
||||||
|
- Microsoft.Extensions.Logging
|
||||||
|
- Microsoft.Extensions.Options
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
Entity Framework Core
|
||||||
|
版权所有 © .NET Foundation and Contributors
|
||||||
|
许可证: MIT
|
||||||
|
来源: https://github.com/dotnet/efcore
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
Serilog
|
||||||
|
版权所有 © Serilog Contributors
|
||||||
|
许可证: Apache-2.0
|
||||||
|
来源: https://github.com/serilog/serilog
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
Polly
|
||||||
|
版权所有 © Polly Contributors
|
||||||
|
许可证: BSD-3-Clause
|
||||||
|
来源: https://github.com/App-vNext/Polly
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
HandyControl
|
||||||
|
版权所有 © HandyOrg
|
||||||
|
许可证: MIT
|
||||||
|
来源: https://github.com/HandyOrg/HandyControl
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
S7.NET+
|
||||||
|
版权所有 © S7.NET+ Contributors
|
||||||
|
许可证: MIT
|
||||||
|
来源: https://github.com/S7NetPlus/s7netplus
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
Tomlyn (TOML 解析)
|
||||||
|
版权所有 © Alexandre Mutel
|
||||||
|
许可证: BSD-2-Clause
|
||||||
|
来源: https://github.com/xoofx/Tomlyn
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
完整的第三方许可证文本,请参阅各组件的官方仓库或 NuGet 包中的 LICENSE 文件。
|
||||||
|
|
||||||
|
本软件依赖的所有第三方组件均为开源软件,并在其各自的许可证下分发。
|
||||||
|
使用本软件即表示您同意遵守这些第三方许可证的条款。
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
# 项目元数据配置总结
|
||||||
|
|
||||||
|
本文档总结了 Sharp-Basic Framework v1.0.1 的元数据配置。
|
||||||
|
|
||||||
|
## ✅ 已完成的配置
|
||||||
|
|
||||||
|
### 1. 核心元数据文件
|
||||||
|
|
||||||
|
#### `Directory.Build.props` (src/Sharp/)
|
||||||
|
- ✅ 作者: duyl328
|
||||||
|
- ✅ 版权: Copyright © 2024-2025 duyl328
|
||||||
|
- ✅ 版本: 1.0.1
|
||||||
|
- ✅ 仓库: https://gitea.duyl328.org/jimmy/sharp-basic
|
||||||
|
- ✅ 许可证: AGPL-3.0-or-later
|
||||||
|
- ✅ 项目描述: 模块化.NET 8企业应用开发框架
|
||||||
|
- ✅ 包标签: wpf;mvvm;dotnet;framework;plc;industrial;iot;siemens;ef-core;serilog;configuration
|
||||||
|
- ✅ 源代码链接支持 (Source Link for Gitea)
|
||||||
|
- ✅ 确定性构建配置
|
||||||
|
|
||||||
|
### 2. 许可证文件
|
||||||
|
|
||||||
|
#### `LICENSE`
|
||||||
|
- ✅ GNU AGPL v3.0 完整文本
|
||||||
|
- ✅ 更新版权声明示例
|
||||||
|
|
||||||
|
### 3. 项目文档
|
||||||
|
|
||||||
|
#### `README.md`
|
||||||
|
- ✅ 项目徽章(版本、许可证、.NET版本)
|
||||||
|
- ✅ 项目信息(版本、作者、仓库、许可证)
|
||||||
|
- ✅ 详细的许可证说明(包含网络服务条款)
|
||||||
|
- ✅ 作者信息和链接
|
||||||
|
- ✅ 版权声明
|
||||||
|
|
||||||
|
#### `AUTHORS`
|
||||||
|
- ✅ 主要作者信息
|
||||||
|
- ✅ 贡献者列表框架
|
||||||
|
|
||||||
|
#### `CONTRIBUTING.md`
|
||||||
|
- ✅ 行为准则
|
||||||
|
- ✅ Bug 报告指南
|
||||||
|
- ✅ 功能请求流程
|
||||||
|
- ✅ 代码规范(命名、注释、风格)
|
||||||
|
- ✅ 测试要求
|
||||||
|
- ✅ 提交信息规范
|
||||||
|
- ✅ Pull Request 流程
|
||||||
|
- ✅ 开发环境设置
|
||||||
|
|
||||||
|
#### `CHANGELOG.md`
|
||||||
|
- ✅ 版本 1.0.1 变更记录
|
||||||
|
- ✅ 版本 1.0.0 初始发布记录
|
||||||
|
- ✅ 版本号规范说明
|
||||||
|
- ✅ 变更类型定义
|
||||||
|
|
||||||
|
#### `NOTICE`
|
||||||
|
- ✅ 项目版权声明
|
||||||
|
- ✅ 第三方依赖列表及其许可证
|
||||||
|
- Microsoft.Extensions.*
|
||||||
|
- Entity Framework Core
|
||||||
|
- Serilog
|
||||||
|
- Polly
|
||||||
|
- HandyControl
|
||||||
|
- S7.NET+
|
||||||
|
- Tomlyn
|
||||||
|
|
||||||
|
## 📋 NuGet 包元数据
|
||||||
|
|
||||||
|
所有项目现在继承以下元数据(通过 Directory.Build.props):
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<Authors>duyl328</Authors>
|
||||||
|
<Company>duyl328</Company>
|
||||||
|
<Product>Sharp-Basic Framework</Product>
|
||||||
|
<Description>模块化.NET 8企业应用开发框架</Description>
|
||||||
|
<Copyright>Copyright © 2024-2025 duyl328. All rights reserved.</Copyright>
|
||||||
|
<VersionPrefix>1.0.1</VersionPrefix>
|
||||||
|
<RepositoryUrl>https://gitea.duyl328.org/jimmy/sharp-basic</RepositoryUrl>
|
||||||
|
<PackageLicenseExpression>AGPL-3.0-or-later</PackageLicenseExpression>
|
||||||
|
<PackageTags>wpf;mvvm;dotnet;framework;plc;industrial;iot;siemens;ef-core;serilog;configuration;csharp;enterprise;modular;iiot</PackageTags>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔍 下一步建议
|
||||||
|
|
||||||
|
### 可选的额外文件
|
||||||
|
|
||||||
|
1. **CODE_OF_CONDUCT.md** - 社区行为准则
|
||||||
|
- 推荐使用 Contributor Covenant
|
||||||
|
- 定义社区标准和执法机制
|
||||||
|
|
||||||
|
2. **SECURITY.md** - 安全策略
|
||||||
|
- 如何报告安全漏洞
|
||||||
|
- 支持的版本
|
||||||
|
- 披露政策
|
||||||
|
|
||||||
|
3. **.editorconfig** - 编辑器配置
|
||||||
|
- 统一代码风格
|
||||||
|
- 适用于所有贡献者
|
||||||
|
|
||||||
|
4. **Issue/PR 模板** (Gitea 支持)
|
||||||
|
- `.gitea/ISSUE_TEMPLATE/bug_report.md`
|
||||||
|
- `.gitea/ISSUE_TEMPLATE/feature_request.md`
|
||||||
|
- `.gitea/PULL_REQUEST_TEMPLATE.md`
|
||||||
|
|
||||||
|
5. **Badges 徽章增强**
|
||||||
|
- 构建状态(如果有 CI)
|
||||||
|
- 测试覆盖率
|
||||||
|
- NuGet 下载量
|
||||||
|
- 文档链接
|
||||||
|
|
||||||
|
### 持续维护
|
||||||
|
|
||||||
|
- 📅 定期更新 CHANGELOG.md
|
||||||
|
- 👥 更新 AUTHORS 文件添加新贡献者
|
||||||
|
- 📦 发布新版本时更新版本号
|
||||||
|
- 📄 保持文档与代码同步
|
||||||
|
|
||||||
|
## 🎯 标准合规性检查
|
||||||
|
|
||||||
|
✅ **开源许可证**: AGPL-3.0-or-later
|
||||||
|
✅ **版本管理**: 语义化版本 1.0.1
|
||||||
|
✅ **版权声明**: 所有必要文件已包含
|
||||||
|
✅ **第三方归属**: NOTICE 文件已创建
|
||||||
|
✅ **贡献流程**: CONTRIBUTING.md 已完善
|
||||||
|
✅ **变更跟踪**: CHANGELOG.md 已建立
|
||||||
|
✅ **作者信息**: AUTHORS 文件已创建
|
||||||
|
✅ **项目描述**: README.md 详细且专业
|
||||||
|
|
||||||
|
## 📝 维护命令
|
||||||
|
|
||||||
|
### 更新版本号
|
||||||
|
```powershell
|
||||||
|
# 编辑 Directory.Build.props
|
||||||
|
# 更新 <VersionPrefix> 标签
|
||||||
|
# 更新 CHANGELOG.md 添加新版本记录
|
||||||
|
```
|
||||||
|
|
||||||
|
### 构建 NuGet 包
|
||||||
|
```bash
|
||||||
|
dotnet pack -c Release
|
||||||
|
```
|
||||||
|
|
||||||
|
### 验证元数据
|
||||||
|
```bash
|
||||||
|
dotnet pack --no-build -c Release -o ./nupkg
|
||||||
|
# 检查生成的 .nupkg 文件的元数据
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**项目现在已符合标准开源项目的要求!** 🎉
|
||||||
|
|
||||||
|
所有元数据已正确配置,文档完善,准备好发布和接受贡献。
|
||||||
@@ -1,287 +1,113 @@
|
|||||||
# sharp-basic
|
# Sharp-Basic Framework
|
||||||
|
|
||||||
C# 公共基础核心(模块化、多项目可复用)
|
[](https://gitea.duyl328.org/jimmy/sharp-basic)
|
||||||
|
[](LICENSE)
|
||||||
|
[](https://dotnet.microsoft.com/)
|
||||||
|
|
||||||
sharp-basic 旨在将常用基础能力拆分为可独立复用的模块(库项目),帮助业务项目即取即用,减少样板代码与重复实现。
|
一个基于 .NET 8 的现代化工业自动化与企业级应用开发套件,采用模块化架构设计,提供了从底层通信、基础设施到上层 UI 的完整解决方案。
|
||||||
|
|
||||||
当前仓库采用多项目结构,包含 Common、Config 等核心模块,以及 Logging、EventBus、NetClient/NetServer 等待实现或扩展的模块,同时提供 WPF/Client 示例项目便于演示与调试。
|
## 📋 项目信息
|
||||||
|
|
||||||
## 功能概览
|
- **版本**: 1.0.1
|
||||||
|
- **作者**: [duyl328](https://seyl.me/)
|
||||||
|
- **仓库**: [https://gitea.duyl328.org/jimmy/sharp-basic](https://gitea.duyl328.org/jimmy/sharp-basic)
|
||||||
|
- **许可证**: AGPL-3.0-or-later
|
||||||
|
|
||||||
- 通用工具(Sharp.Common)
|
## 🏗️ 架构概览
|
||||||
- 结果类型 Result/Result<T>:统一成功/失败表达、错误码/异常、Map/Bind/LINQ 风格链式操作、同步/异步 Try 包装
|
|
||||||
- 轻量服务容器 Global:线程安全注册与获取(Singleton/Transient)、实例变更通知(订阅发布)、批量管理
|
|
||||||
- 实用工具与扩展:JSON 序列化选项与 IPAddress 转换、端口探测、管理员权限检测、字符串解析等
|
|
||||||
- 配置中心(Sharp.Config)
|
|
||||||
- 统一构建器:支持 Json/Ini/Xml/Toml 多源叠加、可选/必选、基路径/文件提供器、环境变量、热重载
|
|
||||||
- 强类型绑定与验证:DataAnnotations 递归验证、TryGet/异常详情、对象保存与读取(多格式序列化)
|
|
||||||
- 扩展钩子:自定义 IConfigurationBuilder 管道(ConfigureBuilderHook)
|
|
||||||
- 日志(Sharp.Log)【规划中】
|
|
||||||
- 基于 Microsoft.Extensions.Logging 的统一抽象,适配 Console/File/Serilog/NLog,结构化日志与上下文
|
|
||||||
- 事件总线(Sharp.EventBus)【规划中】
|
|
||||||
- 进程内发布订阅、异步/顺序/粘性事件,支持 WPF 弱订阅;外部 Broker(Redis/RabbitMQ/Kafka)适配扩展
|
|
||||||
- 网络(Sharp.NetClient / Sharp.NetServer)【规划中】
|
|
||||||
- HttpClientFactory 封装、重试熔断超时、请求签名、统一 Result<T> 返回;最小 API Host 与中间件
|
|
||||||
- 桌面工具(Sharp.Wpf / Sharp.Client)
|
|
||||||
- WPF 示例/样板,用于演示配置热更、事件订阅、日志等集成的参考
|
|
||||||
|
|
||||||
## 目录结构
|
本项目包含以下核心模块,旨在实现高内聚、低耦合的开发体验:
|
||||||
|
|
||||||
```
|
### 🔌 核心基础设施
|
||||||
src/Sharp/
|
- **[Sharp.Common](Common/README.md)**: 基础工具库。提供轻量级 DI 容器 (`Global`)、函数式结果模式 (`Result<T>`)、以及丰富的扩展方法(String, DateTime, Encryption, Reflection)。
|
||||||
Common/ # 通用能力(Result、Global、Utils、扩展)
|
- **[Sharp.Config](Config/README.md)**: 强大的配置管理。支持 JSON/XML/INI/TOML 多源聚合、强类型绑定和数据注解验证。
|
||||||
Config/ # 配置集中管理与序列化
|
- **[Sharp.Log](Log/README.md)**: 统一日志门面。基于 Serilog 封装,支持一行代码接入,提供结构化日志和文件滚动策略。
|
||||||
Log/ # 日志抽象(规划中)
|
- **[Sharp.EventBus](EventBus/README.md)**: (开发中) 轻量级事件总线。
|
||||||
EventBus/ # 事件总线(规划中)
|
|
||||||
NetClient/ # 客户端网络(规划中)
|
### 💾 数据与存储
|
||||||
NetServer/ # 服务端网络(规划中)
|
- **[Sharp.Data](Data/README.md)**: 极简数据访问层。基于 EF Core,提供开箱即用的仓储模式、自动审计(创建人/时间)、软删除和分页支持。
|
||||||
Wpf/ # WPF 示例
|
- **[Sharp.Repository](Repository/README.md)**: 数据库集成模块。通过配置切换 SQL Server/MySQL/PostgreSQL/Sqlite,支持自动迁移。
|
||||||
Client/ # WPF 示例(另一个样板)
|
|
||||||
test/
|
### 🌐 网络与通信
|
||||||
Common.Test/ # Common 核心用例(MSTest)
|
- **[Sharp.NetServer](NetServer/README.md)**: 轻量级 Web 框架。基于 `HttpListener`,提供类似 ASP.NET Core 的 Controller/Route/Middleware 开发体验,适合嵌入式或桌面应用内嵌 Web 服务。
|
||||||
|
- **[Sharp.NetClient](NetClient/README.md)**: 强类型 HTTP 客户端。集成 Polly 重试策略、Bearer Token 认证和 multipart 上传。
|
||||||
|
- **[Sharp.Ports](Ports/README.md)**: 串口通信库。提供串口发现、连接管理、数据监听和诊断工具。
|
||||||
|
|
||||||
|
### 🏭 工业互联 (IIoT)
|
||||||
|
- **[Sharp.Plc](Plc/README.md)**: PLC 通信抽象层。定义了统一的读写、订阅接口。
|
||||||
|
- **[Sharp.Plc.Siemens](Plc.Siemens/README.md)**: 西门子驱动实现。支持 S7-200/300/400/1200/1500,内置智能批量读取优化算法(自动合并相邻地址请求)。
|
||||||
|
|
||||||
|
### 🖥️ 桌面应用 UI
|
||||||
|
- **[Sharp.Wpf](Wpf/README.md)**: WPF 基础库。提供 MVVM 基类、全局异常捕获、常用转换器、对话框服务和行为扩展。
|
||||||
|
- **[Sharp.Client](Client/README.md)**: 综合客户端示例。集成 PLC 监控、串口调试和用户登录功能的工业控制台应用。
|
||||||
|
- **[Sharp.Server](Server/README.md)**: 服务器控制台示例。展示了如何在一个 WPF 应用中托管 NetServer Web 服务,并提供可视化管理界面。
|
||||||
|
|
||||||
|
## 🚀 快速开始
|
||||||
|
|
||||||
|
### 环境要求
|
||||||
|
- .NET 8.0 SDK
|
||||||
|
- Visual Studio 2022 或 VS Code
|
||||||
|
|
||||||
|
### 构建项目
|
||||||
|
```bash
|
||||||
|
# 还原依赖
|
||||||
|
dotnet restore
|
||||||
|
|
||||||
|
# 编译解决方案
|
||||||
|
dotnet build Sharp.sln -c Debug
|
||||||
```
|
```
|
||||||
|
|
||||||
目标框架:.NET 8(net8.0 / net8.0-windows)。
|
### 运行示例
|
||||||
|
|
||||||
## 快速开始
|
**1. 运行 WPF 客户端:**
|
||||||
|
```bash
|
||||||
> 当前未发布 NuGet 包,可在解决方案内直接引用相关项目,或将模块作为子模块引入你的解决方案。
|
dotnet run --project Client/Client.csproj
|
||||||
|
|
||||||
### 使用 Result 统一返回
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
using Common.Results;
|
|
||||||
|
|
||||||
Result<int> r1 = Result.Success(123);
|
|
||||||
Result r2 = Result.Failure("something wrong");
|
|
||||||
|
|
||||||
// Try 模式(同步/异步)
|
|
||||||
var r3 = Result.Try(() => int.Parse("456"));
|
|
||||||
var r4 = await Result.TryAsync(async () => { await Task.Delay(10); return 789; });
|
|
||||||
|
|
||||||
// Map/Bind 链式管道
|
|
||||||
var r5 = r3.Map(x => x * 2).Where(v => v > 500, "too small");
|
|
||||||
|
|
||||||
// 提取值(或异常)
|
|
||||||
int value = r5.GetOrDefault(0); // 成功取值,否则默认
|
|
||||||
int must = r5.GetOrThrow(); // 失败抛出异常
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 使用 Global 注册与订阅
|
**2. 运行嵌入式 Web 服务器:**
|
||||||
|
```bash
|
||||||
```csharp
|
dotnet run --project Server/Server.csproj
|
||||||
using Common;
|
|
||||||
|
|
||||||
// 注册单例(实例/工厂),与瞬态
|
|
||||||
Global.RegisterSingleton(new AppConfig { AppName = "MyApp", Version = "1.0" });
|
|
||||||
Global.RegisterSingleton(() => new CacheService());
|
|
||||||
Global.RegisterTransient(() => new RequestContext());
|
|
||||||
|
|
||||||
// 订阅服务变更
|
|
||||||
Global.Subscribe<AppConfig>(e =>
|
|
||||||
{
|
|
||||||
Console.WriteLine($"Config changed: {e.OldValue?.Version} -> {e.NewValue.Version}");
|
|
||||||
});
|
|
||||||
|
|
||||||
// 更新将触发订阅回调
|
|
||||||
Global.Update(new AppConfig { AppName = "MyApp", Version = "1.1" });
|
|
||||||
|
|
||||||
// 获取
|
|
||||||
var config = Global.Get<AppConfig>();
|
|
||||||
```
|
```
|
||||||
|
|
||||||
示例类型(自定义)
|
**3. 运行 NetServer 纯控制台示例:**
|
||||||
|
```bash
|
||||||
```csharp
|
dotnet run --project NetServer/Examples
|
||||||
public sealed class AppConfig { public string AppName { get; set; } = ""; public string Version { get; set; } = ""; }
|
|
||||||
public sealed class CacheService { }
|
|
||||||
public sealed class RequestContext { public string RequestId { get; set; } = Guid.NewGuid().ToString(); }
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 配置管理(多源叠加/强类型/验证/保存)
|
## 🧪 测试
|
||||||
|
|
||||||
```csharp
|
项目包含完整的单元测试,位于解决方案同级的 `test` 目录中。
|
||||||
using Sharp.Config;
|
|
||||||
|
|
||||||
// 1) 从多个文件构建(支持 Json/Ini/Xml/Toml),并启用环境变量与热重载
|
|
||||||
var cfg = ConfigBuilder.Build(opt =>
|
|
||||||
{
|
|
||||||
opt.BasePath = AppContext.BaseDirectory;
|
|
||||||
opt.ReloadOnChange = true;
|
|
||||||
opt.IncludeEnvironmentVariables = true;
|
|
||||||
opt.EnvironmentPrefix = "MYAPP_"; // 可选
|
|
||||||
opt.Sources.Add(new ConfigSource { Path = "appsettings.json", Format = ConfigFormat.Json, Optional = false });
|
|
||||||
opt.Sources.Add(new ConfigSource { Path = "appsettings.Development.json", Format = ConfigFormat.Json, Optional = true });
|
|
||||||
});
|
|
||||||
|
|
||||||
// 2) 强类型绑定 + 验证(DataAnnotations)
|
|
||||||
var options = cfg.Get<MyOptions>(validate: true);
|
|
||||||
|
|
||||||
// 3) 热重载回调
|
|
||||||
using var _ = cfg.OnReload(() => Console.WriteLine("Configuration reloaded"));
|
|
||||||
|
|
||||||
// 4) 保存/读取对象(跨格式序列化)
|
|
||||||
cfg.Save(options, path: "out/appsettings.toml", format: ConfigFormat.Toml);
|
|
||||||
var reloaded = cfg.LoadFromFile<MyOptions>("out/appsettings.toml", ConfigFormat.Toml);
|
|
||||||
|
|
||||||
public sealed class MyOptions
|
|
||||||
{
|
|
||||||
[System.ComponentModel.DataAnnotations.Required]
|
|
||||||
public string Name { get; set; } = string.Empty;
|
|
||||||
public int Port { get; set; } = 8080;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
示例 appsettings.json
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "demo",
|
|
||||||
"port": 5000
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 测试
|
|
||||||
|
|
||||||
- 测试框架:MSTest(并行执行)。
|
|
||||||
- 运行:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
dotnet test
|
# 运行所有测试
|
||||||
|
dotnet test Sharp.sln
|
||||||
```
|
```
|
||||||
|
|
||||||
核心用例位于 `test/Common.Test`,覆盖 Result 与 Global 的主要功能与边界场景。
|
## 🤝 贡献指南
|
||||||
|
欢迎提交 Pull Request 或 Issue。请确保:
|
||||||
|
1. 代码风格符合 .NET 规范。
|
||||||
|
2. 新增功能包含对应的单元测试。
|
||||||
|
3. 提交信息简洁明了。
|
||||||
|
|
||||||
## WPF 工具包(Sharp.Wpf)
|
## 📄 许可证
|
||||||
|
|
||||||
提供即插即用的 WPF 常用基础能力:
|
本项目采用 [GNU Affero General Public License v3.0 或更高版本](LICENSE) (AGPL-3.0-or-later) 开源协议。
|
||||||
|
|
||||||
- 全局异常处理:统一捕获 UI/后台/未观察任务异常,支持弹窗与自定义日志
|
**关键要求**:
|
||||||
- MVVM 基础:`ViewModelBase`(INPC/IsBusy)、`RelayCommand`、`AsyncCommand`
|
- ✅ 可以自由使用、修改和分发
|
||||||
- Dispatcher 封装:`IDispatcher`/`DispatcherService`,提供 UI 线程调用统一入口
|
- ✅ 必须保持开源(包括网络服务)
|
||||||
- 对话与文件对话:`IDialogService`、`IFileDialogService`
|
- ✅ 修改后必须使用相同许可证
|
||||||
- 常用转换器:Bool/NullOrEmpty 可见性、布尔取反、枚举描述
|
- ⚠️ **网络服务条款**:如果您修改本软件并通过网络提供服务(如 Web API),必须向用户提供修改后的源代码
|
||||||
- 附加行为:EventToCommand(事件转 ICommand)
|
|
||||||
- 窗口工具:`WindowPlacement` 窗口位置/大小/状态持久化
|
|
||||||
|
|
||||||
### 引用
|
如需商业许可或有疑问,请联系作者。
|
||||||
|
|
||||||
在你的 WPF 项目中添加项目引用:
|
## 👤 作者
|
||||||
|
|
||||||
```xml
|
**duyl328**
|
||||||
<!-- Client/Client.csproj -->
|
- 网站: [https://seyl.me/](https://seyl.me/)
|
||||||
<ItemGroup>
|
- 仓库: [https://gitea.duyl328.org/jimmy/sharp-basic](https://gitea.duyl328.org/jimmy/sharp-basic)
|
||||||
<ProjectReference Include="..\Wpf\Wpf.csproj" />
|
|
||||||
<!-- 或通过 NuGet: <PackageReference Include="Sharp.Wpf" Version="x.y.z" /> -->
|
|
||||||
|
|
||||||
</ItemGroup>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 全局异常处理(App 启动)
|
查看 [AUTHORS](AUTHORS) 文件了解完整贡献者列表。
|
||||||
|
|
||||||
```csharp
|
---
|
||||||
// App.Startup.cs
|
|
||||||
using System.Windows;
|
|
||||||
using Sharp.Wpf.Exceptions;
|
|
||||||
|
|
||||||
public partial class App : Application
|
Copyright © 2024-2025 duyl328. All rights reserved.
|
||||||
{
|
|
||||||
protected override void OnStartup(StartupEventArgs e)
|
|
||||||
{
|
|
||||||
base.OnStartup(e);
|
|
||||||
new UiExceptionHandler().Register(new ExceptionHandlingOptions
|
|
||||||
{
|
|
||||||
ShowDialog = true,
|
|
||||||
DialogTitle = "应用程序错误"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### XAML 集成(窗口位置、行为、转换器)
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<Window x:Class="Client.MainWindow"
|
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:b="clr-namespace:Sharp.Wpf.Behaviors;assembly=Sharp.Wpf"
|
|
||||||
xmlns:w="clr-namespace:Sharp.Wpf.Windowing;assembly=Sharp.Wpf"
|
|
||||||
xmlns:conv="clr-namespace:Sharp.Wpf.Converters;assembly=Sharp.Wpf"
|
|
||||||
Title="MainWindow"
|
|
||||||
w:WindowPlacement.RememberPlacement="True"
|
|
||||||
w:WindowPlacement.PlacementId="MainWindow">
|
|
||||||
<Window.Resources>
|
|
||||||
<conv:BoolToVisibilityConverter x:Key="Bool2Vis" />
|
|
||||||
</Window.Resources>
|
|
||||||
<StackPanel Margin="16">
|
|
||||||
<Button Content="点击弹窗"
|
|
||||||
b:EventToCommand.EventName="Click"
|
|
||||||
b:EventToCommand.Command="{Binding ShowDialogCommand}" />
|
|
||||||
<Button Content="触发异常"
|
|
||||||
b:EventToCommand.EventName="Click"
|
|
||||||
b:EventToCommand.Command="{Binding ThrowCommand}" />
|
|
||||||
<TextBlock Text="正在处理..."
|
|
||||||
Visibility="{Binding IsBusy, Converter={StaticResource Bool2Vis}}"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Window>
|
|
||||||
```
|
|
||||||
|
|
||||||
### ViewModel 与命令
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
using System;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Sharp.Wpf.Commands;
|
|
||||||
using Sharp.Wpf.Mvvm;
|
|
||||||
using Sharp.Wpf.Services;
|
|
||||||
|
|
||||||
public sealed class MainViewModel : ViewModelBase
|
|
||||||
{
|
|
||||||
private readonly IDialogService _dialog = new DialogService();
|
|
||||||
|
|
||||||
public string Greeting { get; set; } = "欢迎使用 Sharp.Wpf";
|
|
||||||
public IAsyncCommand ShowDialogCommand { get; }
|
|
||||||
public RelayCommand ThrowCommand { get; }
|
|
||||||
|
|
||||||
public MainViewModel()
|
|
||||||
{
|
|
||||||
ShowDialogCommand = new AsyncCommand(async () =>
|
|
||||||
{
|
|
||||||
try { IsBusy = true; await Task.Delay(300); await _dialog.ShowInfoAsync("Hello!"); }
|
|
||||||
finally { IsBusy = false; }
|
|
||||||
});
|
|
||||||
|
|
||||||
ThrowCommand = new RelayCommand(() => throw new InvalidOperationException("演示未处理异常"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 文件对话
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
var files = await new FileDialogService().OpenFilesAsync("Images|*.png;*.jpg");
|
|
||||||
var folder = await new FileDialogService().SelectFolderAsync();
|
|
||||||
```
|
|
||||||
|
|
||||||
## 路线图(Roadmap)
|
|
||||||
|
|
||||||
- P0(最优先)
|
|
||||||
- Logging 抽象与 Serilog 适配;EventBus 进程内 Typed 事件;Config 对接 IOptions 与变更广播
|
|
||||||
- P1
|
|
||||||
- Net.Client:HttpClientFactory 封装 + Result<T> 模式;WPF Toolkit:MVVM/事件聚合/通知与主题
|
|
||||||
- P2
|
|
||||||
- 可靠性(Polly)、OpenTelemetry、外部 Broker 适配(Redis/RabbitMQ/Kafka)、敏感字段加密
|
|
||||||
- P3
|
|
||||||
- 文档/示例完善、CI/CD(自动打包发布)、更多单测与性能基准
|
|
||||||
|
|
||||||
## 贡献与规范
|
|
||||||
|
|
||||||
- 欢迎通过 Issue/PR 参与贡献。
|
|
||||||
- 代码风格:.NET 8、可空启用、简化 `using`,命名空间建议统一为 `Sharp.*`(Common 模块后续将统一为 `Sharp.Common`)。
|
|
||||||
- 变更需保持向后兼容(次版本),破坏式变更请在主版本发布并注明迁移指南。
|
|
||||||
|
|
||||||
## 许可
|
|
||||||
|
|
||||||
本项目采用 GNU AGPLv3 许可证,详见根目录 `LICENSE`。
|
|
||||||
|
|
||||||
如需在私有环境或商业场景中使用且不希望开源网络服务端改动代码,请联系作者洽谈其他授权方式。
|
|
||||||
@@ -0,0 +1,361 @@
|
|||||||
|
{
|
||||||
|
// ========================================
|
||||||
|
// Sharp-Basic 框架配置模板
|
||||||
|
//
|
||||||
|
// 这是一个完整的配置文件模板,包含了所有Sharp模块的配置示例
|
||||||
|
// 使用时可以根据项目需要选择性保留相关配置节
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// 系统配置 (System)
|
||||||
|
// 约定配置节名称:System
|
||||||
|
// 用于:NetClient、NetServer 的默认HTTP配置
|
||||||
|
// ========================================
|
||||||
|
"System": {
|
||||||
|
// HTTP服务监听IP地址
|
||||||
|
// 支持:IP地址、localhost、域名
|
||||||
|
"HTTPIp": "127.0.0.1",
|
||||||
|
|
||||||
|
// HTTP服务监听端口
|
||||||
|
// 范围:1-65535
|
||||||
|
"HttpPort": 8080,
|
||||||
|
|
||||||
|
// HTTP请求超时时间(秒)
|
||||||
|
// 范围:1-600
|
||||||
|
"TimeoutSeconds": 30,
|
||||||
|
|
||||||
|
// HTTP协议
|
||||||
|
// 可选值:http、https
|
||||||
|
"Scheme": "http"
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// 数据库配置 (Database)
|
||||||
|
// 约定配置节名称:Database
|
||||||
|
// 用于:Data 模块 - AddSharpDatabase<TContext>(configuration)
|
||||||
|
// ========================================
|
||||||
|
"Database": {
|
||||||
|
// 数据库提供程序
|
||||||
|
// 可选值:SqlServer、Sqlite、PostgreSQL、MySql、InMemory
|
||||||
|
"Provider": "SqlServer",
|
||||||
|
|
||||||
|
// 数据库连接字符串
|
||||||
|
// SqlServer示例:Server=.;Database=MyApp;Trusted_Connection=True;TrustServerCertificate=True;
|
||||||
|
// Sqlite示例:Data Source=app.db
|
||||||
|
// PostgreSQL示例:Host=localhost;Database=myapp;Username=postgres;Password=123456
|
||||||
|
// MySql示例:server=localhost;database=myapp;user=root;password=123456
|
||||||
|
"ConnectionString": "Server=.;Database=MyApp;Trusted_Connection=True;TrustServerCertificate=True;"
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// 日志配置 (Logging)
|
||||||
|
// 约定配置节名称:Logging
|
||||||
|
// 用于:Log 模块 - AddLogging(b => b.AddSharpLog())
|
||||||
|
// ========================================
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft": "Warning",
|
||||||
|
"Microsoft.Hosting.Lifetime": "Information"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Sharp日志扩展配置
|
||||||
|
"SharpLog": {
|
||||||
|
// 日志文件路径(相对或绝对路径)
|
||||||
|
"LogFilePath": "logs/app-.log",
|
||||||
|
|
||||||
|
// 日志滚动间隔
|
||||||
|
// 可选值:Infinite、Year、Month、Day、Hour、Minute
|
||||||
|
"RollingInterval": "Day",
|
||||||
|
|
||||||
|
// 单个日志文件最大大小(字节)
|
||||||
|
// null 表示不限制
|
||||||
|
"FileSizeLimitBytes": 10485760,
|
||||||
|
|
||||||
|
// 保留的日志文件数量
|
||||||
|
// null 表示不限制
|
||||||
|
"RetainedFileCountLimit": 31,
|
||||||
|
|
||||||
|
// 是否输出到控制台
|
||||||
|
"EnableConsole": true,
|
||||||
|
|
||||||
|
// 是否输出到文件
|
||||||
|
"EnableFile": true,
|
||||||
|
|
||||||
|
// 是否启用结构化日志(JSON格式)
|
||||||
|
"EnableStructuredLog": false
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// HTTP客户端配置 (NetClient)
|
||||||
|
// 约定配置节名称:NetClient
|
||||||
|
// 用于:NetClient 模块 - AddNetClient(configuration.GetSection("NetClient"))
|
||||||
|
// ========================================
|
||||||
|
"NetClient": {
|
||||||
|
// 基础地址(留空则使用System配置)
|
||||||
|
"BaseAddress": "http://localhost:8080",
|
||||||
|
|
||||||
|
// 请求超时时间
|
||||||
|
"Timeout": "00:00:30",
|
||||||
|
|
||||||
|
// 是否启用日志
|
||||||
|
"EnableLogging": true,
|
||||||
|
|
||||||
|
// 是否包含关联ID
|
||||||
|
"IncludeCorrelationId": true,
|
||||||
|
|
||||||
|
// 关联ID请求头名称
|
||||||
|
"CorrelationIdHeaderName": "X-Correlation-ID",
|
||||||
|
|
||||||
|
// 是否使用Bearer Token认证
|
||||||
|
"UseBearerToken": false,
|
||||||
|
|
||||||
|
// 重试配置
|
||||||
|
"Retry": {
|
||||||
|
// 是否启用重试
|
||||||
|
"Enabled": true,
|
||||||
|
|
||||||
|
// 最大重试次数
|
||||||
|
"MaxRetries": 3,
|
||||||
|
|
||||||
|
// 重试延迟(秒)
|
||||||
|
"DelaySeconds": 2
|
||||||
|
},
|
||||||
|
|
||||||
|
// 默认请求头
|
||||||
|
"DefaultRequestHeaders": {
|
||||||
|
"User-Agent": "Sharp-NetClient/1.0",
|
||||||
|
"Accept": "application/json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// HTTP服务端配置 (NetServer)
|
||||||
|
// 约定配置节名称:NetServer
|
||||||
|
// 用于:NetServer 模块 - AddNetServer(configManager)
|
||||||
|
// ========================================
|
||||||
|
"NetServer": {
|
||||||
|
// 监听URL列表(留空则使用System配置)
|
||||||
|
"ListenUrls": [
|
||||||
|
"http://localhost:8080/",
|
||||||
|
"http://0.0.0.0:8080/"
|
||||||
|
],
|
||||||
|
|
||||||
|
// 是否启用详细错误信息
|
||||||
|
"ShowDetailedErrors": true,
|
||||||
|
|
||||||
|
// 请求正文最大大小(字节)
|
||||||
|
"MaxRequestBodySize": 30000000,
|
||||||
|
|
||||||
|
// 请求超时时间
|
||||||
|
"RequestTimeout": "00:05:00"
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// PLC配置 - 西门子 (Plc:Siemens)
|
||||||
|
// 约定配置节名称:Plc:Siemens
|
||||||
|
// 用于:Plc.Siemens 模块 - AddSiemensPlc(configuration.GetSection("Plc:Siemens"))
|
||||||
|
// ========================================
|
||||||
|
"Plc": {
|
||||||
|
"Siemens": {
|
||||||
|
// PLC IP地址
|
||||||
|
"IpAddress": "192.168.1.100",
|
||||||
|
|
||||||
|
// CPU类型
|
||||||
|
// 可选值:S71200、S71500、S7300、S7400、S7200、S7200Smart
|
||||||
|
"CpuType": "S71200",
|
||||||
|
|
||||||
|
// 机架号(S7-300/400)
|
||||||
|
"Rack": 0,
|
||||||
|
|
||||||
|
// 插槽号(S7-300/400)
|
||||||
|
"Slot": 1,
|
||||||
|
|
||||||
|
// 端口号
|
||||||
|
"Port": 102,
|
||||||
|
|
||||||
|
// 连接超时时间(毫秒)
|
||||||
|
"ConnectTimeout": 5000,
|
||||||
|
|
||||||
|
// 读写超时时间(毫秒)
|
||||||
|
"ReadWriteTimeout": 3000,
|
||||||
|
|
||||||
|
// 监控轮询间隔(毫秒)
|
||||||
|
"MonitorInterval": 500,
|
||||||
|
|
||||||
|
// 是否自动重连
|
||||||
|
"AutoReconnect": true,
|
||||||
|
|
||||||
|
// 重连间隔(毫秒)
|
||||||
|
"ReconnectInterval": 5000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// 串口配置 (SerialPort)
|
||||||
|
// 约定配置节名称:SerialPort
|
||||||
|
// 用于:Ports 模块 - AddSerialPortService()
|
||||||
|
// ========================================
|
||||||
|
"SerialPort": {
|
||||||
|
// 端口名称(如 COM1、/dev/ttyUSB0)
|
||||||
|
"PortName": "COM1",
|
||||||
|
|
||||||
|
// 波特率
|
||||||
|
// 常用值:9600、19200、38400、57600、115200
|
||||||
|
"BaudRate": 9600,
|
||||||
|
|
||||||
|
// 数据位
|
||||||
|
// 可选值:5、6、7、8
|
||||||
|
"DataBits": 8,
|
||||||
|
|
||||||
|
// 停止位
|
||||||
|
// 可选值:None、One、Two、OnePointFive
|
||||||
|
"StopBits": "One",
|
||||||
|
|
||||||
|
// 校验位
|
||||||
|
// 可选值:None、Odd、Even、Mark、Space
|
||||||
|
"Parity": "None",
|
||||||
|
|
||||||
|
// 读取超时时间(毫秒)
|
||||||
|
// -1 表示无限等待
|
||||||
|
"ReadTimeout": 1000,
|
||||||
|
|
||||||
|
// 写入超时时间(毫秒)
|
||||||
|
// -1 表示无限等待
|
||||||
|
"WriteTimeout": 1000,
|
||||||
|
|
||||||
|
// 接收缓冲区大小(字节)
|
||||||
|
"ReadBufferSize": 4096,
|
||||||
|
|
||||||
|
// 发送缓冲区大小(字节)
|
||||||
|
"WriteBufferSize": 2048
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// 进程执行配置 (Process)
|
||||||
|
// 约定配置节名称:Process
|
||||||
|
// 用于:Process 模块 - AddSharpProcess()
|
||||||
|
// ========================================
|
||||||
|
"Process": {
|
||||||
|
// 默认工作目录
|
||||||
|
"WorkingDirectory": "",
|
||||||
|
|
||||||
|
// 默认超时时间(毫秒)
|
||||||
|
// -1 表示无限等待
|
||||||
|
"DefaultTimeout": 30000,
|
||||||
|
|
||||||
|
// 是否重定向标准输出
|
||||||
|
"RedirectStandardOutput": true,
|
||||||
|
|
||||||
|
// 是否重定向标准错误
|
||||||
|
"RedirectStandardError": true,
|
||||||
|
|
||||||
|
// 是否使用Shell执行
|
||||||
|
"UseShellExecute": false,
|
||||||
|
|
||||||
|
// 是否创建窗口
|
||||||
|
"CreateNoWindow": true
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// Python配置 (Python)
|
||||||
|
// 约定配置节名称:Python
|
||||||
|
// 用于:Python 模块 - AddSharpPython()
|
||||||
|
// ========================================
|
||||||
|
"Python": {
|
||||||
|
// Python解释器路径
|
||||||
|
// 留空则自动查找系统PATH中的python
|
||||||
|
"PythonPath": "",
|
||||||
|
|
||||||
|
// Python脚本根目录
|
||||||
|
"ScriptDirectory": "scripts",
|
||||||
|
|
||||||
|
// 默认超时时间(毫秒)
|
||||||
|
"DefaultTimeout": 60000,
|
||||||
|
|
||||||
|
// 是否启用虚拟环境
|
||||||
|
"UseVirtualEnv": false,
|
||||||
|
|
||||||
|
// 虚拟环境路径
|
||||||
|
"VirtualEnvPath": ""
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// 缓存配置 (Cache)
|
||||||
|
// 约定配置节名称:Cache
|
||||||
|
// 用于:Cache 模块 - AddSharpCache()
|
||||||
|
// ========================================
|
||||||
|
"Cache": {
|
||||||
|
// 默认过期时间(秒)
|
||||||
|
"DefaultExpirationSeconds": 600,
|
||||||
|
|
||||||
|
// 缓存大小限制(条目数)
|
||||||
|
// null 表示不限制
|
||||||
|
"SizeLimit": null,
|
||||||
|
|
||||||
|
// 内存压缩比例(0.0-1.0)
|
||||||
|
"CompactionPercentage": 0.25,
|
||||||
|
|
||||||
|
// 过期扫描频率(秒)
|
||||||
|
"ExpirationScanFrequency": 60
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// 事件总线配置 (EventBus)
|
||||||
|
// 约定配置节名称:EventBus
|
||||||
|
// 用于:EventBus 模块 - AddEventBus()
|
||||||
|
// ========================================
|
||||||
|
"EventBus": {
|
||||||
|
// 是否启用异步处理
|
||||||
|
"EnableAsync": true,
|
||||||
|
|
||||||
|
// 异步事件队列容量
|
||||||
|
"QueueCapacity": 1000,
|
||||||
|
|
||||||
|
// 是否在异常时继续执行其他订阅者
|
||||||
|
"ContinueOnError": true,
|
||||||
|
|
||||||
|
// 是否启用死信队列
|
||||||
|
"EnableDeadLetterQueue": false,
|
||||||
|
|
||||||
|
// 死信队列容量
|
||||||
|
"DeadLetterQueueCapacity": 100
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// WPF配置 (Wpf)
|
||||||
|
// 约定配置节名称:Wpf
|
||||||
|
// 用于:Wpf 模块 - AddSharpWpf()
|
||||||
|
// ========================================
|
||||||
|
"Wpf": {
|
||||||
|
// 默认对话框标题
|
||||||
|
"DefaultDialogTitle": "提示",
|
||||||
|
|
||||||
|
// 是否启用全局异常处理
|
||||||
|
"EnableGlobalExceptionHandler": true,
|
||||||
|
|
||||||
|
// 主题
|
||||||
|
// 可选值:Light、Dark
|
||||||
|
"Theme": "Light",
|
||||||
|
|
||||||
|
// 语言
|
||||||
|
// 可选值:zh-CN、en-US
|
||||||
|
"Language": "zh-CN"
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// 环境变量覆盖说明
|
||||||
|
// ========================================
|
||||||
|
// 所有配置都可以通过环境变量覆盖,格式:
|
||||||
|
// SHARP__{节名}__{键名}
|
||||||
|
//
|
||||||
|
// 示例:
|
||||||
|
// SHARP__Database__Provider=Sqlite
|
||||||
|
// SHARP__Database__ConnectionString=Data Source=test.db
|
||||||
|
// SHARP__Plc__Siemens__IpAddress=192.168.1.200
|
||||||
|
// SHARP__System__HttpPort=9090
|
||||||
|
//
|
||||||
|
// 注意:双下划线 __ 用于分隔层级
|
||||||
|
// ========================================
|
||||||
|
}
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
# 01 - 最小 WPF 应用
|
||||||
|
|
||||||
|
这是一个最简单的 Sharp WPF 应用示例,演示了框架的基础用法。
|
||||||
|
|
||||||
|
## 🎯 学习目标
|
||||||
|
|
||||||
|
- 了解 Sharp 框架的基本结构
|
||||||
|
- 掌握 MVVM 模式(ViewModelBase)
|
||||||
|
- 使用 Commands(RelayCommand, AsyncCommand)
|
||||||
|
- 使用对话框服务(IDialogService)
|
||||||
|
- 理解 Quick Start 配置方式
|
||||||
|
|
||||||
|
## 📦 使用的模块
|
||||||
|
|
||||||
|
- **Sharp.Common** - Result<T>, 基础工具
|
||||||
|
- **Sharp.Config** - 配置管理
|
||||||
|
- **Sharp.Wpf** - MVVM 基础设施
|
||||||
|
|
||||||
|
## 🏗️ 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
01-MinimalWpfApp/
|
||||||
|
├── App.xaml # 应用程序入口
|
||||||
|
├── App.xaml.cs # Quick Start 配置
|
||||||
|
├── MainWindow.xaml # 主窗口界面
|
||||||
|
├── MainWindow.xaml.cs # 主窗口代码隐藏
|
||||||
|
├── ViewModels/
|
||||||
|
│ └── MainViewModel.cs # 主窗口 ViewModel
|
||||||
|
├── appsettings.json # 配置文件
|
||||||
|
└── README.md # 本文件
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 快速运行
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd examples/01-MinimalWpfApp
|
||||||
|
dotnet run
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 核心代码讲解
|
||||||
|
|
||||||
|
### 1. App.xaml.cs - 应用启动
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
protected override void OnStartup(StartupEventArgs e)
|
||||||
|
{
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
|
||||||
|
// 一行添加基础服务(配置、日志、缓存)
|
||||||
|
services.AddSharpEssentials();
|
||||||
|
|
||||||
|
// 一行添加 WPF 服务(对话框、调度器、导航、主题)
|
||||||
|
services.AddSharpWpf();
|
||||||
|
|
||||||
|
// 注册 ViewModel 和 Window
|
||||||
|
services.AddViewModel<MainViewModel>();
|
||||||
|
services.AddTransient<MainWindow>();
|
||||||
|
|
||||||
|
// 构建服务提供者
|
||||||
|
_serviceProvider = services.BuildServiceProvider();
|
||||||
|
|
||||||
|
// 显示主窗口
|
||||||
|
var mainWindow = _serviceProvider.GetRequiredService<MainWindow>();
|
||||||
|
mainWindow.Show();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键点**:
|
||||||
|
- `AddSharpEssentials()` - 自动配置 Config + Log + Cache
|
||||||
|
- `AddSharpWpf()` - 自动注册 WPF 服务
|
||||||
|
- 无需手动配置,使用约定优于配置
|
||||||
|
|
||||||
|
### 2. MainViewModel.cs - ViewModel 示例
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class MainViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
private readonly IDialogService _dialogService;
|
||||||
|
private string _message = "Hello, Sharp!";
|
||||||
|
private int _counter = 0;
|
||||||
|
|
||||||
|
public string Message
|
||||||
|
{
|
||||||
|
get => _message;
|
||||||
|
set => SetProperty(ref _message, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Counter
|
||||||
|
{
|
||||||
|
get => _counter;
|
||||||
|
set => SetProperty(ref _counter, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 同步命令
|
||||||
|
public ICommand IncrementCommand { get; }
|
||||||
|
|
||||||
|
// 异步命令
|
||||||
|
public ICommand ShowDialogCommand { get; }
|
||||||
|
|
||||||
|
public MainViewModel(IDialogService dialogService)
|
||||||
|
{
|
||||||
|
_dialogService = dialogService;
|
||||||
|
|
||||||
|
// RelayCommand - 同步操作
|
||||||
|
IncrementCommand = new RelayCommand(() => {
|
||||||
|
Counter++;
|
||||||
|
Message = $"你点击了 {Counter} 次";
|
||||||
|
});
|
||||||
|
|
||||||
|
// AsyncCommand - 异步操作
|
||||||
|
ShowDialogCommand = new AsyncCommand(async (ct) => {
|
||||||
|
IsBusy = true; // 显示加载状态
|
||||||
|
|
||||||
|
// 模拟耗时操作
|
||||||
|
await Task.Delay(1000, ct);
|
||||||
|
|
||||||
|
_dialogService.ShowMessage($"当前计数: {Counter}", "提示");
|
||||||
|
|
||||||
|
IsBusy = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键点**:
|
||||||
|
- 继承 `ViewModelBase` 获得 INotifyPropertyChanged
|
||||||
|
- 使用 `SetProperty()` 自动触发属性变化通知
|
||||||
|
- `IsBusy` 属性用于显示加载状态
|
||||||
|
- `RelayCommand` 用于同步操作
|
||||||
|
- `AsyncCommand` 用于异步操作(支持取消)
|
||||||
|
|
||||||
|
### 3. MainWindow.xaml - 界面绑定
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<Window ...>
|
||||||
|
<Window.DataContext>
|
||||||
|
<!-- ViewModel 会通过 DI 自动注入 -->
|
||||||
|
</Window.DataContext>
|
||||||
|
|
||||||
|
<StackPanel Margin="20">
|
||||||
|
<!-- 绑定属性 -->
|
||||||
|
<TextBlock Text="{Binding Message}" FontSize="24"/>
|
||||||
|
|
||||||
|
<!-- 绑定命令 -->
|
||||||
|
<Button Content="点击我" Command="{Binding IncrementCommand}"/>
|
||||||
|
|
||||||
|
<!-- 异步命令 -->
|
||||||
|
<Button Content="显示对话框" Command="{Binding ShowDialogCommand}"/>
|
||||||
|
|
||||||
|
<!-- 显示加载状态 -->
|
||||||
|
<TextBlock Text="加载中..." Visibility="{Binding IsBusy, Converter={StaticResource BoolToVisibilityConverter}}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Window>
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键点**:
|
||||||
|
- 使用 `{Binding}` 绑定 ViewModel 属性
|
||||||
|
- Command 绑定到 ICommand 属性
|
||||||
|
- 使用内置转换器(BoolToVisibilityConverter)
|
||||||
|
|
||||||
|
## 🎓 练习建议
|
||||||
|
|
||||||
|
### 练习1:添加新属性
|
||||||
|
在 MainViewModel 中添加一个 `Name` 属性,并在界面上显示。
|
||||||
|
|
||||||
|
### 练习2:添加清零按钮
|
||||||
|
添加一个 `ResetCommand`,点击后将 `Counter` 重置为 0。
|
||||||
|
|
||||||
|
### 练习3:使用对话框确认
|
||||||
|
在 ResetCommand 中,先弹出确认对话框,用户确认后再清零。
|
||||||
|
|
||||||
|
### 练习4:尝试 Result<T>
|
||||||
|
修改 ShowDialogCommand,使用 `Result.Try()` 包装可能出错的代码。
|
||||||
|
|
||||||
|
## 📚 延伸阅读
|
||||||
|
|
||||||
|
- [Sharp.Wpf README](../../src/Sharp/Wpf/README.md) - WPF 模块完整文档
|
||||||
|
- [QUICKSTART.md](../../src/Sharp/QUICKSTART.md) - 快速启动指南
|
||||||
|
- 下一个示例:[02-WpfWithDatabase](../02-WpfWithDatabase/README.md)
|
||||||
|
|
||||||
|
## ❓ 常见问题
|
||||||
|
|
||||||
|
**Q: 为什么 MainWindow 不需要手动设置 DataContext?**
|
||||||
|
A: 通过 DI 容器获取 Window 时,会自动注入对应的 ViewModel。
|
||||||
|
|
||||||
|
**Q: IsBusy 是在哪里定义的?**
|
||||||
|
A: `IsBusy` 是 `ViewModelBase` 的内置属性,用于显示加载状态。
|
||||||
|
|
||||||
|
**Q: 可以不用 DI 吗?**
|
||||||
|
A: 可以,但不推荐。使用 DI 可以更好地管理依赖关系,便于测试。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**开始编码吧!** 🚀
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
# 03 - 表单验证示例
|
||||||
|
|
||||||
|
演示如何使用 Sharp.Wpf 的表单验证功能,包括 DataAnnotations 验证、实时错误提示和自定义验证。
|
||||||
|
|
||||||
|
## 🎯 学习目标
|
||||||
|
|
||||||
|
- 使用 `ValidatableViewModelBase` 实现表单验证
|
||||||
|
- 掌握 DataAnnotations 验证特性
|
||||||
|
- 实现实时验证反馈
|
||||||
|
- 自定义验证规则
|
||||||
|
- 显示验证错误消息
|
||||||
|
|
||||||
|
## 📦 使用的模块
|
||||||
|
|
||||||
|
- **Sharp.Wpf** - ValidatableViewModelBase, 验证支持
|
||||||
|
|
||||||
|
## 🚀 快速运行
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd examples/03-FormValidation
|
||||||
|
dotnet run
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 核心代码讲解
|
||||||
|
|
||||||
|
### 1. UserViewModel.cs - 带验证的 ViewModel
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Wpf.Validation;
|
||||||
|
|
||||||
|
public class UserViewModel : ValidatableViewModelBase
|
||||||
|
{
|
||||||
|
private string _username = "";
|
||||||
|
private string _email = "";
|
||||||
|
private string _password = "";
|
||||||
|
private string _confirmPassword = "";
|
||||||
|
private int _age;
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "用户名不能为空")]
|
||||||
|
[StringLength(20, MinimumLength = 3, ErrorMessage = "用户名长度必须在3-20个字符之间")]
|
||||||
|
public string Username
|
||||||
|
{
|
||||||
|
get => _username;
|
||||||
|
set => SetProperty(ref _username, value, validate: true); // 启用验证
|
||||||
|
}
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "邮箱不能为空")]
|
||||||
|
[EmailAddress(ErrorMessage = "邮箱格式不正确")]
|
||||||
|
public string Email
|
||||||
|
{
|
||||||
|
get => _email;
|
||||||
|
set => SetProperty(ref _email, value, validate: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "密码不能为空")]
|
||||||
|
[StringLength(50, MinimumLength = 6, ErrorMessage = "密码长度必须在6-50个字符之间")]
|
||||||
|
[RegularExpression(@"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$",
|
||||||
|
ErrorMessage = "密码必须包含大小写字母和数字")]
|
||||||
|
public string Password
|
||||||
|
{
|
||||||
|
get => _password;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
SetProperty(ref _password, value, validate: true);
|
||||||
|
// 密码变化时,重新验证确认密码
|
||||||
|
ValidateProperty(ConfirmPassword, nameof(ConfirmPassword));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "请确认密码")]
|
||||||
|
public string ConfirmPassword
|
||||||
|
{
|
||||||
|
get => _confirmPassword;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
SetProperty(ref _confirmPassword, value, validate: false);
|
||||||
|
// 自定义验证:确认密码必须与密码一致
|
||||||
|
if (value != Password)
|
||||||
|
{
|
||||||
|
AddError(nameof(ConfirmPassword), "两次密码输入不一致");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ClearErrors(nameof(ConfirmPassword));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Range(18, 120, ErrorMessage = "年龄必须在18-120之间")]
|
||||||
|
public int Age
|
||||||
|
{
|
||||||
|
get => _age;
|
||||||
|
set => SetProperty(ref _age, value, validate: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交命令
|
||||||
|
public ICommand SubmitCommand { get; }
|
||||||
|
|
||||||
|
public UserViewModel(IDialogService dialogService)
|
||||||
|
{
|
||||||
|
SubmitCommand = new RelayCommand(() =>
|
||||||
|
{
|
||||||
|
// 验证所有属性
|
||||||
|
if (ValidateAll())
|
||||||
|
{
|
||||||
|
dialogService.ShowMessage(
|
||||||
|
$"注册成功!\n用户名: {Username}\n邮箱: {Email}\n年龄: {Age}",
|
||||||
|
"成功");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
dialogService.ShowMessage("请修正表单错误后再提交", "验证失败");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. MainWindow.xaml - 验证错误显示
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<Window x:Class="FormValidationExample.MainWindow"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
Title="表单验证示例" Height="500" Width="400">
|
||||||
|
<Window.Resources>
|
||||||
|
<!-- 错误模板 -->
|
||||||
|
<ControlTemplate x:Key="ValidationErrorTemplate">
|
||||||
|
<DockPanel>
|
||||||
|
<Border BorderBrush="Red" BorderThickness="1" CornerRadius="2">
|
||||||
|
<AdornedElementPlaceholder/>
|
||||||
|
</Border>
|
||||||
|
<TextBlock Foreground="Red" FontSize="12" Margin="5,0,0,0"
|
||||||
|
Text="{Binding [0].ErrorContent}"
|
||||||
|
DockPanel.Dock="Right"/>
|
||||||
|
</DockPanel>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Window.Resources>
|
||||||
|
|
||||||
|
<StackPanel Margin="20">
|
||||||
|
<TextBlock Text="用户注册表单" FontSize="24" FontWeight="Bold" Margin="0,0,0,20"/>
|
||||||
|
|
||||||
|
<!-- 用户名 -->
|
||||||
|
<TextBlock Text="用户名:"/>
|
||||||
|
<TextBox Text="{Binding Username, UpdateSourceTrigger=PropertyChanged, ValidatesOnNotifyDataErrors=True}"
|
||||||
|
Validation.ErrorTemplate="{StaticResource ValidationErrorTemplate}"
|
||||||
|
Margin="0,5,0,15"/>
|
||||||
|
|
||||||
|
<!-- 邮箱 -->
|
||||||
|
<TextBlock Text="邮箱:"/>
|
||||||
|
<TextBox Text="{Binding Email, UpdateSourceTrigger=PropertyChanged, ValidatesOnNotifyDataErrors=True}"
|
||||||
|
Validation.ErrorTemplate="{StaticResource ValidationErrorTemplate}"
|
||||||
|
Margin="0,5,0,15"/>
|
||||||
|
|
||||||
|
<!-- 密码 -->
|
||||||
|
<TextBlock Text="密码:"/>
|
||||||
|
<PasswordBox x:Name="PasswordBox" Margin="0,5,0,5"/>
|
||||||
|
<TextBlock Text="* 密码必须包含大小写字母和数字" FontSize="10" Foreground="Gray" Margin="0,0,0,15"/>
|
||||||
|
|
||||||
|
<!-- 确认密码 -->
|
||||||
|
<TextBlock Text="确认密码:"/>
|
||||||
|
<PasswordBox x:Name="ConfirmPasswordBox" Margin="0,5,0,15"/>
|
||||||
|
|
||||||
|
<!-- 年龄 -->
|
||||||
|
<TextBlock Text="年龄:"/>
|
||||||
|
<TextBox Text="{Binding Age, UpdateSourceTrigger=PropertyChanged, ValidatesOnNotifyDataErrors=True}"
|
||||||
|
Validation.ErrorTemplate="{StaticResource ValidationErrorTemplate}"
|
||||||
|
Margin="0,5,0,20"/>
|
||||||
|
|
||||||
|
<!-- 提交按钮 -->
|
||||||
|
<Button Content="提交注册" Command="{Binding SubmitCommand}"
|
||||||
|
Height="40" FontSize="16"/>
|
||||||
|
|
||||||
|
<!-- 验证状态 -->
|
||||||
|
<TextBlock Margin="0,10,0,0" Foreground="Red"
|
||||||
|
Text="表单包含错误,请修正后提交"
|
||||||
|
Visibility="{Binding HasErrors, Converter={StaticResource BoolToVisibilityConverter}}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Window>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 关键点说明
|
||||||
|
|
||||||
|
**ValidatableViewModelBase 的关键特性**:
|
||||||
|
1. **实时验证** - `SetProperty(ref field, value, validate: true)`
|
||||||
|
2. **验证所有属性** - `ValidateAll()` 返回 bool
|
||||||
|
3. **自定义错误** - `AddError(propertyName, errorMessage)`
|
||||||
|
4. **清除错误** - `ClearErrors(propertyName)`
|
||||||
|
5. **错误状态** - `HasErrors` 属性
|
||||||
|
|
||||||
|
**XAML 绑定要点**:
|
||||||
|
1. **UpdateSourceTrigger=PropertyChanged** - 实时触发验证
|
||||||
|
2. **ValidatesOnNotifyDataErrors=True** - 启用错误显示
|
||||||
|
3. **Validation.ErrorTemplate** - 自定义错误样式
|
||||||
|
|
||||||
|
## 🎓 练习建议
|
||||||
|
|
||||||
|
### 练习1:添加更多验证规则
|
||||||
|
添加电话号码字段,使用正则表达式验证手机号格式。
|
||||||
|
|
||||||
|
### 练习2:异步验证
|
||||||
|
实现用户名唯一性检查(模拟调用 API)。
|
||||||
|
|
||||||
|
### 练习3:条件验证
|
||||||
|
当用户勾选"我是学生"时,年龄必须小于25岁。
|
||||||
|
|
||||||
|
### 练习4:自定义验证特性
|
||||||
|
创建 `UniqueUsernameAttribute` 自定义验证特性。
|
||||||
|
|
||||||
|
## 💡 常见问题
|
||||||
|
|
||||||
|
**Q: PasswordBox 无法绑定,怎么办?**
|
||||||
|
A: PasswordBox 的 Password 属性不是依赖属性,不能直接绑定。可以使用附加属性或在代码隐藏中处理。
|
||||||
|
|
||||||
|
**Q: 为什么有时验证不触发?**
|
||||||
|
A: 确保:
|
||||||
|
1. 绑定使用了 `UpdateSourceTrigger=PropertyChanged`
|
||||||
|
2. SetProperty 的 validate 参数设为 true
|
||||||
|
3. XAML 中设置了 `ValidatesOnNotifyDataErrors=True`
|
||||||
|
|
||||||
|
**Q: 如何自定义错误消息样式?**
|
||||||
|
A: 修改 `Validation.ErrorTemplate`,可以自定义边框颜色、图标等。
|
||||||
|
|
||||||
|
## 📚 延伸阅读
|
||||||
|
|
||||||
|
- [DataAnnotations 验证特性](https://learn.microsoft.com/zh-cn/dotnet/api/system.componentmodel.dataannotations)
|
||||||
|
- [INotifyDataErrorInfo 接口](https://learn.microsoft.com/zh-cn/dotnet/api/system.componentmodel.inotifydataerrorinfo)
|
||||||
|
- 下一个示例:[04-Navigation](../04-Navigation/README.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**掌握表单验证,构建高质量的用户输入体验!** ✅
|
||||||
@@ -0,0 +1,315 @@
|
|||||||
|
# 04 - 导航系统示例
|
||||||
|
|
||||||
|
演示如何使用 Sharp.Wpf 的导航服务实现多页面 WPF 应用,包括页面跳转、参数传递、前进/后退等功能。
|
||||||
|
|
||||||
|
## 🎯 学习目标
|
||||||
|
|
||||||
|
- 使用 `INavigationService` 实现页面导航
|
||||||
|
- 理解导航历史栈(前进/后退)
|
||||||
|
- 页面间参数传递
|
||||||
|
- 导航生命周期钩子(INavigationAware)
|
||||||
|
- 弹出式导航并获取结果
|
||||||
|
|
||||||
|
## 📦 使用的模块
|
||||||
|
|
||||||
|
- **Sharp.Wpf** - INavigationService, NavigationService
|
||||||
|
|
||||||
|
## 🚀 快速运行
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd examples/04-Navigation
|
||||||
|
dotnet run
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 核心代码讲解
|
||||||
|
|
||||||
|
### 1. MainViewModel.cs - 主导航容器
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Wpf.Navigation;
|
||||||
|
|
||||||
|
public class MainViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
private readonly INavigationService _navigationService;
|
||||||
|
private ViewModelBase? _currentPage;
|
||||||
|
|
||||||
|
public ViewModelBase? CurrentPage
|
||||||
|
{
|
||||||
|
get => _currentPage;
|
||||||
|
set => SetProperty(ref _currentPage, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CanGoBack => _navigationService.CanGoBack;
|
||||||
|
public bool CanGoForward => _navigationService.CanGoForward;
|
||||||
|
|
||||||
|
public ICommand NavigateToHomeCommand { get; }
|
||||||
|
public ICommand NavigateToProductsCommand { get; }
|
||||||
|
public ICommand NavigateToSettingsCommand { get; }
|
||||||
|
public ICommand GoBackCommand { get; }
|
||||||
|
public ICommand GoForwardCommand { get; }
|
||||||
|
|
||||||
|
public MainViewModel(INavigationService navigationService)
|
||||||
|
{
|
||||||
|
_navigationService = navigationService;
|
||||||
|
|
||||||
|
// 监听导航事件
|
||||||
|
_navigationService.Navigated += (s, vm) =>
|
||||||
|
{
|
||||||
|
CurrentPage = vm;
|
||||||
|
OnPropertyChanged(nameof(CanGoBack));
|
||||||
|
OnPropertyChanged(nameof(CanGoForward));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 导航命令
|
||||||
|
NavigateToHomeCommand = new AsyncCommand(async (ct) =>
|
||||||
|
{
|
||||||
|
await _navigationService.NavigateAsync<HomeViewModel>();
|
||||||
|
});
|
||||||
|
|
||||||
|
NavigateToProductsCommand = new AsyncCommand(async (ct) =>
|
||||||
|
{
|
||||||
|
await _navigationService.NavigateAsync<ProductsViewModel>();
|
||||||
|
});
|
||||||
|
|
||||||
|
NavigateToSettingsCommand = new AsyncCommand(async (ct) =>
|
||||||
|
{
|
||||||
|
await _navigationService.NavigateAsync<SettingsViewModel>();
|
||||||
|
});
|
||||||
|
|
||||||
|
GoBackCommand = new AsyncCommand(async (ct) =>
|
||||||
|
{
|
||||||
|
await _navigationService.GoBackAsync();
|
||||||
|
}, () => CanGoBack);
|
||||||
|
|
||||||
|
GoForwardCommand = new AsyncCommand(async (ct) =>
|
||||||
|
{
|
||||||
|
await _navigationService.GoForwardAsync();
|
||||||
|
}, () => CanGoForward);
|
||||||
|
|
||||||
|
// 初始导航到首页
|
||||||
|
Task.Run(async () => await _navigationService.NavigateAsync<HomeViewModel>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. ProductsViewModel.cs - 带参数的页面
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class ProductsViewModel : ViewModelBase, INavigationAware
|
||||||
|
{
|
||||||
|
private readonly INavigationService _navigationService;
|
||||||
|
private readonly IDialogService _dialogService;
|
||||||
|
private string _category = "全部";
|
||||||
|
|
||||||
|
public string Category
|
||||||
|
{
|
||||||
|
get => _category;
|
||||||
|
set => SetProperty(ref _category, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ICommand ViewProductDetailsCommand { get; }
|
||||||
|
|
||||||
|
public ProductsViewModel(
|
||||||
|
INavigationService navigationService,
|
||||||
|
IDialogService dialogService)
|
||||||
|
{
|
||||||
|
_navigationService = navigationService;
|
||||||
|
_dialogService = dialogService;
|
||||||
|
|
||||||
|
ViewProductDetailsCommand = new AsyncCommand<int>(async (productId, ct) =>
|
||||||
|
{
|
||||||
|
// 导航到产品详情页,传递产品ID
|
||||||
|
await _navigationService.NavigateAsync<ProductDetailViewModel>(productId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导航生命周期:进入页面时
|
||||||
|
public async Task OnNavigatedToAsync(object? parameter)
|
||||||
|
{
|
||||||
|
if (parameter is string category)
|
||||||
|
{
|
||||||
|
Category = category;
|
||||||
|
_dialogService.ShowMessage($"显示分类: {category}", "提示");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 可以在这里加载数据
|
||||||
|
await LoadProductsAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导航生命周期:离开页面时
|
||||||
|
public async Task OnNavigatedFromAsync()
|
||||||
|
{
|
||||||
|
// 清理资源、保存状态等
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadProductsAsync()
|
||||||
|
{
|
||||||
|
IsBusy = true;
|
||||||
|
await Task.Delay(500); // 模拟加载
|
||||||
|
IsBusy = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. ProductDetailViewModel.cs - 详情页(带结果返回)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class ProductDetailViewModel : ViewModelBase,
|
||||||
|
INavigationAware,
|
||||||
|
INavigationResult<bool> // 返回 bool(是否购买)
|
||||||
|
{
|
||||||
|
private readonly TaskCompletionSource<bool> _resultSource = new();
|
||||||
|
private int _productId;
|
||||||
|
private string _productName = "";
|
||||||
|
|
||||||
|
public int ProductId
|
||||||
|
{
|
||||||
|
get => _productId;
|
||||||
|
set => SetProperty(ref _productId, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ProductName
|
||||||
|
{
|
||||||
|
get => _productName;
|
||||||
|
set => SetProperty(ref _productName, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ICommand BuyCommand { get; }
|
||||||
|
public ICommand CancelCommand { get; }
|
||||||
|
|
||||||
|
public ProductDetailViewModel()
|
||||||
|
{
|
||||||
|
BuyCommand = new RelayCommand(() =>
|
||||||
|
{
|
||||||
|
// 返回结果:已购买
|
||||||
|
_resultSource.SetResult(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
CancelCommand = new RelayCommand(() =>
|
||||||
|
{
|
||||||
|
// 返回结果:取消
|
||||||
|
_resultSource.SetResult(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task OnNavigatedToAsync(object? parameter)
|
||||||
|
{
|
||||||
|
if (parameter is int productId)
|
||||||
|
{
|
||||||
|
ProductId = productId;
|
||||||
|
// 加载产品详情
|
||||||
|
ProductName = $"产品 #{productId}";
|
||||||
|
}
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task OnNavigatedFromAsync() => Task.CompletedTask;
|
||||||
|
|
||||||
|
// 实现 INavigationResult<bool>
|
||||||
|
public Task<bool> GetResultAsync()
|
||||||
|
{
|
||||||
|
return _resultSource.Task;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. App.xaml.cs - 注册导航服务
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
protected override void OnStartup(StartupEventArgs e)
|
||||||
|
{
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
|
||||||
|
services.AddSharpWpf(); // 自动注册 INavigationService
|
||||||
|
|
||||||
|
// 注册所有页面 ViewModel
|
||||||
|
services.AddViewModel<MainViewModel>();
|
||||||
|
services.AddViewModel<HomeViewModel>();
|
||||||
|
services.AddViewModel<ProductsViewModel>();
|
||||||
|
services.AddViewModel<ProductDetailViewModel>();
|
||||||
|
services.AddViewModel<SettingsViewModel>();
|
||||||
|
|
||||||
|
var provider = services.BuildServiceProvider();
|
||||||
|
var mainWindow = provider.GetRequiredService<MainWindow>();
|
||||||
|
mainWindow.Show();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. MainWindow.xaml - 导航容器
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<Window x:Class="NavigationExample.MainWindow"
|
||||||
|
Title="导航系统示例" Height="600" Width="800">
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- 导航栏 -->
|
||||||
|
<StackPanel Grid.Row="0" Orientation="Horizontal" Background="#f0f0f0" Padding="10">
|
||||||
|
<Button Content="首页" Command="{Binding NavigateToHomeCommand}" Margin="0,0,10,0"/>
|
||||||
|
<Button Content="产品列表" Command="{Binding NavigateToProductsCommand}" Margin="0,0,10,0"/>
|
||||||
|
<Button Content="设置" Command="{Binding NavigateToSettingsCommand}" Margin="0,0,20,0"/>
|
||||||
|
|
||||||
|
<Button Content="← 后退" Command="{Binding GoBackCommand}" Margin="0,0,10,0"/>
|
||||||
|
<Button Content="前进 →" Command="{Binding GoForwardCommand}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- 内容区域 - 显示当前页面 -->
|
||||||
|
<ContentControl Grid.Row="1" Content="{Binding CurrentPage}">
|
||||||
|
<ContentControl.Resources>
|
||||||
|
<!-- 为每个 ViewModel 指定对应的 View -->
|
||||||
|
<DataTemplate DataType="{x:Type local:HomeViewModel}">
|
||||||
|
<local:HomeView/>
|
||||||
|
</DataTemplate>
|
||||||
|
<DataTemplate DataType="{x:Type local:ProductsViewModel}">
|
||||||
|
<local:ProductsView/>
|
||||||
|
</DataTemplate>
|
||||||
|
<DataTemplate DataType="{x:Type local:SettingsViewModel}">
|
||||||
|
<local:SettingsView/>
|
||||||
|
</DataTemplate>
|
||||||
|
</ContentControl.Resources>
|
||||||
|
</ContentControl>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎓 练习建议
|
||||||
|
|
||||||
|
### 练习1:面包屑导航
|
||||||
|
添加面包屑显示当前导航路径(如:首页 > 产品列表 > 产品详情)。
|
||||||
|
|
||||||
|
### 练习2:导航守卫
|
||||||
|
实现 `INavigationGuard` 接口,在离开页面前检查是否有未保存的更改。
|
||||||
|
|
||||||
|
### 练习3:嵌套导航
|
||||||
|
在产品列表页内部实现子导航(如:全部/热销/新品标签页切换)。
|
||||||
|
|
||||||
|
### 练习4:导航缓存
|
||||||
|
实现页面缓存,避免重复创建 ViewModel(保持页面状态)。
|
||||||
|
|
||||||
|
## 💡 常见问题
|
||||||
|
|
||||||
|
**Q: 导航后如何保持页面状态?**
|
||||||
|
A: 不要后退时,CurrentViewModel 会保留在前进栈中,前进时会恢复状态。如果需要完全保持状态,可以使用 Singleton ViewModel。
|
||||||
|
|
||||||
|
**Q: 如何传递复杂对象作为参数?**
|
||||||
|
A: 使用 `NavigateAsync(object parameter)`,parameter 可以是任何对象。
|
||||||
|
|
||||||
|
**Q: 导航历史栈何时清空?**
|
||||||
|
A: 调用 `_navigationService.ClearHistory()` 手动清空。
|
||||||
|
|
||||||
|
**Q: 能否使用动画过渡?**
|
||||||
|
A: 可以,在 ContentControl 上添加 Storyboard 实现页面切换动画。
|
||||||
|
|
||||||
|
## 📚 延伸阅读
|
||||||
|
|
||||||
|
- [WPF 数据模板](https://learn.microsoft.com/zh-cn/dotnet/desktop/wpf/data/data-templating-overview)
|
||||||
|
- [MVVM 导航模式](https://prismlibrary.com/docs/navigation.html)
|
||||||
|
- 下一个示例:[05-Localization](../05-Localization/README.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**掌握导航系统,构建流畅的多页面应用!** 🧭
|
||||||
@@ -0,0 +1,278 @@
|
|||||||
|
# 05 - 国际化(i18n)示例
|
||||||
|
|
||||||
|
演示如何使用 Sharp.Wpf 的国际化功能实现多语言支持,包括运行时切换语言、资源文件管理等。
|
||||||
|
|
||||||
|
## 🎯 学习目标
|
||||||
|
|
||||||
|
- 使用 `ILocalizationService` 实现多语言
|
||||||
|
- 在 XAML 中使用 `{loc:Localize}` 绑定本地化字符串
|
||||||
|
- 运行时动态切换语言
|
||||||
|
- 管理资源文件(.resx)
|
||||||
|
- 格式化带参数的本地化字符串
|
||||||
|
|
||||||
|
## 📦 使用的模块
|
||||||
|
|
||||||
|
- **Sharp.Wpf** - ILocalizationService, LocalizeExtension
|
||||||
|
|
||||||
|
## 🚀 快速运行
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd examples/05-Localization
|
||||||
|
dotnet run
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 核心代码讲解
|
||||||
|
|
||||||
|
### 1. 创建资源文件
|
||||||
|
|
||||||
|
```
|
||||||
|
Resources/
|
||||||
|
├── Strings.resx # 默认语言(中文)
|
||||||
|
├── Strings.en-US.resx # 英文
|
||||||
|
└── Strings.ja-JP.resx # 日文(可选)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Strings.resx** (中文):
|
||||||
|
```xml
|
||||||
|
<data name="Welcome" xml:space="preserve">
|
||||||
|
<value>欢迎使用 Sharp 框架</value>
|
||||||
|
</data>
|
||||||
|
<data name="Username" xml:space="preserve">
|
||||||
|
<value>用户名</value>
|
||||||
|
</data>
|
||||||
|
<data name="Password" xml:space="preserve">
|
||||||
|
<value>密码</value>
|
||||||
|
</data>
|
||||||
|
<data name="Login" xml:space="preserve">
|
||||||
|
<value>登录</value>
|
||||||
|
</data>
|
||||||
|
<data name="WelcomeUser" xml:space="preserve">
|
||||||
|
<value>欢迎, {0}!</value>
|
||||||
|
</data>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Strings.en-US.resx** (英文):
|
||||||
|
```xml
|
||||||
|
<data name="Welcome" xml:space="preserve">
|
||||||
|
<value>Welcome to Sharp Framework</value>
|
||||||
|
</data>
|
||||||
|
<data name="Username" xml:space="preserve">
|
||||||
|
<value>Username</value>
|
||||||
|
</data>
|
||||||
|
<data name="Password" xml:space="preserve">
|
||||||
|
<value>Password</value>
|
||||||
|
</data>
|
||||||
|
<data name="Login" xml:space="preserve">
|
||||||
|
<value>Login</value>
|
||||||
|
</data>
|
||||||
|
<data name="WelcomeUser" xml:space="preserve">
|
||||||
|
<value>Welcome, {0}!</value>
|
||||||
|
</data>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. App.xaml.cs - 注册国际化服务
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Resources;
|
||||||
|
using Wpf.Localization;
|
||||||
|
|
||||||
|
protected override void OnStartup(StartupEventArgs e)
|
||||||
|
{
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
|
||||||
|
services.AddSharpWpf();
|
||||||
|
|
||||||
|
// 添加国际化支持
|
||||||
|
var resourceManager = new ResourceManager(
|
||||||
|
"LocalizationExample.Resources.Strings", // 资源文件的完整命名空间
|
||||||
|
Assembly.GetExecutingAssembly());
|
||||||
|
|
||||||
|
services.AddLocalization(
|
||||||
|
resourceManager,
|
||||||
|
new CultureInfo("zh-CN"), // 中文
|
||||||
|
new CultureInfo("en-US"), // 英文
|
||||||
|
new CultureInfo("ja-JP") // 日文
|
||||||
|
);
|
||||||
|
|
||||||
|
services.AddViewModel<MainViewModel>();
|
||||||
|
|
||||||
|
var provider = services.BuildServiceProvider();
|
||||||
|
var mainWindow = provider.GetRequiredService<MainWindow>();
|
||||||
|
mainWindow.Show();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. MainViewModel.cs - 语言切换
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Wpf.Localization;
|
||||||
|
|
||||||
|
public class MainViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
private readonly ILocalizationService _localization;
|
||||||
|
private readonly IDialogService _dialogService;
|
||||||
|
|
||||||
|
public ILocalizationService Localization => _localization;
|
||||||
|
|
||||||
|
public List<CultureInfo> AvailableLanguages { get; }
|
||||||
|
|
||||||
|
public CultureInfo CurrentLanguage
|
||||||
|
{
|
||||||
|
get => _localization.CurrentCulture;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_localization.ChangeCulture(value);
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ICommand ChangeLanguageCommand { get; }
|
||||||
|
public ICommand ShowGreetingCommand { get; }
|
||||||
|
|
||||||
|
public MainViewModel(
|
||||||
|
ILocalizationService localization,
|
||||||
|
IDialogService dialogService)
|
||||||
|
{
|
||||||
|
_localization = localization;
|
||||||
|
_dialogService = dialogService;
|
||||||
|
|
||||||
|
AvailableLanguages = localization.GetSupportedCultures().ToList();
|
||||||
|
|
||||||
|
// 监听语言变化
|
||||||
|
_localization.CultureChanged += (s, culture) =>
|
||||||
|
{
|
||||||
|
_dialogService.ShowMessage(
|
||||||
|
$"语言已切换到: {culture.NativeName}",
|
||||||
|
"提示");
|
||||||
|
};
|
||||||
|
|
||||||
|
ChangeLanguageCommand = new RelayCommand<CultureInfo>(culture =>
|
||||||
|
{
|
||||||
|
if (culture != null)
|
||||||
|
{
|
||||||
|
CurrentLanguage = culture;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ShowGreetingCommand = new RelayCommand(() =>
|
||||||
|
{
|
||||||
|
// 使用格式化字符串
|
||||||
|
var message = _localization.GetString("WelcomeUser", "张三");
|
||||||
|
_dialogService.ShowMessage(message, "问候");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. MainWindow.xaml - 使用本地化绑定
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<Window x:Class="LocalizationExample.MainWindow"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:loc="clr-namespace:Wpf.Localization;assembly=Sharp.Wpf"
|
||||||
|
Title="国际化示例" Height="400" Width="500">
|
||||||
|
<StackPanel Margin="20">
|
||||||
|
<!-- 方式1: 使用 LocalizeExtension(推荐) -->
|
||||||
|
<TextBlock Text="{loc:Localize Welcome}" FontSize="24" FontWeight="Bold"/>
|
||||||
|
|
||||||
|
<!-- 语言选择 -->
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,20,0,0">
|
||||||
|
<TextBlock Text="选择语言:" VerticalAlignment="Center" Margin="0,0,10,0"/>
|
||||||
|
<ComboBox ItemsSource="{Binding AvailableLanguages}"
|
||||||
|
SelectedItem="{Binding CurrentLanguage}"
|
||||||
|
DisplayMemberPath="NativeName"
|
||||||
|
Width="150"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- 登录表单 -->
|
||||||
|
<StackPanel Margin="0,30,0,0">
|
||||||
|
<!-- 用户名 -->
|
||||||
|
<TextBlock Text="{loc:Localize Username}"/>
|
||||||
|
<TextBox Margin="0,5,0,15"/>
|
||||||
|
|
||||||
|
<!-- 密码 -->
|
||||||
|
<TextBlock Text="{loc:Localize Password}"/>
|
||||||
|
<PasswordBox Margin="0,5,0,15"/>
|
||||||
|
|
||||||
|
<!-- 登录按钮 -->
|
||||||
|
<Button Content="{loc:Localize Login}"
|
||||||
|
Height="35" FontSize="14"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- 方式2: 直接绑定 Localization 服务 -->
|
||||||
|
<StackPanel Margin="0,30,0,0">
|
||||||
|
<TextBlock Text="{Binding Localization[Welcome]}" FontStyle="Italic"/>
|
||||||
|
<TextBlock Text="{Binding Localization[Username]}" Margin="0,5,0,0"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- 测试格式化字符串 -->
|
||||||
|
<Button Content="显示问候" Command="{Binding ShowGreetingCommand}" Margin="0,20,0,0"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Window>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 在代码中使用本地化
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 方式1: 通过服务获取
|
||||||
|
var welcomeText = _localization.GetString("Welcome");
|
||||||
|
|
||||||
|
// 方式2: 带格式化参数
|
||||||
|
var greeting = _localization.GetString("WelcomeUser", username);
|
||||||
|
|
||||||
|
// 方式3: 索引器(用于绑定)
|
||||||
|
var password = _localization["Password"];
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎓 练习建议
|
||||||
|
|
||||||
|
### 练习1:添加更多语言
|
||||||
|
添加法语(fr-FR)或德语(de-DE)资源文件。
|
||||||
|
|
||||||
|
### 练习2:保存语言偏好
|
||||||
|
将用户选择的语言保存到配置文件,下次启动时自动加载。
|
||||||
|
|
||||||
|
### 练习3:动态加载资源
|
||||||
|
实现从外部 JSON 文件加载翻译资源(而非编译进程序)。
|
||||||
|
|
||||||
|
### 练习4:翻译进度显示
|
||||||
|
显示每种语言的翻译完成度(如:英文 95%,日文 60%)。
|
||||||
|
|
||||||
|
## 💡 常见问题
|
||||||
|
|
||||||
|
**Q: 资源文件修改后不生效?**
|
||||||
|
A: 确保:
|
||||||
|
1. 资源文件的"生成操作"设为"嵌入的资源"
|
||||||
|
2. 重新生成项目(Clean + Rebuild)
|
||||||
|
|
||||||
|
**Q: 如何支持从右到左(RTL)语言?**
|
||||||
|
A: 监听 CultureChanged 事件,设置 `FlowDirection`:
|
||||||
|
```csharp
|
||||||
|
_localization.CultureChanged += (s, culture) => {
|
||||||
|
if (culture.TextInfo.IsRightToLeft) {
|
||||||
|
Application.Current.MainWindow.FlowDirection = FlowDirection.RightToLeft;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**Q: 能否热重载资源文件?**
|
||||||
|
A: Sharp.Wpf 当前使用编译时嵌入资源,不支持运行时重载。可以使用外部 JSON 文件实现热重载。
|
||||||
|
|
||||||
|
**Q: 如何处理复数形式?**
|
||||||
|
A: 使用不同的资源键或使用占位符:
|
||||||
|
```
|
||||||
|
"{Count} items" → "{0} 个项目"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📚 延伸阅读
|
||||||
|
|
||||||
|
- [.NET 资源文件](https://learn.microsoft.com/zh-cn/dotnet/core/extensions/resources)
|
||||||
|
- [CultureInfo 类](https://learn.microsoft.com/zh-cn/dotnet/api/system.globalization.cultureinfo)
|
||||||
|
- [WPF 本地化](https://learn.microsoft.com/zh-cn/dotnet/desktop/wpf/advanced/globalization-for-wpf)
|
||||||
|
- 下一个示例:[06-Theming](../06-Theming/README.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**构建面向全球用户的应用!** 🌍
|
||||||
@@ -0,0 +1,312 @@
|
|||||||
|
# 06 - 主题系统示例
|
||||||
|
|
||||||
|
演示如何使用 Sharp.Wpf 的主题系统实现浅色/深色主题切换,包括自定义颜色、跟随系统主题等功能。
|
||||||
|
|
||||||
|
## 🎯 学习目标
|
||||||
|
|
||||||
|
- 使用 `IThemeService` 管理应用主题
|
||||||
|
- 实现浅色/深色主题切换
|
||||||
|
- 自定义主题颜色
|
||||||
|
- 跟随系统主题设置
|
||||||
|
- 动态主题切换(无需重启)
|
||||||
|
|
||||||
|
## 📦 使用的模块
|
||||||
|
|
||||||
|
- **Sharp.Wpf** - IThemeService, ThemeService
|
||||||
|
|
||||||
|
## 🚀 快速运行
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd examples/06-Theming
|
||||||
|
dotnet run
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 核心代码讲解
|
||||||
|
|
||||||
|
### 1. App.xaml.cs - 注册主题服务
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Wpf.Theming;
|
||||||
|
|
||||||
|
protected override void OnStartup(StartupEventArgs e)
|
||||||
|
{
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
|
||||||
|
services.AddSharpWpf(); // 自动注册 IThemeService
|
||||||
|
|
||||||
|
services.AddViewModel<MainViewModel>();
|
||||||
|
|
||||||
|
var provider = services.BuildServiceProvider();
|
||||||
|
|
||||||
|
// 应用初始主题
|
||||||
|
var themeService = provider.GetRequiredService<IThemeService>();
|
||||||
|
themeService.ApplyTheme(Theme.Light); // 或 Theme.System
|
||||||
|
|
||||||
|
var mainWindow = provider.GetRequiredService<MainWindow>();
|
||||||
|
mainWindow.Show();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. MainViewModel.cs - 主题切换逻辑
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Wpf.Theming;
|
||||||
|
|
||||||
|
public class MainViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
private readonly IThemeService _themeService;
|
||||||
|
private Theme _currentTheme;
|
||||||
|
|
||||||
|
public Theme CurrentTheme
|
||||||
|
{
|
||||||
|
get => _currentTheme;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
SetProperty(ref _currentTheme, value);
|
||||||
|
_themeService.ApplyTheme(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ICommand SwitchToLightCommand { get; }
|
||||||
|
public ICommand SwitchToDarkCommand { get; }
|
||||||
|
public ICommand SwitchToSystemCommand { get; }
|
||||||
|
|
||||||
|
public MainViewModel(IThemeService themeService)
|
||||||
|
{
|
||||||
|
_themeService = themeService;
|
||||||
|
_currentTheme = themeService.CurrentTheme;
|
||||||
|
|
||||||
|
// 监听主题变化
|
||||||
|
_themeService.ThemeChanged += (s, theme) =>
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(CurrentTheme));
|
||||||
|
};
|
||||||
|
|
||||||
|
SwitchToLightCommand = new RelayCommand(() =>
|
||||||
|
{
|
||||||
|
CurrentTheme = Theme.Light;
|
||||||
|
});
|
||||||
|
|
||||||
|
SwitchToDarkCommand = new RelayCommand(() =>
|
||||||
|
{
|
||||||
|
CurrentTheme = Theme.Dark;
|
||||||
|
});
|
||||||
|
|
||||||
|
SwitchToSystemCommand = new RelayCommand(() =>
|
||||||
|
{
|
||||||
|
CurrentTheme = Theme.System;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. MainWindow.xaml - 使用主题颜色
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<Window x:Class="ThemingExample.MainWindow"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
Title="主题系统示例" Height="500" Width="600"
|
||||||
|
Background="{DynamicResource BackgroundColor}"> <!-- 使用主题颜色 -->
|
||||||
|
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- 主题切换栏 -->
|
||||||
|
<StackPanel Grid.Row="0" Orientation="Horizontal"
|
||||||
|
Background="{DynamicResource PrimaryColor}" Padding="15">
|
||||||
|
<TextBlock Text="选择主题:"
|
||||||
|
Foreground="White" VerticalAlignment="Center" Margin="0,0,10,0"/>
|
||||||
|
<Button Content="☀️ 浅色" Command="{Binding SwitchToLightCommand}"
|
||||||
|
Margin="0,0,10,0" Padding="15,5"/>
|
||||||
|
<Button Content="🌙 深色" Command="{Binding SwitchToDarkCommand}"
|
||||||
|
Margin="0,0,10,0" Padding="15,5"/>
|
||||||
|
<Button Content="🖥️ 跟随系统" Command="{Binding SwitchToSystemCommand}"
|
||||||
|
Padding="15,5"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- 内容区域 -->
|
||||||
|
<ScrollViewer Grid.Row="1" Padding="20">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="主题颜色预览" FontSize="24" FontWeight="Bold"
|
||||||
|
Foreground="{DynamicResource ForegroundColor}" Margin="0,0,0,20"/>
|
||||||
|
|
||||||
|
<!-- 基础颜色 -->
|
||||||
|
<GroupBox Header="基础颜色" Margin="0,0,0,15">
|
||||||
|
<StackPanel>
|
||||||
|
<Border Background="{DynamicResource PrimaryColor}"
|
||||||
|
Height="40" Margin="0,5">
|
||||||
|
<TextBlock Text="PrimaryColor" Foreground="White"
|
||||||
|
VerticalAlignment="Center" HorizontalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<Border Background="{DynamicResource BackgroundColor}"
|
||||||
|
BorderBrush="{DynamicResource BorderColor}" BorderThickness="1"
|
||||||
|
Height="40" Margin="0,5">
|
||||||
|
<TextBlock Text="BackgroundColor"
|
||||||
|
Foreground="{DynamicResource ForegroundColor}"
|
||||||
|
VerticalAlignment="Center" HorizontalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</GroupBox>
|
||||||
|
|
||||||
|
<!-- 状态颜色 -->
|
||||||
|
<GroupBox Header="状态颜色" Margin="0,0,0,15">
|
||||||
|
<StackPanel>
|
||||||
|
<Border Background="{DynamicResource SuccessColor}"
|
||||||
|
Height="40" Margin="0,5">
|
||||||
|
<TextBlock Text="✓ Success" Foreground="White"
|
||||||
|
VerticalAlignment="Center" HorizontalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<Border Background="{DynamicResource WarningColor}"
|
||||||
|
Height="40" Margin="0,5">
|
||||||
|
<TextBlock Text="⚠ Warning" Foreground="White"
|
||||||
|
VerticalAlignment="Center" HorizontalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<Border Background="{DynamicResource ErrorColor}"
|
||||||
|
Height="40" Margin="0,5">
|
||||||
|
<TextBlock Text="✗ Error" Foreground="White"
|
||||||
|
VerticalAlignment="Center" HorizontalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<Border Background="{DynamicResource InfoColor}"
|
||||||
|
Height="40" Margin="0,5">
|
||||||
|
<TextBlock Text="ℹ Info" Foreground="White"
|
||||||
|
VerticalAlignment="Center" HorizontalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</GroupBox>
|
||||||
|
|
||||||
|
<!-- 控件预览 -->
|
||||||
|
<GroupBox Header="控件预览" Margin="0,0,0,15">
|
||||||
|
<StackPanel>
|
||||||
|
<!-- 按钮 -->
|
||||||
|
<Button Content="普通按钮" Margin="0,5" Padding="10,5"
|
||||||
|
Background="{DynamicResource ButtonBackgroundColor}"
|
||||||
|
Foreground="{DynamicResource ForegroundColor}"/>
|
||||||
|
|
||||||
|
<!-- 输入框 -->
|
||||||
|
<TextBox Text="输入框示例" Margin="0,5" Padding="5"
|
||||||
|
Background="{DynamicResource InputBackgroundColor}"
|
||||||
|
BorderBrush="{DynamicResource InputBorderColor}"/>
|
||||||
|
|
||||||
|
<!-- 复选框 -->
|
||||||
|
<CheckBox Content="复选框示例" Margin="0,5"
|
||||||
|
Foreground="{DynamicResource ForegroundColor}"/>
|
||||||
|
|
||||||
|
<!-- 单选按钮 -->
|
||||||
|
<RadioButton Content="单选按钮示例" Margin="0,5"
|
||||||
|
Foreground="{DynamicResource ForegroundColor}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</GroupBox>
|
||||||
|
|
||||||
|
<!-- 文本示例 -->
|
||||||
|
<GroupBox Header="文本示例">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="这是标题文本" FontSize="20" FontWeight="Bold"
|
||||||
|
Foreground="{DynamicResource ForegroundColor}" Margin="0,5"/>
|
||||||
|
<TextBlock Text="这是正文文本。主题切换后,所有使用 DynamicResource 的颜色都会自动更新,无需重启应用。"
|
||||||
|
TextWrapping="Wrap" Foreground="{DynamicResource ForegroundColor}"
|
||||||
|
Margin="0,5"/>
|
||||||
|
<TextBlock Text="这是次要文本" FontSize="12" FontStyle="Italic"
|
||||||
|
Foreground="Gray" Margin="0,5"/>
|
||||||
|
</StackPanel>
|
||||||
|
</GroupBox>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 自定义主题颜色
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class CustomThemeViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
private readonly IThemeService _themeService;
|
||||||
|
|
||||||
|
public ICommand ChangePrimaryColorCommand { get; }
|
||||||
|
|
||||||
|
public CustomThemeViewModel(IThemeService themeService)
|
||||||
|
{
|
||||||
|
_themeService = themeService;
|
||||||
|
|
||||||
|
ChangePrimaryColorCommand = new RelayCommand(() =>
|
||||||
|
{
|
||||||
|
// 打开颜色选择器(可以使用第三方库或自定义)
|
||||||
|
var color = Colors.DeepSkyBlue; // 示例:选择的颜色
|
||||||
|
|
||||||
|
// 更新主题颜色
|
||||||
|
_themeService.SetColor("PrimaryColor", color);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 主题资源字典(可选 - 高级用法)
|
||||||
|
|
||||||
|
可以创建独立的资源字典文件:
|
||||||
|
|
||||||
|
**Themes/LightTheme.xaml**:
|
||||||
|
```xml
|
||||||
|
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
|
<SolidColorBrush x:Key="PrimaryColor" Color="#0078D7"/>
|
||||||
|
<SolidColorBrush x:Key="BackgroundColor" Color="White"/>
|
||||||
|
<SolidColorBrush x:Key="ForegroundColor" Color="Black"/>
|
||||||
|
<!-- ... 其他颜色 -->
|
||||||
|
</ResourceDictionary>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Themes/DarkTheme.xaml**:
|
||||||
|
```xml
|
||||||
|
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
|
<SolidColorBrush x:Key="PrimaryColor" Color="#0078D7"/>
|
||||||
|
<SolidColorBrush x:Key="BackgroundColor" Color="#202020"/>
|
||||||
|
<SolidColorBrush x:Key="ForegroundColor" Color="White"/>
|
||||||
|
<!-- ... 其他颜色 -->
|
||||||
|
</ResourceDictionary>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎓 练习建议
|
||||||
|
|
||||||
|
### 练习1:添加更多预设主题
|
||||||
|
添加"高对比度"主题,适合视力不佳的用户。
|
||||||
|
|
||||||
|
### 练习2:主题动画过渡
|
||||||
|
在切换主题时添加颜色渐变动画,而非瞬间切换。
|
||||||
|
|
||||||
|
### 练习3:保存主题偏好
|
||||||
|
将用户选择的主题保存到配置文件,下次启动时自动应用。
|
||||||
|
|
||||||
|
### 练习4:自定义主题编辑器
|
||||||
|
创建一个界面,允许用户自定义每种颜色,并导出/导入主题配置。
|
||||||
|
|
||||||
|
## 💡 常见问题
|
||||||
|
|
||||||
|
**Q: StaticResource 和 DynamicResource 的区别?**
|
||||||
|
A:
|
||||||
|
- `StaticResource`: 编译时绑定,主题切换后不会更新
|
||||||
|
- `DynamicResource`: 运行时绑定,主题切换后自动更新(推荐用于主题)
|
||||||
|
|
||||||
|
**Q: 主题切换后部分控件没变化?**
|
||||||
|
A: 确保使用了 `{DynamicResource}` 而非 `{StaticResource}`。
|
||||||
|
|
||||||
|
**Q: 如何跟随 Windows 系统主题?**
|
||||||
|
A: 使用 `Theme.System`,Sharp.Wpf 会自动检测系统主题设置。
|
||||||
|
|
||||||
|
**Q: 能否为不同的窗口设置不同主题?**
|
||||||
|
A: 可以,但不推荐。可以为每个 Window 设置独立的 ResourceDictionary。
|
||||||
|
|
||||||
|
## 📚 延伸阅读
|
||||||
|
|
||||||
|
- [WPF 资源](https://learn.microsoft.com/zh-cn/dotnet/desktop/wpf/systems/xaml-resources-overview)
|
||||||
|
- [WPF 样式和模板](https://learn.microsoft.com/zh-cn/dotnet/desktop/wpf/controls/styles-templates-overview)
|
||||||
|
- [Windows 主题检测](https://learn.microsoft.com/zh-cn/windows/apps/desktop/modernize/apply-windows-themes)
|
||||||
|
- 综合示例:[11-FullStackDemo](../11-FullStackDemo/README.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**打造美观现代的用户界面!** 🎨
|
||||||
@@ -0,0 +1,569 @@
|
|||||||
|
# 11 - Full Stack Demo - 完整功能演示
|
||||||
|
|
||||||
|
这是一个综合示例,集成了 Sharp 框架的几乎所有功能,演示如何构建一个完整的工业监控系统。
|
||||||
|
|
||||||
|
## 🎯 功能列表
|
||||||
|
|
||||||
|
### 核心功能
|
||||||
|
- ✅ 用户登录(数据库验证)
|
||||||
|
- ✅ 多页面导航
|
||||||
|
- ✅ 主题切换(浅色/深色)
|
||||||
|
- ✅ 多语言支持(中英文)
|
||||||
|
- ✅ 表单验证
|
||||||
|
|
||||||
|
### 数据管理
|
||||||
|
- ✅ 产品 CRUD 操作
|
||||||
|
- ✅ 数据分页显示
|
||||||
|
- ✅ 缓存优化
|
||||||
|
|
||||||
|
### 工业IoT
|
||||||
|
- ✅ **PLC 通信测试** - 可配置 IP 和参数进行测试
|
||||||
|
- ✅ **串口通信测试** - 可选择串口和配置参数
|
||||||
|
|
||||||
|
### 其他
|
||||||
|
- ✅ 配置热重载
|
||||||
|
- ✅ 日志记录
|
||||||
|
- ✅ HTTP 客户端调用
|
||||||
|
|
||||||
|
## 📦 使用的模块
|
||||||
|
|
||||||
|
- Sharp.Common
|
||||||
|
- Sharp.Config
|
||||||
|
- Sharp.Wpf
|
||||||
|
- Sharp.Data
|
||||||
|
- Sharp.Cache
|
||||||
|
- Sharp.Log
|
||||||
|
- Sharp.Plc.Siemens
|
||||||
|
- Sharp.Ports
|
||||||
|
- Sharp.NetClient
|
||||||
|
|
||||||
|
## 🚀 快速运行
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd examples/11-FullStackDemo
|
||||||
|
dotnet run
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
11-FullStackDemo/
|
||||||
|
├── App.xaml.cs # 应用启动配置
|
||||||
|
├── appsettings.json # 配置文件
|
||||||
|
├── Models/
|
||||||
|
│ ├── User.cs # 用户实体
|
||||||
|
│ └── Product.cs # 产品实体
|
||||||
|
├── ViewModels/
|
||||||
|
│ ├── LoginViewModel.cs # 登录页
|
||||||
|
│ ├── MainViewModel.cs # 主窗口
|
||||||
|
│ ├── HomeViewModel.cs # 首页(仪表板)
|
||||||
|
│ ├── ProductsViewModel.cs # 产品管理
|
||||||
|
│ ├── PlcTestViewModel.cs # PLC 测试页 ⭐
|
||||||
|
│ ├── PortsTestViewModel.cs# 串口测试页 ⭐
|
||||||
|
│ └── SettingsViewModel.cs # 设置页
|
||||||
|
├── Views/
|
||||||
|
│ ├── LoginWindow.xaml
|
||||||
|
│ ├── MainWindow.xaml
|
||||||
|
│ ├── HomeView.xaml
|
||||||
|
│ ├── ProductsView.xaml
|
||||||
|
│ ├── PlcTestView.xaml # PLC 测试界面 ⭐
|
||||||
|
│ ├── PortsTestView.xaml # 串口测试界面 ⭐
|
||||||
|
│ └── SettingsView.xaml
|
||||||
|
└── Data/
|
||||||
|
└── AppDbContext.cs
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 核心代码讲解
|
||||||
|
|
||||||
|
### 1. App.xaml.cs - 完整启动配置
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
protected override void OnStartup(StartupEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnStartup(e);
|
||||||
|
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
|
||||||
|
// 1. 配置管理
|
||||||
|
var config = ConfigBuilder.CreateDefault().Configuration;
|
||||||
|
services.AddSingleton(config);
|
||||||
|
|
||||||
|
// 2. 数据库
|
||||||
|
services.AddSharpDatabase<AppDbContext>(config);
|
||||||
|
|
||||||
|
// 3. WPF 服务(包括导航、主题、国际化)
|
||||||
|
services.AddSharpWpf();
|
||||||
|
services.AddLocalization(Resources.Strings.ResourceManager,
|
||||||
|
new CultureInfo("zh-CN"),
|
||||||
|
new CultureInfo("en-US"));
|
||||||
|
|
||||||
|
// 4. 缓存
|
||||||
|
services.AddSharpCache();
|
||||||
|
|
||||||
|
// 5. 日志
|
||||||
|
services.AddLogging(builder => {
|
||||||
|
builder.AddConsole();
|
||||||
|
builder.AddDebug();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 6. PLC 服务(可选 - 如果配置中启用)
|
||||||
|
var plcEnabled = config.GetValue<bool>("Plc:Enabled", false);
|
||||||
|
if (plcEnabled)
|
||||||
|
{
|
||||||
|
services.AddSiemensPlc(config.GetSection("Plc:Siemens"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. 串口服务
|
||||||
|
services.AddSingleton<ISerialPortService, SerialPortService>();
|
||||||
|
|
||||||
|
// 8. HTTP 客户端
|
||||||
|
services.AddHttpClient();
|
||||||
|
|
||||||
|
// 9. 注册 ViewModels
|
||||||
|
services.AddViewModel<LoginViewModel>();
|
||||||
|
services.AddViewModel<MainViewModel>();
|
||||||
|
services.AddViewModel<HomeViewModel>();
|
||||||
|
services.AddViewModel<ProductsViewModel>();
|
||||||
|
services.AddViewModel<PlcTestViewModel>();
|
||||||
|
services.AddViewModel<PortsTestViewModel>();
|
||||||
|
services.AddViewModel<SettingsViewModel>();
|
||||||
|
|
||||||
|
// 10. 注册 Windows
|
||||||
|
services.AddTransient<LoginWindow>();
|
||||||
|
services.AddTransient<MainWindow>();
|
||||||
|
|
||||||
|
_serviceProvider = services.BuildServiceProvider();
|
||||||
|
|
||||||
|
// 11. 初始化数据库
|
||||||
|
using (var scope = _serviceProvider.CreateScope())
|
||||||
|
{
|
||||||
|
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||||
|
db.Database.EnsureCreated();
|
||||||
|
SeedData(db); // 初始化测试数据
|
||||||
|
}
|
||||||
|
|
||||||
|
// 12. 应用主题
|
||||||
|
var themeService = _serviceProvider.GetRequiredService<IThemeService>();
|
||||||
|
var savedTheme = config.GetValue<string>("App:Theme", "Light");
|
||||||
|
themeService.ApplyTheme(Enum.Parse<Theme>(savedTheme));
|
||||||
|
|
||||||
|
// 13. 显示登录窗口
|
||||||
|
ShowLoginWindow();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. PlcTestViewModel.cs - PLC 测试页 ⭐
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class PlcTestViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
private readonly IDialogService _dialogService;
|
||||||
|
private IPlcService? _plcService;
|
||||||
|
|
||||||
|
// 连接参数
|
||||||
|
private string _ipAddress = "192.168.1.100";
|
||||||
|
private int _rack = 0;
|
||||||
|
private int _slot = 1;
|
||||||
|
private bool _isConnected;
|
||||||
|
|
||||||
|
public string IpAddress
|
||||||
|
{
|
||||||
|
get => _ipAddress;
|
||||||
|
set => SetProperty(ref _ipAddress, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Rack
|
||||||
|
{
|
||||||
|
get => _rack;
|
||||||
|
set => SetProperty(ref _rack, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Slot
|
||||||
|
{
|
||||||
|
get => _slot;
|
||||||
|
set => SetProperty(ref _slot, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsConnected
|
||||||
|
{
|
||||||
|
get => _isConnected;
|
||||||
|
set => SetProperty(ref _isConnected, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读写测试
|
||||||
|
private string _address = "DB1.DBD0";
|
||||||
|
private string _value = "0";
|
||||||
|
private string _readResult = "";
|
||||||
|
|
||||||
|
public string Address
|
||||||
|
{
|
||||||
|
get => _address;
|
||||||
|
set => SetProperty(ref _address, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Value
|
||||||
|
{
|
||||||
|
get => _value;
|
||||||
|
set => SetProperty(ref _value, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ReadResult
|
||||||
|
{
|
||||||
|
get => _readResult;
|
||||||
|
set => SetProperty(ref _readResult, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ICommand ConnectCommand { get; }
|
||||||
|
public ICommand DisconnectCommand { get; }
|
||||||
|
public ICommand ReadCommand { get; }
|
||||||
|
public ICommand WriteCommand { get; }
|
||||||
|
|
||||||
|
public PlcTestViewModel(IDialogService dialogService)
|
||||||
|
{
|
||||||
|
_dialogService = dialogService;
|
||||||
|
|
||||||
|
ConnectCommand = new AsyncCommand(async (ct) =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
IsBusy = true;
|
||||||
|
|
||||||
|
// 动态创建 PLC 服务
|
||||||
|
var options = new SiemensPlcOptions
|
||||||
|
{
|
||||||
|
IpAddress = IpAddress,
|
||||||
|
Rack = Rack,
|
||||||
|
Slot = Slot,
|
||||||
|
CpuType = CpuType.S71500
|
||||||
|
};
|
||||||
|
|
||||||
|
_plcService = new PlcMonitorService(
|
||||||
|
new SiemensDriver(options),
|
||||||
|
new SiemensOptimizer(),
|
||||||
|
new BigEndianConverter(),
|
||||||
|
Options.Create(new PlcMonitorOptions()),
|
||||||
|
null);
|
||||||
|
|
||||||
|
await _plcService.StartAsync(ct);
|
||||||
|
|
||||||
|
IsConnected = true;
|
||||||
|
_dialogService.ShowMessage("PLC 连接成功!", "成功");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_dialogService.ShowMessage($"连接失败: {ex.Message}", "错误");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
IsBusy = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
DisconnectCommand = new AsyncCommand(async (ct) =>
|
||||||
|
{
|
||||||
|
if (_plcService != null)
|
||||||
|
{
|
||||||
|
await _plcService.StopAsync();
|
||||||
|
IsConnected = false;
|
||||||
|
_dialogService.ShowMessage("已断开连接", "提示");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ReadCommand = new AsyncCommand(async (ct) =>
|
||||||
|
{
|
||||||
|
if (_plcService == null)
|
||||||
|
{
|
||||||
|
_dialogService.ShowMessage("请先连接 PLC", "提示");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
IsBusy = true;
|
||||||
|
var result = await _plcService.ReadAsync<int>(Address);
|
||||||
|
|
||||||
|
if (result.IsSuccess)
|
||||||
|
{
|
||||||
|
ReadResult = $"读取成功: {result.Value}";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ReadResult = $"读取失败: {result.ErrorMessage}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ReadResult = $"读取异常: {ex.Message}";
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
IsBusy = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
WriteCommand = new AsyncCommand(async (ct) =>
|
||||||
|
{
|
||||||
|
if (_plcService == null)
|
||||||
|
{
|
||||||
|
_dialogService.ShowMessage("请先连接 PLC", "提示");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
IsBusy = true;
|
||||||
|
var intValue = int.Parse(Value);
|
||||||
|
var result = await _plcService.WriteAsync(Address, intValue);
|
||||||
|
|
||||||
|
if (result.IsSuccess)
|
||||||
|
{
|
||||||
|
_dialogService.ShowMessage("写入成功!", "成功");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_dialogService.ShowMessage($"写入失败: {result.ErrorMessage}", "错误");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_dialogService.ShowMessage($"写入异常: {ex.Message}", "错误");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
IsBusy = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. PortsTestViewModel.cs - 串口测试页 ⭐
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class PortsTestViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
private readonly ISerialPortService _serialPortService;
|
||||||
|
private readonly IDialogService _dialogService;
|
||||||
|
|
||||||
|
// 可用串口列表
|
||||||
|
public ObservableCollection<string> AvailablePorts { get; } = new();
|
||||||
|
|
||||||
|
private string? _selectedPort;
|
||||||
|
private int _baudRate = 9600;
|
||||||
|
private bool _isConnected;
|
||||||
|
|
||||||
|
public string? SelectedPort
|
||||||
|
{
|
||||||
|
get => _selectedPort;
|
||||||
|
set => SetProperty(ref _selectedPort, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int BaudRate
|
||||||
|
{
|
||||||
|
get => _baudRate;
|
||||||
|
set => SetProperty(ref _baudRate, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsConnected
|
||||||
|
{
|
||||||
|
get => _isConnected;
|
||||||
|
set => SetProperty(ref _isConnected, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送/接收
|
||||||
|
private string _sendData = "";
|
||||||
|
private string _receiveData = "";
|
||||||
|
|
||||||
|
public string SendData
|
||||||
|
{
|
||||||
|
get => _sendData;
|
||||||
|
set => SetProperty(ref _sendData, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ReceiveData
|
||||||
|
{
|
||||||
|
get => _receiveData;
|
||||||
|
set => SetProperty(ref _receiveData, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ICommand RefreshPortsCommand { get; }
|
||||||
|
public ICommand ConnectCommand { get; }
|
||||||
|
public ICommand DisconnectCommand { get; }
|
||||||
|
public ICommand SendCommand { get; }
|
||||||
|
public ICommand ClearCommand { get; }
|
||||||
|
|
||||||
|
public PortsTestViewModel(
|
||||||
|
ISerialPortService serialPortService,
|
||||||
|
IDialogService dialogService)
|
||||||
|
{
|
||||||
|
_serialPortService = serialPortService;
|
||||||
|
_dialogService = dialogService;
|
||||||
|
|
||||||
|
// 监听数据接收
|
||||||
|
_serialPortService.DataReceived += (s, e) =>
|
||||||
|
{
|
||||||
|
Application.Current.Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
ReceiveData += $"[{DateTime.Now:HH:mm:ss}] 接收: {BitConverter.ToString(e.Data)}\n";
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
RefreshPortsCommand = new RelayCommand(() =>
|
||||||
|
{
|
||||||
|
AvailablePorts.Clear();
|
||||||
|
var ports = SerialPortDiscovery.GetAvailablePorts();
|
||||||
|
foreach (var port in ports)
|
||||||
|
{
|
||||||
|
AvailablePorts.Add(port.PortName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (AvailablePorts.Count > 0)
|
||||||
|
{
|
||||||
|
SelectedPort = AvailablePorts[0];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ConnectCommand = new AsyncCommand(async (ct) =>
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(SelectedPort))
|
||||||
|
{
|
||||||
|
_dialogService.ShowMessage("请选择串口", "提示");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var options = new SerialPortOptions
|
||||||
|
{
|
||||||
|
PortName = SelectedPort,
|
||||||
|
BaudRate = BaudRate
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _serialPortService.StartListeningAsync(options, autoReconnect: false);
|
||||||
|
|
||||||
|
if (result.IsSuccess)
|
||||||
|
{
|
||||||
|
IsConnected = true;
|
||||||
|
_dialogService.ShowMessage("串口连接成功!", "成功");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_dialogService.ShowMessage($"连接失败: {result.ErrorMessage}", "错误");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_dialogService.ShowMessage($"连接异常: {ex.Message}", "错误");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
DisconnectCommand = new AsyncCommand(async (ct) =>
|
||||||
|
{
|
||||||
|
await _serialPortService.StopListeningAsync();
|
||||||
|
IsConnected = false;
|
||||||
|
_dialogService.ShowMessage("已断开连接", "提示");
|
||||||
|
});
|
||||||
|
|
||||||
|
SendCommand = new AsyncCommand(async (ct) =>
|
||||||
|
{
|
||||||
|
if (!IsConnected)
|
||||||
|
{
|
||||||
|
_dialogService.ShowMessage("请先连接串口", "提示");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 将字符串转换为字节数组
|
||||||
|
var data = Encoding.UTF8.GetBytes(SendData);
|
||||||
|
var result = await _serialPortService.SendAsync(data);
|
||||||
|
|
||||||
|
if (result.IsSuccess)
|
||||||
|
{
|
||||||
|
ReceiveData += $"[{DateTime.Now:HH:mm:ss}] 发送: {SendData}\n";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_dialogService.ShowMessage($"发送失败: {result.ErrorMessage}", "错误");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_dialogService.ShowMessage($"发送异常: {ex.Message}", "错误");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ClearCommand = new RelayCommand(() =>
|
||||||
|
{
|
||||||
|
ReceiveData = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始化:刷新串口列表
|
||||||
|
RefreshPortsCommand.Execute(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. appsettings.json - 配置文件
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"App": {
|
||||||
|
"Theme": "Light",
|
||||||
|
"Language": "zh-CN"
|
||||||
|
},
|
||||||
|
"Database": {
|
||||||
|
"Provider": "Sqlite",
|
||||||
|
"ConnectionString": "Data Source=demo.db"
|
||||||
|
},
|
||||||
|
"Plc": {
|
||||||
|
"Enabled": false,
|
||||||
|
"Siemens": {
|
||||||
|
"IpAddress": "192.168.1.100",
|
||||||
|
"Port": 102,
|
||||||
|
"CpuType": "S71500",
|
||||||
|
"Rack": 0,
|
||||||
|
"Slot": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎓 使用说明
|
||||||
|
|
||||||
|
### 测试 PLC 连接
|
||||||
|
1. 进入"PLC 测试"页面
|
||||||
|
2. 输入 PLC 的 IP 地址、Rack、Slot
|
||||||
|
3. 点击"连接"按钮
|
||||||
|
4. 连接成功后,输入地址(如 `DB1.DBD0`)
|
||||||
|
5. 点击"读取"或"写入"测试通信
|
||||||
|
|
||||||
|
### 测试串口通信
|
||||||
|
1. 进入"串口测试"页面
|
||||||
|
2. 点击"刷新端口"查看可用串口
|
||||||
|
3. 选择串口并设置波特率
|
||||||
|
4. 点击"连接"
|
||||||
|
5. 在发送框输入数据,点击"发送"
|
||||||
|
6. 接收框会显示收到的数据
|
||||||
|
|
||||||
|
## 💡 提示
|
||||||
|
|
||||||
|
- PLC 和串口功能可以独立测试,无需实际设备(会显示连接失败,但不影响其他功能)
|
||||||
|
- 数据库使用 SQLite,无需安装
|
||||||
|
- 配置文件支持热重载,修改后自动生效
|
||||||
|
- 所有功能都有错误处理,不会导致程序崩溃
|
||||||
|
|
||||||
|
## 📚 延伸阅读
|
||||||
|
|
||||||
|
- 所有示例项目的 README
|
||||||
|
- Sharp 框架文档
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**这是一个完整的工业监控系统模板,可以直接用于实际项目开发!** 🏭
|
||||||
@@ -0,0 +1,273 @@
|
|||||||
|
# Sharp Framework Examples
|
||||||
|
|
||||||
|
本目录包含 Sharp 框架各个功能的示例项目,帮助你快速上手。
|
||||||
|
|
||||||
|
## 📚 示例项目列表
|
||||||
|
|
||||||
|
### 基础示例
|
||||||
|
|
||||||
|
#### 1. **01-MinimalWpfApp** - 最小 WPF 应用 ⭐
|
||||||
|
**演示内容**:
|
||||||
|
- 最简单的 Sharp WPF 应用
|
||||||
|
- 仅使用 Common + Wpf 模块
|
||||||
|
- MVVM 基础(ViewModelBase, Commands)
|
||||||
|
- 对话框服务
|
||||||
|
|
||||||
|
**适用场景**:刚接触 Sharp 框架,想快速了解基本用法
|
||||||
|
|
||||||
|
**运行**:
|
||||||
|
```bash
|
||||||
|
cd examples/01-MinimalWpfApp
|
||||||
|
dotnet run
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 2. **02-WpfWithDatabase** - WPF + 数据库 ⭐⭐
|
||||||
|
**演示内容**:
|
||||||
|
- WPF + SQLite 数据库
|
||||||
|
- Repository 模式(CRUD操作)
|
||||||
|
- 数据绑定与列表展示
|
||||||
|
- 添加/编辑/删除产品
|
||||||
|
|
||||||
|
**适用场景**:需要本地数据存储的桌面应用
|
||||||
|
|
||||||
|
**技术栈**:Common, Config, Wpf, Data (SQLite)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 3. **03-FormValidation** - 表单验证 ⭐⭐
|
||||||
|
**演示内容**:
|
||||||
|
- ValidatableViewModelBase 使用
|
||||||
|
- DataAnnotations 验证
|
||||||
|
- 实时验证反馈
|
||||||
|
- 错误消息显示
|
||||||
|
|
||||||
|
**适用场景**:需要用户输入验证的应用
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 4. **04-Navigation** - 导航系统 ⭐⭐
|
||||||
|
**演示内容**:
|
||||||
|
- INavigationService 使用
|
||||||
|
- 多页面导航
|
||||||
|
- 前进/后退功能
|
||||||
|
- 页面间参数传递
|
||||||
|
- 导航生命周期(INavigationAware)
|
||||||
|
|
||||||
|
**适用场景**:多页面 WPF 应用
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 5. **05-Localization** - 国际化 ⭐⭐
|
||||||
|
**演示内容**:
|
||||||
|
- 多语言支持(中文、英文)
|
||||||
|
- LocalizeExtension XAML 绑定
|
||||||
|
- 运行时切换语言
|
||||||
|
- 资源文件管理
|
||||||
|
|
||||||
|
**适用场景**:需要多语言支持的应用
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 6. **06-Theming** - 主题系统 ⭐⭐
|
||||||
|
**演示内容**:
|
||||||
|
- 浅色/深色主题切换
|
||||||
|
- 自定义主题颜色
|
||||||
|
- 跟随系统主题
|
||||||
|
- 动态主题切换
|
||||||
|
|
||||||
|
**适用场景**:需要主题切换的现代应用
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 工业IoT示例
|
||||||
|
|
||||||
|
#### 7. **07-PlcMonitoring** - PLC 监控 ⭐⭐⭐
|
||||||
|
**演示内容**:
|
||||||
|
- 连接西门子 PLC
|
||||||
|
- 实时数据监控
|
||||||
|
- 数据变化订阅
|
||||||
|
- 读写PLC标签
|
||||||
|
- 数据可视化(图表)
|
||||||
|
|
||||||
|
**适用场景**:工业自动化、设备监控
|
||||||
|
|
||||||
|
**技术栈**:Wpf, Plc.Siemens, Cache(缓存历史数据)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 8. **08-SerialPortComm** - 串口通信 ⭐⭐
|
||||||
|
**演示内容**:
|
||||||
|
- 串口自动发现
|
||||||
|
- 数据收发
|
||||||
|
- HEX/ASCII 显示
|
||||||
|
- 数据记录
|
||||||
|
|
||||||
|
**适用场景**:串口设备通信、调试工具
|
||||||
|
|
||||||
|
**技术栈**:Wpf, Ports
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 网络示例
|
||||||
|
|
||||||
|
#### 9. **09-HttpServer** - 嵌入式 Web 服务器 ⭐⭐⭐
|
||||||
|
**演示内容**:
|
||||||
|
- 在 WPF 应用中嵌入 HTTP 服务器
|
||||||
|
- RESTful API 实现
|
||||||
|
- 中间件使用(CORS, 认证, 日志)
|
||||||
|
- WPF 界面管理服务器
|
||||||
|
|
||||||
|
**适用场景**:设备管理后台、本地Web服务
|
||||||
|
|
||||||
|
**技术栈**:Wpf, NetServer, Log
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 10. **10-HttpClient** - HTTP 客户端 ⭐⭐
|
||||||
|
**演示内容**:
|
||||||
|
- 调用外部 API
|
||||||
|
- 重试策略(Polly)
|
||||||
|
- Result<T> 错误处理
|
||||||
|
- 缓存 API 响应
|
||||||
|
|
||||||
|
**适用场景**:需要调用外部服务的应用
|
||||||
|
|
||||||
|
**技术栈**:Wpf, NetClient, Cache
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 综合示例
|
||||||
|
|
||||||
|
#### 11. **11-FullStackDemo** - 完整功能演示 ⭐⭐⭐⭐
|
||||||
|
**演示内容**:
|
||||||
|
- 用户登录(数据库验证)
|
||||||
|
- 产品管理(CRUD)
|
||||||
|
- PLC 数据监控
|
||||||
|
- 串口通信
|
||||||
|
- 数据报表导出
|
||||||
|
- 多语言
|
||||||
|
- 主题切换
|
||||||
|
- 配置热重载
|
||||||
|
|
||||||
|
**适用场景**:工业监控系统、MES系统、数采系统
|
||||||
|
|
||||||
|
**技术栈**:全部模块
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 12. **12-CachingDemo** - 缓存使用 ⭐
|
||||||
|
**演示内容**:
|
||||||
|
- 字典数据缓存
|
||||||
|
- API 响应缓存
|
||||||
|
- 计算结果缓存
|
||||||
|
- 缓存失效策略
|
||||||
|
|
||||||
|
**技术栈**:Wpf, Cache, NetClient
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 13. **13-EventBusDemo** - 事件总线 ⭐
|
||||||
|
**演示内容**:
|
||||||
|
- 跨模块通信
|
||||||
|
- 事件发布/订阅
|
||||||
|
- 解耦架构
|
||||||
|
|
||||||
|
**技术栈**:Wpf, EventBus
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 快速开始
|
||||||
|
|
||||||
|
### 运行示例
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 克隆仓库
|
||||||
|
git clone https://github.com/your-repo/sharp-basic.git
|
||||||
|
cd sharp-basic/examples
|
||||||
|
|
||||||
|
# 2. 选择一个示例
|
||||||
|
cd 01-MinimalWpfApp
|
||||||
|
|
||||||
|
# 3. 还原依赖并运行
|
||||||
|
dotnet restore
|
||||||
|
dotnet run
|
||||||
|
```
|
||||||
|
|
||||||
|
### 从示例创建新项目
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 复制示例作为起点
|
||||||
|
cp -r examples/02-WpfWithDatabase MyNewProject
|
||||||
|
cd MyNewProject
|
||||||
|
|
||||||
|
# 修改项目名称
|
||||||
|
# 编辑 .csproj 文件和命名空间
|
||||||
|
# 开始开发!
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📖 学习路径
|
||||||
|
|
||||||
|
### 新手推荐路径
|
||||||
|
1. **01-MinimalWpfApp** - 了解基础
|
||||||
|
2. **02-WpfWithDatabase** - 学习数据库操作
|
||||||
|
3. **03-FormValidation** - 掌握表单验证
|
||||||
|
4. **04-Navigation** - 理解导航系统
|
||||||
|
|
||||||
|
### 工业自动化开发者
|
||||||
|
1. **07-PlcMonitoring** - PLC 通信
|
||||||
|
2. **08-SerialPortComm** - 串口通信
|
||||||
|
3. **11-FullStackDemo** - 完整系统
|
||||||
|
|
||||||
|
### Web 开发者转 WPF
|
||||||
|
1. **09-HttpServer** - HTTP 服务器
|
||||||
|
2. **10-HttpClient** - HTTP 客户端
|
||||||
|
3. **06-Theming** - 主题系统
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 示例代码说明
|
||||||
|
|
||||||
|
所有示例都遵循:
|
||||||
|
- ✅ **最佳实践**:展示推荐的使用方式
|
||||||
|
- ✅ **完整代码**:可直接运行,无需额外配置
|
||||||
|
- ✅ **详细注释**:中文注释,解释关键代码
|
||||||
|
- ✅ **README**:每个示例都有独立的 README
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ 技术要求
|
||||||
|
|
||||||
|
- .NET 8.0 SDK
|
||||||
|
- Visual Studio 2022 或 VS Code (推荐安装 C# Dev Kit)
|
||||||
|
- Windows 10/11 (WPF 应用需要 Windows)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 贡献示例
|
||||||
|
|
||||||
|
欢迎贡献新的示例!请确保:
|
||||||
|
1. 代码简洁明了
|
||||||
|
2. 包含 README 说明
|
||||||
|
3. 注释使用中文
|
||||||
|
4. 可独立运行
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ❓ 常见问题
|
||||||
|
|
||||||
|
**Q: 示例可以直接用于生产吗?**
|
||||||
|
A: 示例代码主要用于学习,生产使用需要根据实际情况调整(如安全性、错误处理、日志等)。
|
||||||
|
|
||||||
|
**Q: 示例使用什么数据库?**
|
||||||
|
A: 大部分示例使用 SQLite(无需安装,开箱即用),也有 SQL Server 示例。
|
||||||
|
|
||||||
|
**Q: 可以在 .NET Framework 上运行吗?**
|
||||||
|
A: 不可以,Sharp 框架基于 .NET 8,需要 .NET 8 运行时。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**选择一个示例开始你的 Sharp 之旅吧!** 🎉
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,346 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
Sharp-Basic is a modular C# .NET 8 framework for building WPF and C# applications with reusable components. It provides a comprehensive toolkit with emphasis on:
|
||||||
|
- **Modularity**: Independent, reusable libraries that can be used selectively - "引入即用"
|
||||||
|
- **Quick Start**: Zero-configuration setup with sensible defaults - "约定优于配置"
|
||||||
|
- **Desktop Applications**: WPF-based UI toolkit with MVVM infrastructure
|
||||||
|
- **Industrial IoT**: PLC integration (Siemens) and serial port communication (optional)
|
||||||
|
- **Configuration as Code**: Multi-format (JSON/XML/INI/TOML) with hot-reload
|
||||||
|
- **Functional Programming**: Result<T> pattern for railway-oriented error handling
|
||||||
|
- **Opinionated**: Strong technology choices (EF Core, Serilog, etc.) for team consistency
|
||||||
|
|
||||||
|
**Design Philosophy**: "固执己见的框架" - Makes strong choices about technology stack to enable fast development. If you can't use EF Core, this framework may not be for you.
|
||||||
|
|
||||||
|
**Language**: All comments and documentation are in Chinese (中文).
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
**New to Sharp?** See `QUICKSTART.md` for zero-configuration setup guide.
|
||||||
|
|
||||||
|
### Minimal WPF Application (3 lines)
|
||||||
|
```csharp
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
services.AddSharpEssentials(); // Config + Logging + Cache
|
||||||
|
services.AddSharpWpf(); // WPF Services (Dialog, Dispatcher)
|
||||||
|
services.AddViewModel<MainViewModel>();
|
||||||
|
```
|
||||||
|
|
||||||
|
### With Database (1 more line)
|
||||||
|
```csharp
|
||||||
|
services.AddSharpDatabase<AppDbContext>(config); // Reads from appsettings.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build Commands
|
||||||
|
|
||||||
|
### Build the entire solution
|
||||||
|
```bash
|
||||||
|
cd src/Sharp
|
||||||
|
dotnet build Sharp.sln
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build specific project
|
||||||
|
```bash
|
||||||
|
cd src/Sharp
|
||||||
|
dotnet build Sharp.Demo/Sharp.Demo.csproj # WPF demo application (最佳实践示例)
|
||||||
|
dotnet build Common/Common.csproj # Core utilities
|
||||||
|
dotnet build Config/Config.csproj # Configuration management
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run tests
|
||||||
|
```bash
|
||||||
|
cd src/Sharp
|
||||||
|
dotnet test Sharp.sln # All tests
|
||||||
|
dotnet test ../../test/Common.Test # Specific test project
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run the Sharp.Demo application
|
||||||
|
```bash
|
||||||
|
cd src/Sharp
|
||||||
|
dotnet run --project Sharp.Demo/Sharp.Demo.csproj
|
||||||
|
```
|
||||||
|
|
||||||
|
### Clean build artifacts
|
||||||
|
```bash
|
||||||
|
cd src/Sharp
|
||||||
|
dotnet clean Sharp.sln
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
### Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/Sharp/ # All source projects
|
||||||
|
├── Common/ # Core foundation (Result<T>, Global container, utilities, SystemConfig)
|
||||||
|
├── Config/ # Multi-format configuration with hot-reload
|
||||||
|
├── Wpf/ # WPF UI toolkit (MVVM, dialogs, converters) - NO UI library dependency
|
||||||
|
├── Wpf.HandyControl/ # HandyControl UI library integration (optional)
|
||||||
|
├── Wpf.DevTools/ # WPF Developer Tools (DevTools, LogViewer, Performance Monitor)
|
||||||
|
├── Data/ # EF Core wrapper with repository pattern
|
||||||
|
├── Log/ # Serilog-based structured logging
|
||||||
|
├── Cache/ # Memory cache (planned)
|
||||||
|
├── EventBus/ # Event pub/sub (planned)
|
||||||
|
├── NetClient/ # HTTP client with Polly resilience
|
||||||
|
├── NetServer/ # Lightweight HTTP server with middleware
|
||||||
|
├── Ports/ # Serial port abstraction (RS232/RS485)
|
||||||
|
├── Plc/ # PLC abstraction interface
|
||||||
|
├── Plc.Siemens/ # Siemens S7 PLC implementation (S7.NET+)
|
||||||
|
├── Process/ # Process execution
|
||||||
|
├── Python/ # Python script execution
|
||||||
|
└── Sharp.Demo/ # 综合演示应用(最佳实践示例,替代了 Client 和 Server)
|
||||||
|
|
||||||
|
test/ # MSTest test projects
|
||||||
|
├── Common.Test/
|
||||||
|
├── Config.Test/
|
||||||
|
├── Data.Test/
|
||||||
|
├── Log.Test/
|
||||||
|
├── NetClient.Test/
|
||||||
|
└── NetServer.Test/
|
||||||
|
|
||||||
|
../../appsettings.template.json # 完整的配置模板文件(约定配置节名称)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Architectural Patterns
|
||||||
|
|
||||||
|
#### 1. Result<T> Pattern (Railway-Oriented Programming)
|
||||||
|
- Eliminates exceptions as control flow
|
||||||
|
- Provides chainable operations: `Map`, `Bind`, `Match`, `Where`, `OnSuccess`, `OnFailure`
|
||||||
|
- Synchronous and async wrappers: `Result.Try()`, `Result.TryAsync()`
|
||||||
|
- Example usage:
|
||||||
|
```csharp
|
||||||
|
Result.Try(() => int.Parse(input))
|
||||||
|
.Map(x => x * 2)
|
||||||
|
.Where(v => v > 100, "Value too small")
|
||||||
|
.OnSuccess(v => Console.WriteLine(v))
|
||||||
|
.OnFailure((ex, msg) => logger.LogError(msg));
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Dependency Injection (Hybrid Approach)
|
||||||
|
- **Primary**: Microsoft.Extensions.DependencyInjection for standard DI
|
||||||
|
- **Fallback**: Global service locator (Common.Global) for legacy/convenience
|
||||||
|
- Services registered in `DependencyInjection` folders within each module
|
||||||
|
- Sharp.Demo app startup (App.xaml.cs) orchestrates all service registration
|
||||||
|
|
||||||
|
#### 3. Configuration Management
|
||||||
|
- Multi-source configuration builder supporting JSON/XML/INI/TOML
|
||||||
|
- Hot-reload with change notifications
|
||||||
|
- Strong typing with DataAnnotations validation
|
||||||
|
- IOptions<T> pattern integration
|
||||||
|
- Environment variable overrides with optional prefix
|
||||||
|
|
||||||
|
#### 4. Repository Pattern with EF Core
|
||||||
|
- Generic `IRepository<TEntity>` abstraction
|
||||||
|
- Built-in entity types: `Entity`, `AuditableEntity`, `ISoftDeletable`
|
||||||
|
- Auto-audit tracking (CreatedAt, CreatedBy, UpdatedAt, UpdatedBy)
|
||||||
|
- Multi-database support: SQL Server, SQLite, PostgreSQL, MySQL, In-Memory
|
||||||
|
|
||||||
|
#### 5. MVVM for WPF
|
||||||
|
- `ViewModelBase` with INotifyPropertyChanged and IsBusy tracking
|
||||||
|
- `RelayCommand` and `AsyncCommand` implementations
|
||||||
|
- Services: `IDialogService`, `IFileDialogService`, `IDispatcherService`
|
||||||
|
- Converters: BoolToVisibility, InverseBooleanConverter, EnumToDescriptionConverter
|
||||||
|
- Global exception handling for UI/Task/AppDomain exceptions
|
||||||
|
|
||||||
|
#### 6. Middleware Pattern
|
||||||
|
- Custom HTTP server (NetServer) with ASP.NET Core-style pipeline
|
||||||
|
- Middleware: Authentication, CORS, Rate Limiting, Caching, Logging, Exception Handling
|
||||||
|
- Attribute-based routing with `RouteAttribute`
|
||||||
|
- Custom `IMiddleware` interface for extensibility
|
||||||
|
|
||||||
|
## Module Dependencies
|
||||||
|
|
||||||
|
**Core Layer** (no external dependencies):
|
||||||
|
- Common → (no dependencies)
|
||||||
|
|
||||||
|
**Configuration Layer**:
|
||||||
|
- Config → Microsoft.Extensions.Configuration.*, Tomlyn (TOML support)
|
||||||
|
|
||||||
|
**Data Layer**:
|
||||||
|
- Data → EF Core, Common
|
||||||
|
- Repository → Data
|
||||||
|
|
||||||
|
**Network Layer**:
|
||||||
|
- NetClient → Polly, Microsoft.Extensions.Http, Common
|
||||||
|
- NetServer → Common, Log
|
||||||
|
|
||||||
|
**WPF Layer**:
|
||||||
|
- Wpf → (no UI library dependency - pure MVVM)
|
||||||
|
- Wpf.HandyControl → Wpf, HandyControl 3.5.0 (optional UI library integration)
|
||||||
|
- Wpf.DevTools → Wpf (Developer Tools: DevTools, LogViewer, Performance Monitor)
|
||||||
|
- Sharp.Demo → Wpf, Wpf.HandyControl, Wpf.DevTools, Config, NetClient, NetServer, Log, Data, Plc.Siemens, Ports, Process, Python
|
||||||
|
|
||||||
|
**Industrial IoT**:
|
||||||
|
- Ports → (serial port communication)
|
||||||
|
- Plc → (abstraction only)
|
||||||
|
- Plc.Siemens → Plc, S7.NET+ library
|
||||||
|
|
||||||
|
**Logging**:
|
||||||
|
- Log → Serilog, Microsoft.Extensions.Logging
|
||||||
|
|
||||||
|
## Common Development Workflows
|
||||||
|
|
||||||
|
### Quick Start for New Projects (Recommended)
|
||||||
|
1. Install NuGet packages (or reference projects)
|
||||||
|
2. Create `appsettings.json` in project root
|
||||||
|
3. Use Quick Start extensions in `App.xaml.cs`:
|
||||||
|
```csharp
|
||||||
|
protected override void OnStartup(StartupEventArgs e) {
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
|
||||||
|
// Option 1: Minimal (Config + Log + Cache + WPF)
|
||||||
|
services.AddSharpEssentials();
|
||||||
|
services.AddSharpWpf();
|
||||||
|
|
||||||
|
// Option 2: With Database
|
||||||
|
var config = ConfigBuilder.CreateDefault().Configuration;
|
||||||
|
services.AddSharpEssentials(config);
|
||||||
|
services.AddSharpDatabase<AppDbContext>(config); // Reads from appsettings.json
|
||||||
|
services.AddSharpWpf();
|
||||||
|
|
||||||
|
// Register ViewModels
|
||||||
|
services.AddViewModel<MainViewModel>();
|
||||||
|
|
||||||
|
var provider = services.BuildServiceProvider();
|
||||||
|
// Show main window...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
4. See `QUICKSTART.md` for complete examples
|
||||||
|
|
||||||
|
### Adding a new module/library
|
||||||
|
1. Create project under `src/Sharp/`
|
||||||
|
2. Target `net8.0` (or `net8.0-windows` for WPF)
|
||||||
|
3. Enable nullable reference types: `<Nullable>enable</Nullable>`
|
||||||
|
4. Create `DependencyInjection` folder for service registration extensions with `AddSharpXxx()` methods
|
||||||
|
5. Create comprehensive `README.md` with Quick Start section
|
||||||
|
6. Add test project under `test/` with MSTest
|
||||||
|
7. Add to Sharp.sln: `dotnet sln add <ProjectPath>`
|
||||||
|
|
||||||
|
### Working with Result<T>
|
||||||
|
- Use `Result.Try()` to wrap exception-prone code
|
||||||
|
- Chain operations with `Map()` for transformations
|
||||||
|
- Use `Bind()` for operations that return Result<T>
|
||||||
|
- Extract values with `GetOrDefault()`, `GetOrThrow()`, or `Match()`
|
||||||
|
- Always handle both success and failure cases
|
||||||
|
|
||||||
|
### Using the Global container
|
||||||
|
- Register singletons: `Global.RegisterSingleton<T>(instance)` or `Global.RegisterSingleton<T>(() => new T())`
|
||||||
|
- Register transients: `Global.RegisterTransient<T>(() => new T())`
|
||||||
|
- Subscribe to changes: `Global.Subscribe<T>(e => { /* handle change */ })`
|
||||||
|
- Retrieve services: `Global.Get<T>()`
|
||||||
|
- **Note**: Prefer standard DI (IServiceCollection) for new code; Global is for legacy/compatibility
|
||||||
|
|
||||||
|
### Configuration workflow (Quick Start)
|
||||||
|
1. Create `appsettings.json` in project root
|
||||||
|
2. Use default configuration:
|
||||||
|
```csharp
|
||||||
|
var config = ConfigBuilder.CreateDefault().Configuration;
|
||||||
|
```
|
||||||
|
3. Configuration automatically loaded with:
|
||||||
|
- Hot-reload enabled
|
||||||
|
- Environment variables (prefix: `APP_`)
|
||||||
|
- Environment-specific files (appsettings.Development.json, etc.)
|
||||||
|
|
||||||
|
### Configuration workflow (Manual)
|
||||||
|
1. Add config files to project root (appsettings.json, etc.)
|
||||||
|
2. Create strongly-typed options class with DataAnnotations
|
||||||
|
3. Build ConfigManager: `ConfigBuilder.BuildFromFiles(...)`
|
||||||
|
4. Register with DI: `services.AddSingleton(cfg)` and `services.AddSingleton(cfg.Configuration)`
|
||||||
|
5. Use hot-reload callbacks: `cfg.OnReload(() => { /* refresh */ })`
|
||||||
|
|
||||||
|
### WPF Sharp.Demo startup flow (Modern Quick Start)
|
||||||
|
```csharp
|
||||||
|
protected override void OnStartup(StartupEventArgs e) {
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
var config = ConfigBuilderExtensions.CreateDefault().Configuration;
|
||||||
|
|
||||||
|
services.AddSharpEssentials(config);
|
||||||
|
services.AddSharpWpf();
|
||||||
|
services.AddSharpHandyControl();
|
||||||
|
services.AddSharpDevTools();
|
||||||
|
services.AddViewModel<LauncherViewModel>();
|
||||||
|
services.AddTransient<LauncherWindow>();
|
||||||
|
|
||||||
|
var provider = services.BuildServiceProvider();
|
||||||
|
provider.GetRequiredService<LauncherWindow>().Show();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Working with PLC (Siemens)
|
||||||
|
- Interface: `IPlcService` in Plc project
|
||||||
|
- Implementation: Plc.Siemens using S7.NET+ library
|
||||||
|
- Supports: S7-1200, S7-1500, S7-300, S7-400
|
||||||
|
- Register with DI: `services.AddSiemensPlc(config.GetSection("Plc:Siemens"))`
|
||||||
|
- Methods: Read/Write memory areas, data blocks, Subscribe for monitoring
|
||||||
|
|
||||||
|
### Serial Port Communication
|
||||||
|
- Service: `ISerialPortService` in Ports module
|
||||||
|
- Discovery: `SerialPortDiscovery` for auto-detection
|
||||||
|
- Event-based data reception: `DataReceivedEventArgs`
|
||||||
|
- Configured with: baud rate, parity, stop bits, data bits
|
||||||
|
|
||||||
|
### Using Cache (When Implemented)
|
||||||
|
```csharp
|
||||||
|
// Dictionary data caching
|
||||||
|
var categories = await _cache.GetOrSetAsync(
|
||||||
|
"categories:all",
|
||||||
|
() => _repository.GetAllAsync(),
|
||||||
|
TimeSpan.FromMinutes(30)
|
||||||
|
);
|
||||||
|
|
||||||
|
// API response caching
|
||||||
|
var weather = await _cache.GetOrSetAsync(
|
||||||
|
$"weather:{city}",
|
||||||
|
() => _apiClient.GetWeatherAsync(city),
|
||||||
|
TimeSpan.FromMinutes(5)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using EventBus (When Implemented)
|
||||||
|
```csharp
|
||||||
|
// Publish
|
||||||
|
_eventBus.Publish(new UserLoginEvent(username, DateTime.Now));
|
||||||
|
|
||||||
|
// Subscribe
|
||||||
|
_eventBus.Subscribe<UserLoginEvent>(e => {
|
||||||
|
WelcomeMessage = $"欢迎, {e.Username}!";
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Code Style
|
||||||
|
|
||||||
|
- **Target Framework**: .NET 8 (net8.0 / net8.0-windows)
|
||||||
|
- **Nullable**: Enabled (`#nullable enable`)
|
||||||
|
- **Implicit Usings**: Enabled
|
||||||
|
- **Namespace Convention**: `Sharp.*` (Common will migrate to `Sharp.Common`)
|
||||||
|
- **Language**: All comments, docs, and strings in Chinese
|
||||||
|
- **Backwards Compatibility**: Maintain for minor versions; breaking changes only in major versions
|
||||||
|
- **Testing**: MSTest with parallel execution enabled
|
||||||
|
|
||||||
|
## Design Principles
|
||||||
|
|
||||||
|
1. **"引入即用" (Plug and Play)**: Every module should work with minimal configuration
|
||||||
|
2. **"约定优于配置" (Convention over Configuration)**: Sensible defaults for everything
|
||||||
|
3. **"固执己见" (Opinionated)**: Strong technology choices (EF Core, Serilog, HandyControl)
|
||||||
|
- If you can't use our stack, this framework is not for you
|
||||||
|
- Designed for team/personal consistency, not maximum flexibility
|
||||||
|
4. **Quick Start First**: Every module README must have a "Quick Start" section showing 1-line setup
|
||||||
|
5. **No Abstraction Overkill**: Don't create interfaces just for flexibility - commit to implementations
|
||||||
|
|
||||||
|
## Module README Template
|
||||||
|
|
||||||
|
Every module must have a README with:
|
||||||
|
1. **Quick Start** section (5-second setup)
|
||||||
|
2. **Why You Need This** section (problem/solution)
|
||||||
|
3. **Core API** section (main interfaces/classes)
|
||||||
|
4. **Typical Scenarios** section (3-5 real-world examples)
|
||||||
|
5. **Configuration** section (if applicable)
|
||||||
|
|
||||||
|
See `EventBus/README.md` and `Cache/README.md` for examples (even though not implemented yet).
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
GNU AGPLv3 - Network services that use this framework must open source modifications. Contact author for alternative commercial licensing.
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<AssemblyName>Sharp.Cache</AssemblyName>
|
||||||
|
<RootNamespace>Sharp.Cache</RootNamespace>
|
||||||
|
|
||||||
|
<!-- 程序集元数据 -->
|
||||||
|
<Description>Sharp Framework 缓存库 - 内存缓存抽象和实现(规划中)</Description>
|
||||||
|
|
||||||
|
<!-- NuGet 包元数据 -->
|
||||||
|
<PackageTags>sharp;cache;memory-cache;caching</PackageTags>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Sharp.Cache;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 缓存扩展方法
|
||||||
|
/// </summary>
|
||||||
|
public static class CacheExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取缓存,不存在则执行工厂方法并缓存结果(同步)
|
||||||
|
/// </summary>
|
||||||
|
public static T GetOrSet<T>(
|
||||||
|
this ICacheService cache,
|
||||||
|
string key,
|
||||||
|
Func<T> factory,
|
||||||
|
TimeSpan? expiration = null)
|
||||||
|
{
|
||||||
|
if (cache.TryGet<T>(key, out var cached))
|
||||||
|
{
|
||||||
|
return cached!;
|
||||||
|
}
|
||||||
|
|
||||||
|
var value = factory();
|
||||||
|
cache.Set(key, value, expiration);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取缓存,不存在则执行工厂方法并缓存结果(异步)
|
||||||
|
/// </summary>
|
||||||
|
public static async Task<T> GetOrSetAsync<T>(
|
||||||
|
this ICacheService cache,
|
||||||
|
string key,
|
||||||
|
Func<Task<T>> factory,
|
||||||
|
TimeSpan? expiration = null)
|
||||||
|
{
|
||||||
|
if (cache.TryGet<T>(key, out var cached))
|
||||||
|
{
|
||||||
|
return cached!;
|
||||||
|
}
|
||||||
|
|
||||||
|
var value = await factory();
|
||||||
|
cache.Set(key, value, expiration);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置(滑动过期)
|
||||||
|
/// </summary>
|
||||||
|
public static T GetOrSetSliding<T>(
|
||||||
|
this ICacheService cache,
|
||||||
|
string key,
|
||||||
|
Func<T> factory,
|
||||||
|
TimeSpan slidingExpiration)
|
||||||
|
{
|
||||||
|
if (cache.TryGet<T>(key, out var cached))
|
||||||
|
{
|
||||||
|
return cached!;
|
||||||
|
}
|
||||||
|
|
||||||
|
var value = factory();
|
||||||
|
cache.SetSliding(key, value, slidingExpiration);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 批量移除(支持通配符)
|
||||||
|
/// </summary>
|
||||||
|
public static void RemoveByPattern(this ICacheService cache, string pattern)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(pattern))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!pattern.EndsWith("*", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
cache.Remove(pattern);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var prefix = pattern.TrimEnd('*');
|
||||||
|
cache.RemoveByPrefix(prefix);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Sharp.Cache.DependencyInjection;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 缓存服务注册扩展
|
||||||
|
/// </summary>
|
||||||
|
public static class CacheServiceExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 添加 Sharp 内存缓存服务
|
||||||
|
/// </summary>
|
||||||
|
public static IServiceCollection AddSharpCache(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
// 注册 MemoryCache
|
||||||
|
services.AddMemoryCache();
|
||||||
|
|
||||||
|
// 注册 ICacheService
|
||||||
|
services.AddSingleton<ICacheService, MemoryCacheService>();
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 添加 Sharp 内存缓存服务(带配置)
|
||||||
|
/// </summary>
|
||||||
|
public static IServiceCollection AddSharpCache(
|
||||||
|
this IServiceCollection services,
|
||||||
|
Action<Microsoft.Extensions.Caching.Memory.MemoryCacheOptions> configure)
|
||||||
|
{
|
||||||
|
// 注册 MemoryCache 并配置
|
||||||
|
services.AddMemoryCache(configure);
|
||||||
|
|
||||||
|
// 注册 ICacheService
|
||||||
|
services.AddSingleton<ICacheService, MemoryCacheService>();
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
|
||||||
|
namespace Sharp.Cache;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 缓存服务接口
|
||||||
|
/// </summary>
|
||||||
|
public interface ICacheService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取缓存值
|
||||||
|
/// </summary>
|
||||||
|
T? Get<T>(string key);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 尝试获取缓存值
|
||||||
|
/// </summary>
|
||||||
|
bool TryGet<T>(string key, out T? value);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设置缓存
|
||||||
|
/// </summary>
|
||||||
|
void Set<T>(string key, T value, TimeSpan? expiration = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设置缓存(滑动过期)
|
||||||
|
/// </summary>
|
||||||
|
void SetSliding<T>(string key, T value, TimeSpan slidingExpiration);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 移除缓存
|
||||||
|
/// </summary>
|
||||||
|
void Remove(string key);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 清空所有缓存
|
||||||
|
/// </summary>
|
||||||
|
void Clear();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 按前缀批量移除
|
||||||
|
/// </summary>
|
||||||
|
void RemoveByPrefix(string prefix);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 已缓存键集合(只读)
|
||||||
|
/// </summary>
|
||||||
|
IReadOnlyCollection<string> Keys { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 检查缓存是否存在
|
||||||
|
/// </summary>
|
||||||
|
bool Exists(string key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 基于 MemoryCache 的缓存服务实现
|
||||||
|
/// </summary>
|
||||||
|
public class MemoryCacheService : ICacheService
|
||||||
|
{
|
||||||
|
private readonly IMemoryCache _cache;
|
||||||
|
private readonly HashSet<string> _keys = new(); // 跟踪所有键(用于 Clear)
|
||||||
|
private readonly object _lockObj = new();
|
||||||
|
|
||||||
|
public MemoryCacheService(IMemoryCache cache)
|
||||||
|
{
|
||||||
|
_cache = cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
public T? Get<T>(string key)
|
||||||
|
{
|
||||||
|
return _cache.Get<T>(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGet<T>(string key, out T? value)
|
||||||
|
{
|
||||||
|
return _cache.TryGetValue(key, out value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Set<T>(string key, T value, TimeSpan? expiration = null)
|
||||||
|
{
|
||||||
|
var options = new MemoryCacheEntryOptions
|
||||||
|
{
|
||||||
|
AbsoluteExpirationRelativeToNow = expiration ?? TimeSpan.FromHours(1)
|
||||||
|
};
|
||||||
|
|
||||||
|
options.RegisterPostEvictionCallback((k, v, r, s) =>
|
||||||
|
{
|
||||||
|
lock (_lockObj)
|
||||||
|
{
|
||||||
|
_keys.Remove(k.ToString() ?? "");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_cache.Set(key, value, options);
|
||||||
|
|
||||||
|
lock (_lockObj)
|
||||||
|
{
|
||||||
|
_keys.Add(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetSliding<T>(string key, T value, TimeSpan slidingExpiration)
|
||||||
|
{
|
||||||
|
var options = new MemoryCacheEntryOptions
|
||||||
|
{
|
||||||
|
SlidingExpiration = slidingExpiration
|
||||||
|
};
|
||||||
|
|
||||||
|
options.RegisterPostEvictionCallback((k, v, r, s) =>
|
||||||
|
{
|
||||||
|
lock (_lockObj)
|
||||||
|
{
|
||||||
|
_keys.Remove(k.ToString() ?? "");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_cache.Set(key, value, options);
|
||||||
|
|
||||||
|
lock (_lockObj)
|
||||||
|
{
|
||||||
|
_keys.Add(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Remove(string key)
|
||||||
|
{
|
||||||
|
_cache.Remove(key);
|
||||||
|
|
||||||
|
lock (_lockObj)
|
||||||
|
{
|
||||||
|
_keys.Remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
lock (_lockObj)
|
||||||
|
{
|
||||||
|
foreach (var key in _keys.ToList())
|
||||||
|
{
|
||||||
|
_cache.Remove(key);
|
||||||
|
}
|
||||||
|
_keys.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveByPrefix(string prefix)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(prefix))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<string> targets;
|
||||||
|
lock (_lockObj)
|
||||||
|
{
|
||||||
|
targets = _keys.Where(k => k.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var key in targets)
|
||||||
|
{
|
||||||
|
Remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyCollection<string> Keys
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_lockObj)
|
||||||
|
{
|
||||||
|
return _keys.ToList().AsReadOnly();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Exists(string key)
|
||||||
|
{
|
||||||
|
return _cache.TryGetValue(key, out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,623 @@
|
|||||||
|
# Sharp.Cache
|
||||||
|
|
||||||
|
> **🚧 开发中 / Under Construction**
|
||||||
|
|
||||||
|
轻量级内存缓存模块,基于 `Microsoft.Extensions.Caching.Memory`,提供简洁的缓存API。
|
||||||
|
|
||||||
|
## 📋 为什么需要 Cache?
|
||||||
|
|
||||||
|
### 场景对比
|
||||||
|
|
||||||
|
#### ❌ 不使用 Cache - 重复查询
|
||||||
|
```csharp
|
||||||
|
public class ProductService {
|
||||||
|
private readonly IRepository<Category> _categoryRepo;
|
||||||
|
|
||||||
|
// 问题:每次打开编辑对话框都查询数据库
|
||||||
|
public async Task<ProductEditViewModel> GetEditViewModelAsync(int productId) {
|
||||||
|
var product = await _repository.GetByIdAsync(productId);
|
||||||
|
|
||||||
|
// 分类数据很少变化,但每次都查询
|
||||||
|
var categories = await _categoryRepo.GetAllAsync();
|
||||||
|
|
||||||
|
return new ProductEditViewModel {
|
||||||
|
Product = product,
|
||||||
|
Categories = categories
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### ✅ 使用 Cache - 智能缓存
|
||||||
|
```csharp
|
||||||
|
public class ProductService {
|
||||||
|
private readonly IRepository<Category> _categoryRepo;
|
||||||
|
private readonly ICacheService _cache;
|
||||||
|
|
||||||
|
public async Task<ProductEditViewModel> GetEditViewModelAsync(int productId) {
|
||||||
|
var product = await _repository.GetByIdAsync(productId);
|
||||||
|
|
||||||
|
// 从缓存获取,不存在则查询并缓存30分钟
|
||||||
|
var categories = await _cache.GetOrSetAsync(
|
||||||
|
"categories:all",
|
||||||
|
() => _categoryRepo.GetAllAsync(),
|
||||||
|
TimeSpan.FromMinutes(30)
|
||||||
|
);
|
||||||
|
|
||||||
|
return new ProductEditViewModel {
|
||||||
|
Product = product,
|
||||||
|
Categories = categories
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**效果**:
|
||||||
|
- 第一次:查询数据库(耗时 50ms)
|
||||||
|
- 后续访问:从内存读取(耗时 < 1ms)
|
||||||
|
- 用户体验:界面秒开,不再等待
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 设计目标
|
||||||
|
|
||||||
|
1. **极简API** - 一行代码搞定90%的缓存需求
|
||||||
|
2. **类型安全** - 强类型缓存,编译期检查
|
||||||
|
3. **自动过期** - 支持绝对过期和滑动过期
|
||||||
|
4. **引入即用** - 基于 .NET 内置 MemoryCache,零配置
|
||||||
|
5. **线程安全** - 完全线程安全
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 快速开始(未来)
|
||||||
|
|
||||||
|
### 1. 注册服务
|
||||||
|
```csharp
|
||||||
|
// App.xaml.cs
|
||||||
|
protected override void OnStartup(StartupEventArgs e) {
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
|
||||||
|
// 一行注册(基于 Microsoft.Extensions.Caching.Memory)
|
||||||
|
services.AddMemoryCache();
|
||||||
|
|
||||||
|
var provider = services.BuildServiceProvider();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 注入使用
|
||||||
|
```csharp
|
||||||
|
public class WeatherService {
|
||||||
|
private readonly ICacheService _cache;
|
||||||
|
private readonly IHttpClient _httpClient;
|
||||||
|
|
||||||
|
public WeatherService(ICacheService cache, IHttpClient httpClient) {
|
||||||
|
_cache = cache;
|
||||||
|
_httpClient = httpClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Weather> GetWeatherAsync(string city) {
|
||||||
|
// 缓存5分钟,避免频繁调用外部API
|
||||||
|
return await _cache.GetOrSetAsync(
|
||||||
|
$"weather:{city}",
|
||||||
|
() => _httpClient.GetAsync<Weather>($"/api/weather?city={city}"),
|
||||||
|
TimeSpan.FromMinutes(5)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 核心 API 设计
|
||||||
|
|
||||||
|
### ICacheService 接口
|
||||||
|
```csharp
|
||||||
|
public interface ICacheService {
|
||||||
|
/// <summary>
|
||||||
|
/// 获取缓存值
|
||||||
|
/// </summary>
|
||||||
|
T? Get<T>(string key);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 尝试获取缓存值
|
||||||
|
/// </summary>
|
||||||
|
bool TryGet<T>(string key, out T? value);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设置缓存
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">缓存键</param>
|
||||||
|
/// <param name="value">缓存值</param>
|
||||||
|
/// <param name="expiration">绝对过期时间(从现在开始计算)</param>
|
||||||
|
void Set<T>(string key, T value, TimeSpan? expiration = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设置缓存(滑动过期)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">缓存键</param>
|
||||||
|
/// <param name="value">缓存值</param>
|
||||||
|
/// <param name="slidingExpiration">滑动过期时间(最后访问后N时间过期)</param>
|
||||||
|
void SetSliding<T>(string key, T value, TimeSpan slidingExpiration);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 移除缓存
|
||||||
|
/// </summary>
|
||||||
|
void Remove(string key);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 清空所有缓存
|
||||||
|
/// </summary>
|
||||||
|
void Clear();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 检查缓存是否存在
|
||||||
|
/// </summary>
|
||||||
|
bool Exists(string key);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 扩展方法(最常用)
|
||||||
|
```csharp
|
||||||
|
public static class CacheExtensions {
|
||||||
|
/// <summary>
|
||||||
|
/// 获取缓存,不存在则执行工厂方法并缓存结果(同步)
|
||||||
|
/// </summary>
|
||||||
|
public static T GetOrSet<T>(
|
||||||
|
this ICacheService cache,
|
||||||
|
string key,
|
||||||
|
Func<T> factory,
|
||||||
|
TimeSpan? expiration = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取缓存,不存在则执行工厂方法并缓存结果(异步)
|
||||||
|
/// </summary>
|
||||||
|
public static Task<T> GetOrSetAsync<T>(
|
||||||
|
this ICacheService cache,
|
||||||
|
string key,
|
||||||
|
Func<Task<T>> factory,
|
||||||
|
TimeSpan? expiration = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置(滑动过期)
|
||||||
|
/// </summary>
|
||||||
|
public static T GetOrSetSliding<T>(
|
||||||
|
this ICacheService cache,
|
||||||
|
string key,
|
||||||
|
Func<T> factory,
|
||||||
|
TimeSpan slidingExpiration);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 批量移除(支持通配符)
|
||||||
|
/// </summary>
|
||||||
|
public static void RemoveByPattern(
|
||||||
|
this ICacheService cache,
|
||||||
|
string pattern); // 如 "user:*"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 典型使用场景
|
||||||
|
|
||||||
|
### 场景1:字典数据缓存(最常见)
|
||||||
|
```csharp
|
||||||
|
public class UserViewModel : ViewModelBase {
|
||||||
|
private readonly ICacheService _cache;
|
||||||
|
private readonly IRepository<Department> _deptRepo;
|
||||||
|
|
||||||
|
public async Task LoadDepartmentsAsync() {
|
||||||
|
// 部门数据很少变化,缓存1小时
|
||||||
|
Departments = await _cache.GetOrSetAsync(
|
||||||
|
"departments:all",
|
||||||
|
() => _deptRepo.GetAllAsync(),
|
||||||
|
TimeSpan.FromHours(1)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnDepartmentUpdated() {
|
||||||
|
// 手动清除缓存,下次会重新加载
|
||||||
|
_cache.Remove("departments:all");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 场景2:API响应缓存
|
||||||
|
```csharp
|
||||||
|
public class WeatherService {
|
||||||
|
private readonly ICacheService _cache;
|
||||||
|
private readonly IHttpClient _httpClient;
|
||||||
|
|
||||||
|
// 每个城市的天气数据缓存5分钟
|
||||||
|
public async Task<Weather> GetWeatherAsync(string city) {
|
||||||
|
return await _cache.GetOrSetAsync(
|
||||||
|
$"weather:{city}",
|
||||||
|
async () => {
|
||||||
|
var response = await _httpClient.GetAsync<WeatherApiResponse>(
|
||||||
|
$"https://api.weather.com/v1/current?city={city}");
|
||||||
|
return response.ToWeather();
|
||||||
|
},
|
||||||
|
TimeSpan.FromMinutes(5)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 场景3:计算结果缓存
|
||||||
|
```csharp
|
||||||
|
public class ReportService {
|
||||||
|
private readonly ICacheService _cache;
|
||||||
|
|
||||||
|
// 月度报表计算耗时,缓存24小时
|
||||||
|
public async Task<MonthlyReport> GetMonthlyReportAsync(int year, int month) {
|
||||||
|
return await _cache.GetOrSetAsync(
|
||||||
|
$"report:monthly:{year}:{month}",
|
||||||
|
async () => {
|
||||||
|
// 复杂的统计计算
|
||||||
|
var data = await _repository.Query()
|
||||||
|
.Where(x => x.Date.Year == year && x.Date.Month == month)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return CalculateReport(data); // 耗时操作
|
||||||
|
},
|
||||||
|
TimeSpan.FromHours(24)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当有新数据时,清除缓存
|
||||||
|
public void OnDataAdded(DateTime date) {
|
||||||
|
_cache.Remove($"report:monthly:{date.Year}:{date.Month}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 场景4:用户权限缓存
|
||||||
|
```csharp
|
||||||
|
public class AuthorizationService {
|
||||||
|
private readonly ICacheService _cache;
|
||||||
|
|
||||||
|
// 用户权限缓存(滑动过期:30分钟内无访问则过期)
|
||||||
|
public async Task<List<string>> GetUserPermissionsAsync(int userId) {
|
||||||
|
return await _cache.GetOrSetSliding(
|
||||||
|
$"user:permissions:{userId}",
|
||||||
|
async () => {
|
||||||
|
return await _repository.Query<UserPermission>()
|
||||||
|
.Where(x => x.UserId == userId)
|
||||||
|
.Select(x => x.PermissionCode)
|
||||||
|
.ToListAsync();
|
||||||
|
},
|
||||||
|
TimeSpan.FromMinutes(30) // 滑动过期
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 场景5:配置数据缓存
|
||||||
|
```csharp
|
||||||
|
public class SystemConfigService {
|
||||||
|
private readonly ICacheService _cache;
|
||||||
|
private readonly IConfigManager _configManager;
|
||||||
|
|
||||||
|
public T GetConfig<T>(string key) where T : class, new() {
|
||||||
|
return _cache.GetOrSet(
|
||||||
|
$"config:{key}",
|
||||||
|
() => _configManager.Get<T>(key),
|
||||||
|
TimeSpan.FromMinutes(10)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config模块有热重载,可以监听变化并清除缓存
|
||||||
|
public void OnConfigChanged(string key) {
|
||||||
|
_cache.Remove($"config:{key}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 高级功能
|
||||||
|
|
||||||
|
### 1. 缓存键约定
|
||||||
|
```csharp
|
||||||
|
// 推荐的缓存键命名规范
|
||||||
|
public static class CacheKeys {
|
||||||
|
// 格式: {模块}:{实体}:{参数}
|
||||||
|
public static string UserById(int userId) => $"user:profile:{userId}";
|
||||||
|
public static string AllDepartments => "departments:all";
|
||||||
|
public static string WeatherByCity(string city) => $"weather:{city}";
|
||||||
|
public static string ReportMonthly(int year, int month) => $"report:monthly:{year}:{month}";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用
|
||||||
|
var user = await _cache.GetOrSetAsync(
|
||||||
|
CacheKeys.UserById(userId),
|
||||||
|
() => _userRepo.GetByIdAsync(userId),
|
||||||
|
TimeSpan.FromMinutes(10)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. 缓存依赖(级联清除)
|
||||||
|
```csharp
|
||||||
|
public class ProductService {
|
||||||
|
// 当产品更新时,清除相关缓存
|
||||||
|
public async Task UpdateProductAsync(Product product) {
|
||||||
|
await _repository.UpdateAsync(product);
|
||||||
|
|
||||||
|
// 清除产品详情缓存
|
||||||
|
_cache.Remove($"product:{product.Id}");
|
||||||
|
|
||||||
|
// 清除产品列表缓存(可能包含此产品)
|
||||||
|
_cache.RemoveByPattern("products:list:*");
|
||||||
|
|
||||||
|
// 清除分类统计缓存
|
||||||
|
_cache.Remove($"category:stats:{product.CategoryId}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. 缓存预热
|
||||||
|
```csharp
|
||||||
|
public class CacheWarmupService {
|
||||||
|
private readonly ICacheService _cache;
|
||||||
|
|
||||||
|
// 应用启动时预热常用数据
|
||||||
|
public async Task WarmupAsync() {
|
||||||
|
// 加载所有字典数据
|
||||||
|
await _cache.GetOrSetAsync(
|
||||||
|
"departments:all",
|
||||||
|
() => _deptRepo.GetAllAsync(),
|
||||||
|
TimeSpan.FromHours(1)
|
||||||
|
);
|
||||||
|
|
||||||
|
await _cache.GetOrSetAsync(
|
||||||
|
"categories:all",
|
||||||
|
() => _categoryRepo.GetAllAsync(),
|
||||||
|
TimeSpan.FromHours(1)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 加载系统配置
|
||||||
|
_cache.GetOrSet(
|
||||||
|
"system:config",
|
||||||
|
() => _configManager.Get<SystemConfig>(),
|
||||||
|
TimeSpan.FromMinutes(10)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// App.xaml.cs
|
||||||
|
protected override async void OnStartup(StartupEventArgs e) {
|
||||||
|
// ...
|
||||||
|
var warmup = _serviceProvider.GetRequiredService<CacheWarmupService>();
|
||||||
|
await warmup.WarmupAsync(); // 预热缓存
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. 缓存统计(可选)
|
||||||
|
```csharp
|
||||||
|
public interface ICacheStatistics {
|
||||||
|
long TotalHits { get; } // 命中次数
|
||||||
|
long TotalMisses { get; } // 未命中次数
|
||||||
|
double HitRate { get; } // 命中率
|
||||||
|
int CurrentItemCount { get; } // 当前缓存项数量
|
||||||
|
}
|
||||||
|
|
||||||
|
// 诊断用
|
||||||
|
public class DiagnosticsViewModel {
|
||||||
|
public void ShowCacheStats() {
|
||||||
|
var stats = _cache.GetStatistics();
|
||||||
|
MessageBox.Show($"缓存命中率: {stats.HitRate:P2}\n" +
|
||||||
|
$"缓存项数量: {stats.CurrentItemCount}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ 配置选项
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
services.AddMemoryCache(options => {
|
||||||
|
// 缓存大小限制(单位:MB)
|
||||||
|
options.SizeLimit = 100; // 限制100MB
|
||||||
|
|
||||||
|
// 压缩比例(当达到SizeLimit时,压缩到此比例)
|
||||||
|
options.CompactionPercentage = 0.25; // 压缩到25%
|
||||||
|
|
||||||
|
// 过期扫描频率
|
||||||
|
options.ExpirationScanFrequency = TimeSpan.FromMinutes(1);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ 实现方案
|
||||||
|
|
||||||
|
### 基于 Microsoft.Extensions.Caching.Memory
|
||||||
|
- **内置库**:无需额外依赖
|
||||||
|
- **高性能**:基于 ConcurrentDictionary
|
||||||
|
- **线程安全**:完全线程安全
|
||||||
|
- **自动清理**:后台定期清理过期项
|
||||||
|
|
||||||
|
### 架构设计
|
||||||
|
```
|
||||||
|
ICacheService(接口)
|
||||||
|
↓
|
||||||
|
MemoryCacheService(实现)
|
||||||
|
↓ 内部使用
|
||||||
|
Microsoft.Extensions.Caching.Memory.IMemoryCache
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 性能特点
|
||||||
|
|
||||||
|
- **读取速度**:< 1ms(内存访问)
|
||||||
|
- **线程安全**:无锁并发读取
|
||||||
|
- **内存占用**:可配置上限,自动压缩
|
||||||
|
- **过期清理**:后台自动扫描
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 过期策略对比
|
||||||
|
|
||||||
|
### 绝对过期 (Absolute Expiration)
|
||||||
|
```csharp
|
||||||
|
// 从设置时开始,10分钟后过期(无论是否访问)
|
||||||
|
_cache.Set("key", value, TimeSpan.FromMinutes(10));
|
||||||
|
|
||||||
|
// 适用场景:
|
||||||
|
// - API响应缓存(数据有时效性)
|
||||||
|
// - 报表数据(每天更新)
|
||||||
|
// - 字典数据(定期刷新)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 滑动过期 (Sliding Expiration)
|
||||||
|
```csharp
|
||||||
|
// 最后访问后30分钟无访问则过期
|
||||||
|
_cache.SetSliding("key", value, TimeSpan.FromMinutes(30));
|
||||||
|
|
||||||
|
// 适用场景:
|
||||||
|
// - 用户会话数据
|
||||||
|
// - 用户权限缓存
|
||||||
|
// - 热点数据(频繁访问的数据)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 组合使用
|
||||||
|
```csharp
|
||||||
|
// 滑动30分钟,但最多缓存2小时
|
||||||
|
services.AddMemoryCache();
|
||||||
|
|
||||||
|
_cache.Set("key", value, new MemoryCacheEntryOptions {
|
||||||
|
SlidingExpiration = TimeSpan.FromMinutes(30),
|
||||||
|
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(2)
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💾 缓存大小管理
|
||||||
|
|
||||||
|
### 设置项大小
|
||||||
|
```csharp
|
||||||
|
_cache.Set("large-data", value, new MemoryCacheEntryOptions {
|
||||||
|
Size = 1, // 占用1个单位(Size单位由SizeLimit定义)
|
||||||
|
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 优先级
|
||||||
|
```csharp
|
||||||
|
// 低优先级:内存不足时优先清除
|
||||||
|
_cache.Set("low-priority", value, new MemoryCacheEntryOptions {
|
||||||
|
Priority = CacheItemPriority.Low
|
||||||
|
});
|
||||||
|
|
||||||
|
// 高优先级:内存不足时最后清除
|
||||||
|
_cache.Set("high-priority", value, new MemoryCacheEntryOptions {
|
||||||
|
Priority = CacheItemPriority.High
|
||||||
|
});
|
||||||
|
|
||||||
|
// 永不移除(谨慎使用)
|
||||||
|
_cache.Set("never-remove", value, new MemoryCacheEntryOptions {
|
||||||
|
Priority = CacheItemPriority.NeverRemove
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 最佳实践
|
||||||
|
|
||||||
|
### 1. 缓存键命名规范
|
||||||
|
```csharp
|
||||||
|
✅ 好的命名
|
||||||
|
"user:profile:123"
|
||||||
|
"product:details:456"
|
||||||
|
"report:monthly:2024:12"
|
||||||
|
|
||||||
|
❌ 不好的命名
|
||||||
|
"user123"
|
||||||
|
"prod"
|
||||||
|
"report1"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 合理设置过期时间
|
||||||
|
```csharp
|
||||||
|
// 根据数据变化频率设置
|
||||||
|
字典数据:1小时 - 1天
|
||||||
|
用户信息:10分钟 - 30分钟
|
||||||
|
API响应:5分钟 - 15分钟
|
||||||
|
统计数据:1小时 - 24小时
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 避免缓存过大对象
|
||||||
|
```csharp
|
||||||
|
❌ 不推荐:缓存整个DataTable
|
||||||
|
_cache.Set("all-products", largeDataTable);
|
||||||
|
|
||||||
|
✅ 推荐:缓存DTO列表
|
||||||
|
_cache.Set("all-products", productDtos);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 及时清除失效缓存
|
||||||
|
```csharp
|
||||||
|
public async Task UpdateUserAsync(User user) {
|
||||||
|
await _repository.UpdateAsync(user);
|
||||||
|
|
||||||
|
// 立即清除缓存
|
||||||
|
_cache.Remove($"user:profile:{user.Id}");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 使用Result<T>模式
|
||||||
|
```csharp
|
||||||
|
public async Task<Result<Product>> GetProductAsync(int id) {
|
||||||
|
return await _cache.GetOrSetAsync(
|
||||||
|
$"product:{id}",
|
||||||
|
async () => {
|
||||||
|
var product = await _repository.GetByIdAsync(id);
|
||||||
|
return product != null
|
||||||
|
? Result<Product>.Success(product)
|
||||||
|
: Result<Product>.Failure("产品不存在");
|
||||||
|
},
|
||||||
|
TimeSpan.FromMinutes(10)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 未来扩展
|
||||||
|
|
||||||
|
### 可能的扩展方向
|
||||||
|
1. **分布式缓存**(基于 Redis)
|
||||||
|
2. **多级缓存**(L1: Memory, L2: Redis)
|
||||||
|
3. **缓存预加载**(后台任务)
|
||||||
|
4. **缓存同步**(多实例间同步)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📖 参考资料
|
||||||
|
|
||||||
|
- [Microsoft.Extensions.Caching.Memory](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/memory) - 官方文档
|
||||||
|
- [IMemoryCache](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.caching.memory.imemorycache) - API文档
|
||||||
|
- [缓存最佳实践](https://docs.microsoft.com/en-us/azure/architecture/best-practices/caching) - Azure架构指南
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**状态**:设计阶段,未实现
|
||||||
|
**预计实现时间**:根据实际需求决定
|
||||||
|
**实现难度**:低(100行代码即可完成核心功能)
|
||||||
|
**依赖**:Microsoft.Extensions.Caching.Memory(.NET内置)
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<Application x:Class="Client.App"
|
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
|
||||||
<Application.Resources>
|
|
||||||
<ResourceDictionary>
|
|
||||||
<ResourceDictionary.MergedDictionaries>
|
|
||||||
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/SkinDefault.xaml" />
|
|
||||||
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/Theme.xaml" />
|
|
||||||
</ResourceDictionary.MergedDictionaries>
|
|
||||||
</ResourceDictionary>
|
|
||||||
</Application.Resources>
|
|
||||||
</Application>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
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;
|
|
||||||
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
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Interaction logic for App.xaml
|
|
||||||
/// </summary>
|
|
||||||
public partial class App : Application
|
|
||||||
{
|
|
||||||
private ServiceProvider? _serviceProvider;
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
// 2) Build DI container
|
|
||||||
var services = new ServiceCollection();
|
|
||||||
|
|
||||||
// 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")
|
|
||||||
services.AddLogging(b => b.AddSharpLog());
|
|
||||||
|
|
||||||
// Net modules from configuration
|
|
||||||
services.AddNetClient(cm);
|
|
||||||
services.AddNetServer(cm);
|
|
||||||
|
|
||||||
// WPF exception handler
|
|
||||||
services.AddSingleton<IUiExceptionHandler, UiExceptionHandler>();
|
|
||||||
|
|
||||||
// WPF app services
|
|
||||||
services.AddSingleton<IDialogService, DialogService>();
|
|
||||||
services.AddSingleton<IAuthenticationService, DemoAuthenticationService>();
|
|
||||||
services.AddSingleton<IUserSession, UserSession>();
|
|
||||||
|
|
||||||
services.AddTransient<LoginViewModel>();
|
|
||||||
services.AddTransient<MainViewModel>();
|
|
||||||
services.AddTransient<LoginWindow>();
|
|
||||||
services.AddTransient<MainWindow>();
|
|
||||||
|
|
||||||
_serviceProvider = services.BuildServiceProvider();
|
|
||||||
|
|
||||||
// 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>());
|
|
||||||
|
|
||||||
// 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnExit(ExitEventArgs e)
|
|
||||||
{
|
|
||||||
_serviceProvider?.Dispose();
|
|
||||||
base.OnExit(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
using System.Windows;
|
|
||||||
|
|
||||||
[assembly: ThemeInfo(
|
|
||||||
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
|
|
||||||
//(used if a resource is not found in the page,
|
|
||||||
// or application resource dictionaries)
|
|
||||||
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
|
|
||||||
//(used if a resource is not found in the page,
|
|
||||||
// app, or any theme specific resource dictionaries)
|
|
||||||
)]
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<OutputType>WinExe</OutputType>
|
|
||||||
<TargetFramework>net8.0-windows</TargetFramework>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<UseWPF>true</UseWPF>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.1" />
|
|
||||||
<PackageReference Include="HandyControl" Version="3.5.0" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\Wpf\Wpf.csproj" />
|
|
||||||
<ProjectReference Include="..\Config\Config.csproj" />
|
|
||||||
<ProjectReference Include="..\NetClient\NetClient.csproj" />
|
|
||||||
<ProjectReference Include="..\NetServer\NetServer.csproj" />
|
|
||||||
<ProjectReference Include="..\Log\Log.csproj" />
|
|
||||||
<ProjectReference Include="..\Sharp.Infrastructure\Sharp.Infrastructure.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
|
||||||
<PropertyGroup />
|
|
||||||
</Project>
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
using System.Windows;
|
|
||||||
using System.Windows.Controls;
|
|
||||||
|
|
||||||
namespace Client.Helpers;
|
|
||||||
|
|
||||||
public static class PasswordBoxHelper
|
|
||||||
{
|
|
||||||
public static readonly DependencyProperty BindablePasswordProperty =
|
|
||||||
DependencyProperty.RegisterAttached(
|
|
||||||
"BindablePassword",
|
|
||||||
typeof(string),
|
|
||||||
typeof(PasswordBoxHelper),
|
|
||||||
new PropertyMetadata(null, OnBindablePasswordChanged));
|
|
||||||
|
|
||||||
private static readonly DependencyProperty IsUpdatingProperty =
|
|
||||||
DependencyProperty.RegisterAttached(
|
|
||||||
"IsUpdating",
|
|
||||||
typeof(bool),
|
|
||||||
typeof(PasswordBoxHelper),
|
|
||||||
new PropertyMetadata(false));
|
|
||||||
|
|
||||||
public static string GetBindablePassword(DependencyObject obj) => (string)obj.GetValue(BindablePasswordProperty);
|
|
||||||
public static void SetBindablePassword(DependencyObject obj, string value) => obj.SetValue(BindablePasswordProperty, value);
|
|
||||||
|
|
||||||
private static bool GetIsUpdating(DependencyObject obj) => (bool)obj.GetValue(IsUpdatingProperty);
|
|
||||||
private static void SetIsUpdating(DependencyObject obj, bool value) => obj.SetValue(IsUpdatingProperty, value);
|
|
||||||
|
|
||||||
private static void OnBindablePasswordChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
||||||
{
|
|
||||||
if (d is not PasswordBox passwordBox)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
passwordBox.PasswordChanged -= HandlePasswordChanged;
|
|
||||||
|
|
||||||
if (!GetIsUpdating(passwordBox))
|
|
||||||
{
|
|
||||||
passwordBox.Password = e.NewValue?.ToString() ?? string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
passwordBox.PasswordChanged += HandlePasswordChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void HandlePasswordChanged(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (sender is not PasswordBox passwordBox)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
passwordBox.PasswordChanged -= HandlePasswordChanged;
|
|
||||||
|
|
||||||
SetIsUpdating(passwordBox, true);
|
|
||||||
SetBindablePassword(passwordBox, passwordBox.Password);
|
|
||||||
SetIsUpdating(passwordBox, false);
|
|
||||||
|
|
||||||
passwordBox.PasswordChanged += HandlePasswordChanged;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
<hc:GlowWindow x:Class="Client.LoginWindow"
|
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
|
||||||
xmlns:hc="https://handyorg.github.io/handycontrol"
|
|
||||||
xmlns:conv="clr-namespace:Sharp.Wpf.Converters;assembly=Sharp.Wpf"
|
|
||||||
xmlns:h="clr-namespace:Client.Helpers"
|
|
||||||
mc:Ignorable="d"
|
|
||||||
Title="Sharp 客户端登录"
|
|
||||||
WindowStartupLocation="CenterScreen"
|
|
||||||
Width="420"
|
|
||||||
Height="420"
|
|
||||||
ResizeMode="NoResize">
|
|
||||||
<hc:GlowWindow.Resources>
|
|
||||||
<conv:BoolToVisibilityConverter x:Key="BoolToVisibility" />
|
|
||||||
</hc:GlowWindow.Resources>
|
|
||||||
<Grid Background="{DynamicResource RegionBrush}">
|
|
||||||
<Border CornerRadius="16" Padding="32" Background="{DynamicResource PopupBrush}">
|
|
||||||
<Grid>
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="*" />
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Text="欢迎回来"
|
|
||||||
FontSize="24"
|
|
||||||
FontWeight="Bold"
|
|
||||||
Margin="0,0,0,4" />
|
|
||||||
<TextBlock Text="请输入管理员账号信息"
|
|
||||||
Foreground="{DynamicResource SecondaryTextBrush}" />
|
|
||||||
</StackPanel>
|
|
||||||
<StackPanel Grid.Row="1" Margin="0,24,0,0">
|
|
||||||
<TextBlock Text="账号" FontWeight="SemiBold" />
|
|
||||||
<TextBox Margin="0,8,0,0"
|
|
||||||
Text="{Binding UserName, UpdateSourceTrigger=PropertyChanged}"
|
|
||||||
Height="36" />
|
|
||||||
</StackPanel>
|
|
||||||
<StackPanel Grid.Row="2" Margin="0,12,0,0">
|
|
||||||
<TextBlock Text="密码" FontWeight="SemiBold" />
|
|
||||||
<PasswordBox Margin="0,8,0,0"
|
|
||||||
h:PasswordBoxHelper.BindablePassword="{Binding Password, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
|
||||||
Height="36" />
|
|
||||||
</StackPanel>
|
|
||||||
<TextBlock Grid.Row="3"
|
|
||||||
Margin="0,12,0,0"
|
|
||||||
Text="{Binding ErrorMessage}"
|
|
||||||
Foreground="Tomato"
|
|
||||||
Visibility="{Binding HasError, Converter={StaticResource BoolToVisibility}}" />
|
|
||||||
<Button Grid.Row="4"
|
|
||||||
Margin="0,24,0,0"
|
|
||||||
Height="40"
|
|
||||||
Content="登 录"
|
|
||||||
Command="{Binding LoginCommand}" />
|
|
||||||
<ProgressBar Grid.Row="5"
|
|
||||||
Height="4"
|
|
||||||
IsIndeterminate="True"
|
|
||||||
HorizontalAlignment="Stretch"
|
|
||||||
VerticalAlignment="Top"
|
|
||||||
Margin="0,24,0,0"
|
|
||||||
Visibility="{Binding IsBusy, Converter={StaticResource BoolToVisibility}}" />
|
|
||||||
</Grid>
|
|
||||||
</Border>
|
|
||||||
</Grid>
|
|
||||||
</hc:GlowWindow>
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
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, 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnClosed(EventArgs e)
|
|
||||||
{
|
|
||||||
base.OnClosed(e);
|
|
||||||
_viewModel.LoginSucceeded -= HandleLoginSucceeded;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
<hc:GlowWindow x:Class="Client.MainWindow"
|
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
|
||||||
xmlns:hc="https://handyorg.github.io/handycontrol"
|
|
||||||
xmlns:w="clr-namespace:Sharp.Wpf.Windowing;assembly=Sharp.Wpf"
|
|
||||||
xmlns:conv="clr-namespace:Sharp.Wpf.Converters;assembly=Sharp.Wpf"
|
|
||||||
mc:Ignorable="d"
|
|
||||||
Title="Sharp 控制台"
|
|
||||||
Height="560"
|
|
||||||
Width="960"
|
|
||||||
WindowStartupLocation="CenterScreen"
|
|
||||||
w:WindowPlacement.RememberPlacement="True"
|
|
||||||
w:WindowPlacement.PlacementId="MainWindow">
|
|
||||||
<hc:GlowWindow.Resources>
|
|
||||||
<conv:BoolToVisibilityConverter x:Key="Bool2Vis" />
|
|
||||||
</hc:GlowWindow.Resources>
|
|
||||||
<Grid Background="{DynamicResource RegionBrush}">
|
|
||||||
<Grid Margin="24">
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="*" />
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
<Border Background="{DynamicResource PopupBrush}" Padding="24" CornerRadius="16">
|
|
||||||
<Grid>
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="*" />
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Text="{Binding Greeting}" FontSize="26" FontWeight="Bold" />
|
|
||||||
<TextBlock Text="{Binding StatusMessage}" Margin="0,6,0,0" Foreground="{DynamicResource SecondaryTextBrush}" />
|
|
||||||
<TextBlock Text="最后同步: " Margin="0,12,0,0" Foreground="{DynamicResource SecondaryTextBrush}">
|
|
||||||
<Run Text="{Binding LastSyncTime, StringFormat=yyyy-MM-dd HH:mm:ss}" />
|
|
||||||
</TextBlock>
|
|
||||||
</StackPanel>
|
|
||||||
<StackPanel Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center">
|
|
||||||
<Button Content="刷新仪表盘"
|
|
||||||
Padding="18,6"
|
|
||||||
Margin="0,0,12,0"
|
|
||||||
Command="{Binding RefreshDashboardCommand}" />
|
|
||||||
<Button Content="演示弹窗"
|
|
||||||
Padding="18,6"
|
|
||||||
Margin="0,0,12,0"
|
|
||||||
Command="{Binding ShowDialogCommand}" />
|
|
||||||
<Button Content="触发异常"
|
|
||||||
Padding="18,6"
|
|
||||||
Command="{Binding ThrowCommand}" />
|
|
||||||
</StackPanel>
|
|
||||||
</Grid>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<Grid Grid.Row="1" Margin="0,24,0,0">
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="2*" />
|
|
||||||
<ColumnDefinition Width="*" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
|
|
||||||
<UniformGrid Columns="2" Margin="0,0,16,0">
|
|
||||||
<Border Background="{DynamicResource PopupBrush}" Margin="0,0,16,16" Padding="20" CornerRadius="12">
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Text="登录用户" FontWeight="SemiBold" />
|
|
||||||
<TextBlock Text="{Binding UserDisplayName}" FontSize="20" Margin="0,8,0,0" />
|
|
||||||
<TextBlock Text="当前会话已建立"
|
|
||||||
Foreground="{DynamicResource SecondaryTextBrush}" />
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
<Border Background="{DynamicResource PopupBrush}" Margin="0,0,16,16" Padding="20" CornerRadius="12">
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Text="系统健康度" FontWeight="SemiBold" />
|
|
||||||
<TextBlock Text="{Binding StatusMessage}" Margin="0,8,0,0" />
|
|
||||||
<ProgressBar Height="6" Margin="0,12,0,0"
|
|
||||||
IsIndeterminate="True"
|
|
||||||
Visibility="{Binding IsBusy, Converter={StaticResource Bool2Vis}}" />
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
<Border Background="{DynamicResource PopupBrush}" Margin="0,0,16,16" Padding="20" CornerRadius="12">
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Text="上次操作" FontWeight="SemiBold" />
|
|
||||||
<TextBlock Text="{Binding Greeting}" Margin="0,8,0,0" TextWrapping="Wrap" />
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
<Border Background="{DynamicResource PopupBrush}" Margin="0,0,16,16" Padding="20" CornerRadius="12">
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Text="提示" FontWeight="SemiBold" />
|
|
||||||
<TextBlock Text="使用 HandyControl 让界面更柔和" Margin="0,8,0,0" />
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
</UniformGrid>
|
|
||||||
|
|
||||||
<Border Grid.Column="1" Background="{DynamicResource PopupBrush}" Padding="24" CornerRadius="12">
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Text="操作日志" FontSize="18" FontWeight="SemiBold" />
|
|
||||||
<TextBlock Text="演示命令" Foreground="{DynamicResource SecondaryTextBrush}" Margin="0,8,0,16" />
|
|
||||||
<ItemsControl>
|
|
||||||
<ItemsControl.Items>
|
|
||||||
<TextBlock Text="• 刷新仪表盘会模拟异步调用" Margin="0,4" />
|
|
||||||
<TextBlock Text="• 演示弹窗来自 Sharp.Wpf DialogService" Margin="0,4" />
|
|
||||||
<TextBlock Text="• 触发异常会被全局处理" Margin="0,4" />
|
|
||||||
<TextBlock Text="• 登录信息通过依赖注入管理" Margin="0,4" />
|
|
||||||
</ItemsControl.Items>
|
|
||||||
</ItemsControl>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</hc:GlowWindow>
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
using HandyControl.Controls;
|
|
||||||
using Client.ViewModels;
|
|
||||||
|
|
||||||
namespace Client;
|
|
||||||
|
|
||||||
public partial class MainWindow : GlowWindow
|
|
||||||
{
|
|
||||||
public MainWindow(MainViewModel viewModel)
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
DataContext = viewModel;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Client.Services;
|
|
||||||
|
|
||||||
public interface IAuthenticationService
|
|
||||||
{
|
|
||||||
Task<AuthenticationResult> LoginAsync(string userName, string password, CancellationToken cancellationToken = default);
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed record AuthenticationResult(bool Succeeded, string? Error, string? DisplayName);
|
|
||||||
|
|
||||||
public sealed class DemoAuthenticationService : IAuthenticationService
|
|
||||||
{
|
|
||||||
public async Task<AuthenticationResult> LoginAsync(string userName, string password, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
await Task.Delay(600, cancellationToken);
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(userName) || string.IsNullOrWhiteSpace(password))
|
|
||||||
{
|
|
||||||
return new AuthenticationResult(false, "请输入账号和密码", null);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.Equals(userName, "admin", StringComparison.OrdinalIgnoreCase) || password != "123456")
|
|
||||||
{
|
|
||||||
return new AuthenticationResult(false, "账号或密码不正确 (admin / 123456)", null);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new AuthenticationResult(true, null, "Sharp 管理员");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
namespace Client.Services;
|
|
||||||
|
|
||||||
public interface IUserSession
|
|
||||||
{
|
|
||||||
bool IsAuthenticated { get; }
|
|
||||||
string? UserName { get; }
|
|
||||||
string? DisplayName { get; }
|
|
||||||
void SignIn(string userName, string? displayName);
|
|
||||||
void SignOut();
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class UserSession : IUserSession
|
|
||||||
{
|
|
||||||
public bool IsAuthenticated { get; private set; }
|
|
||||||
public string? UserName { get; private set; }
|
|
||||||
public string? DisplayName { get; private set; }
|
|
||||||
|
|
||||||
public void SignIn(string userName, string? displayName)
|
|
||||||
{
|
|
||||||
IsAuthenticated = true;
|
|
||||||
UserName = userName;
|
|
||||||
DisplayName = string.IsNullOrWhiteSpace(displayName) ? userName : displayName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SignOut()
|
|
||||||
{
|
|
||||||
IsAuthenticated = false;
|
|
||||||
UserName = null;
|
|
||||||
DisplayName = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Client.Services;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Sharp.Wpf.Commands;
|
|
||||||
using Sharp.Wpf.Mvvm;
|
|
||||||
|
|
||||||
namespace Client.ViewModels;
|
|
||||||
|
|
||||||
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";
|
|
||||||
private string _password = string.Empty;
|
|
||||||
private string? _errorMessage;
|
|
||||||
|
|
||||||
public event EventHandler? LoginSucceeded;
|
|
||||||
|
|
||||||
public LoginViewModel(IAuthenticationService authenticationService, IUserSession userSession, ILogger<LoginViewModel> logger)
|
|
||||||
{
|
|
||||||
_authenticationService = authenticationService;
|
|
||||||
_userSession = userSession;
|
|
||||||
_logger = logger;
|
|
||||||
_loginCommand = new AsyncCommand(LoginAsync);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string UserName
|
|
||||||
{
|
|
||||||
get => _userName;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (SetProperty(ref _userName, value))
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Password
|
|
||||||
{
|
|
||||||
get => _password;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (SetProperty(ref _password, value))
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string? ErrorMessage
|
|
||||||
{
|
|
||||||
get => _errorMessage;
|
|
||||||
private set
|
|
||||||
{
|
|
||||||
if (SetProperty(ref _errorMessage, value))
|
|
||||||
{
|
|
||||||
OnPropertyChanged(nameof(HasError));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool HasError => !string.IsNullOrWhiteSpace(ErrorMessage);
|
|
||||||
|
|
||||||
public IAsyncCommand LoginCommand => _loginCommand;
|
|
||||||
|
|
||||||
private async Task LoginAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Login attempt for user: {UserName}", UserName);
|
|
||||||
IsBusy = true;
|
|
||||||
ErrorMessage = null;
|
|
||||||
|
|
||||||
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,86 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows.Input;
|
|
||||||
using Client.Services;
|
|
||||||
using Sharp.Wpf.Commands;
|
|
||||||
using Sharp.Wpf.Mvvm;
|
|
||||||
using Sharp.Wpf.Services;
|
|
||||||
|
|
||||||
namespace Client.ViewModels;
|
|
||||||
|
|
||||||
public sealed class MainViewModel : ViewModelBase
|
|
||||||
{
|
|
||||||
private readonly IDialogService _dialogService;
|
|
||||||
private readonly IUserSession _userSession;
|
|
||||||
|
|
||||||
private string _greeting = "欢迎使用 Sharp";
|
|
||||||
private string _statusMessage = "系统就绪";
|
|
||||||
private DateTime _lastSyncTime = DateTime.Now;
|
|
||||||
|
|
||||||
public MainViewModel(IDialogService dialogService, IUserSession userSession)
|
|
||||||
{
|
|
||||||
_dialogService = dialogService;
|
|
||||||
_userSession = userSession;
|
|
||||||
_greeting = $"欢迎回来,{UserDisplayName}";
|
|
||||||
_statusMessage = "准备就绪";
|
|
||||||
|
|
||||||
ShowDialogCommand = new AsyncCommand(ShowDialogAsync);
|
|
||||||
RefreshDashboardCommand = new AsyncCommand(RefreshDashboardAsync);
|
|
||||||
ThrowCommand = new RelayCommand(() => throw new InvalidOperationException("演示:未处理异常将被全局处理"));
|
|
||||||
}
|
|
||||||
|
|
||||||
public string UserDisplayName => _userSession.DisplayName ?? _userSession.UserName ?? "访客";
|
|
||||||
|
|
||||||
public string Greeting
|
|
||||||
{
|
|
||||||
get => _greeting;
|
|
||||||
set => SetProperty(ref _greeting, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string StatusMessage
|
|
||||||
{
|
|
||||||
get => _statusMessage;
|
|
||||||
set => SetProperty(ref _statusMessage, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public DateTime LastSyncTime
|
|
||||||
{
|
|
||||||
get => _lastSyncTime;
|
|
||||||
set => SetProperty(ref _lastSyncTime, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public IAsyncCommand ShowDialogCommand { get; }
|
|
||||||
public IAsyncCommand RefreshDashboardCommand { get; }
|
|
||||||
public ICommand ThrowCommand { get; }
|
|
||||||
|
|
||||||
private async Task ShowDialogAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
IsBusy = true;
|
|
||||||
await Task.Delay(300).ConfigureAwait(false);
|
|
||||||
await _dialogService.ShowInfoAsync($"Hi {UserDisplayName}, 这是来自 Sharp.Wpf 的提醒!").ConfigureAwait(false);
|
|
||||||
Greeting = $"问候更新时间: {DateTime.Now:HH:mm:ss}";
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
IsBusy = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task RefreshDashboardAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
IsBusy = true;
|
|
||||||
StatusMessage = "正在同步仪表盘数据...";
|
|
||||||
await Task.Delay(600).ConfigureAwait(false);
|
|
||||||
LastSyncTime = DateTime.Now;
|
|
||||||
StatusMessage = "同步完成";
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
IsBusy = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
#pragma checksum "..\..\..\App.xaml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "A01D87CBC408FE1E9934F8938867D92F17E3A2CF"
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// <auto-generated>
|
|
||||||
// This code was generated by a tool.
|
|
||||||
//
|
|
||||||
// Changes to this file may cause incorrect behavior and will be lost if
|
|
||||||
// the code is regenerated.
|
|
||||||
// </auto-generated>
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Windows;
|
|
||||||
using System.Windows.Automation;
|
|
||||||
using System.Windows.Controls;
|
|
||||||
using System.Windows.Controls.Primitives;
|
|
||||||
using System.Windows.Controls.Ribbon;
|
|
||||||
using System.Windows.Data;
|
|
||||||
using System.Windows.Documents;
|
|
||||||
using System.Windows.Forms.Integration;
|
|
||||||
using System.Windows.Ink;
|
|
||||||
using System.Windows.Input;
|
|
||||||
using System.Windows.Markup;
|
|
||||||
using System.Windows.Media;
|
|
||||||
using System.Windows.Media.Animation;
|
|
||||||
using System.Windows.Media.Effects;
|
|
||||||
using System.Windows.Media.Imaging;
|
|
||||||
using System.Windows.Media.Media3D;
|
|
||||||
using System.Windows.Media.TextFormatting;
|
|
||||||
using System.Windows.Navigation;
|
|
||||||
using System.Windows.Shapes;
|
|
||||||
using System.Windows.Shell;
|
|
||||||
|
|
||||||
|
|
||||||
namespace Client {
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// App
|
|
||||||
/// </summary>
|
|
||||||
public partial class App : System.Windows.Application {
|
|
||||||
|
|
||||||
private bool _contentLoaded;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// InitializeComponent
|
|
||||||
/// </summary>
|
|
||||||
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
|
||||||
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "9.0.4.0")]
|
|
||||||
public void InitializeComponent() {
|
|
||||||
if (_contentLoaded) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_contentLoaded = true;
|
|
||||||
System.Uri resourceLocater = new System.Uri("/Client;component/app.xaml", System.UriKind.Relative);
|
|
||||||
|
|
||||||
#line 1 "..\..\..\App.xaml"
|
|
||||||
System.Windows.Application.LoadComponent(this, resourceLocater);
|
|
||||||
|
|
||||||
#line default
|
|
||||||
#line hidden
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Application Entry Point.
|
|
||||||
/// </summary>
|
|
||||||
[System.STAThreadAttribute()]
|
|
||||||
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
|
||||||
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "9.0.4.0")]
|
|
||||||
public static void Main() {
|
|
||||||
Client.App app = new Client.App();
|
|
||||||
app.InitializeComponent();
|
|
||||||
app.Run();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
//------------------------------------------------------------------------------
|
|
||||||
// <auto-generated>
|
|
||||||
// 此代码由工具生成。
|
|
||||||
// 运行时版本:4.0.30319.42000
|
|
||||||
//
|
|
||||||
// 对此文件的更改可能会导致不正确的行为,并且如果
|
|
||||||
// 重新生成代码,这些更改将会丢失。
|
|
||||||
// </auto-generated>
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
using System;
|
|
||||||
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+f7660c49e81d5fe2747f6514545ceb961c2066d3")]
|
|
||||||
[assembly: System.Reflection.AssemblyProductAttribute("Client")]
|
|
||||||
[assembly: System.Reflection.AssemblyTitleAttribute("Client")]
|
|
||||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
|
||||||
[assembly: System.Runtime.Versioning.TargetPlatformAttribute("Windows7.0")]
|
|
||||||
[assembly: System.Runtime.Versioning.SupportedOSPlatformAttribute("Windows7.0")]
|
|
||||||
|
|
||||||
// 由 MSBuild WriteCodeFragment 类生成。
|
|
||||||
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
eeea53cf7db257ee716efdd2fdcdb5f1a810b49927811d410a05e1ff19899369
|
|
||||||
-16
@@ -1,16 +0,0 @@
|
|||||||
is_global = true
|
|
||||||
build_property.TargetFramework = net8.0-windows
|
|
||||||
build_property.TargetPlatformMinVersion = 7.0
|
|
||||||
build_property.UsingMicrosoftNETSdkWeb =
|
|
||||||
build_property.ProjectTypeGuids =
|
|
||||||
build_property.InvariantGlobalization =
|
|
||||||
build_property.PlatformNeutralAssembly =
|
|
||||||
build_property.EnforceExtendedAnalyzerRules =
|
|
||||||
build_property._SupportedPlatformList = Linux,macOS,Windows
|
|
||||||
build_property.RootNamespace = Client
|
|
||||||
build_property.ProjectDir = D:\yuanll\sharp-basic\src\Sharp\Client\
|
|
||||||
build_property.EnableComHosting =
|
|
||||||
build_property.EnableGeneratedComInterfaceComImportInterop =
|
|
||||||
build_property.CsWinRTUseWindowsUIXamlProjections = false
|
|
||||||
build_property.EffectiveAnalysisLevelStyle = 8.0
|
|
||||||
build_property.EnableCodeStyleSeverity =
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
#pragma checksum "..\..\..\MainWindow.xaml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "9E813BA3492F961B4CE0E3DE4E78408D972934B6"
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// <auto-generated>
|
|
||||||
// This code was generated by a tool.
|
|
||||||
//
|
|
||||||
// Changes to this file may cause incorrect behavior and will be lost if
|
|
||||||
// the code is regenerated.
|
|
||||||
// </auto-generated>
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
using HandyControl.Controls;
|
|
||||||
using HandyControl.Data;
|
|
||||||
using HandyControl.Expression.Media;
|
|
||||||
using HandyControl.Expression.Shapes;
|
|
||||||
using HandyControl.Interactivity;
|
|
||||||
using HandyControl.Media.Animation;
|
|
||||||
using HandyControl.Media.Effects;
|
|
||||||
using HandyControl.Properties.Langs;
|
|
||||||
using HandyControl.Themes;
|
|
||||||
using HandyControl.Tools;
|
|
||||||
using HandyControl.Tools.Converter;
|
|
||||||
using HandyControl.Tools.Extension;
|
|
||||||
using Sharp.Wpf.Converters;
|
|
||||||
using Sharp.Wpf.Windowing;
|
|
||||||
using System;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Windows;
|
|
||||||
using System.Windows.Automation;
|
|
||||||
using System.Windows.Controls;
|
|
||||||
using System.Windows.Controls.Primitives;
|
|
||||||
using System.Windows.Controls.Ribbon;
|
|
||||||
using System.Windows.Data;
|
|
||||||
using System.Windows.Documents;
|
|
||||||
using System.Windows.Forms.Integration;
|
|
||||||
using System.Windows.Ink;
|
|
||||||
using System.Windows.Input;
|
|
||||||
using System.Windows.Markup;
|
|
||||||
using System.Windows.Media;
|
|
||||||
using System.Windows.Media.Animation;
|
|
||||||
using System.Windows.Media.Effects;
|
|
||||||
using System.Windows.Media.Imaging;
|
|
||||||
using System.Windows.Media.Media3D;
|
|
||||||
using System.Windows.Media.TextFormatting;
|
|
||||||
using System.Windows.Navigation;
|
|
||||||
using System.Windows.Shapes;
|
|
||||||
using System.Windows.Shell;
|
|
||||||
|
|
||||||
|
|
||||||
namespace Client {
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// MainWindow
|
|
||||||
/// </summary>
|
|
||||||
public partial class MainWindow : HandyControl.Controls.GlowWindow, System.Windows.Markup.IComponentConnector {
|
|
||||||
|
|
||||||
private bool _contentLoaded;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// InitializeComponent
|
|
||||||
/// </summary>
|
|
||||||
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
|
||||||
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "9.0.4.0")]
|
|
||||||
public void InitializeComponent() {
|
|
||||||
if (_contentLoaded) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_contentLoaded = true;
|
|
||||||
System.Uri resourceLocater = new System.Uri("/Client;component/mainwindow.xaml", System.UriKind.Relative);
|
|
||||||
|
|
||||||
#line 1 "..\..\..\MainWindow.xaml"
|
|
||||||
System.Windows.Application.LoadComponent(this, resourceLocater);
|
|
||||||
|
|
||||||
#line default
|
|
||||||
#line hidden
|
|
||||||
}
|
|
||||||
|
|
||||||
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
|
||||||
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "9.0.4.0")]
|
|
||||||
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
|
|
||||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes")]
|
|
||||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")]
|
|
||||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily")]
|
|
||||||
void System.Windows.Markup.IComponentConnector.Connect(int connectionId, object target) {
|
|
||||||
this._contentLoaded = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -4,6 +4,19 @@
|
|||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
|
||||||
|
<!-- 程序集元数据 -->
|
||||||
|
<Description>Sharp Framework 核心库 - 提供 Result<T> 模式、Global 容器、系统配置等基础功能</Description>
|
||||||
|
|
||||||
|
<!-- NuGet 包元数据 -->
|
||||||
|
<PackageTags>sharp;common;result;railway-oriented;functional;utilities</PackageTags>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
|
||||||
|
namespace Common.Configuration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 系统级通用配置
|
||||||
|
/// 包含 HTTP 访问所需的基础字段,可被 NetClient/NetServer 等模块复用
|
||||||
|
/// 约定配置节名称:System
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SystemConfig
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
[RegularExpression(@"^([0-9]{1,3}\.){3}[0-9]{1,3}$|^localhost$|^[a-zA-Z0-9_.-]+$")]
|
||||||
|
public string HTTPIp { get; set; } = "127.0.0.1";
|
||||||
|
|
||||||
|
[Range(1, 65535)]
|
||||||
|
public int HttpPort { get; set; } = 8080;
|
||||||
|
|
||||||
|
[Range(1, 600)]
|
||||||
|
public int TimeoutSeconds { get; set; } = 30;
|
||||||
|
|
||||||
|
public string Scheme { get; set; } = "http";
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Common.DependencyInjection;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 快速启动扩展,提供"零配置"的便捷注册方法
|
||||||
|
/// </summary>
|
||||||
|
public static class QuickStartExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 添加 Sharp 框架的所有基础服务(一行搞定)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="services">服务集合</param>
|
||||||
|
/// <param name="configuration">配置(可选,不传则自动创建默认配置)</param>
|
||||||
|
/// <returns>服务集合</returns>
|
||||||
|
public static IServiceCollection AddSharpEssentials(
|
||||||
|
this IServiceCollection services,
|
||||||
|
IConfiguration? configuration = null)
|
||||||
|
{
|
||||||
|
// 1. 配置管理(如果没传就用默认)
|
||||||
|
// Note: ConfigBuilder.CreateDefault() 在 Config 模块中
|
||||||
|
// 这里假设用户已经传入 configuration 或在外部创建
|
||||||
|
if (configuration != null)
|
||||||
|
{
|
||||||
|
services.AddSingleton(configuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 缓存服务(使用 Cache 模块的扩展方法)
|
||||||
|
// Note: AddSharpCache() 在 Cache.DependencyInjection 中定义
|
||||||
|
services.AddMemoryCache();
|
||||||
|
|
||||||
|
// 3. 全局服务定位器(兼容性)
|
||||||
|
if (configuration != null)
|
||||||
|
{
|
||||||
|
RegisterGlobalServices(services, configuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 注册到 Global 容器(向后兼容)
|
||||||
|
/// </summary>
|
||||||
|
private static void RegisterGlobalServices(IServiceCollection services, IConfiguration configuration)
|
||||||
|
{
|
||||||
|
// 构建临时 ServiceProvider
|
||||||
|
var provider = services.BuildServiceProvider();
|
||||||
|
|
||||||
|
// 注册到 Global
|
||||||
|
Global.RegisterSingleton(configuration);
|
||||||
|
Global.RegisterSingleton(provider);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -55,7 +55,11 @@ public class ServiceChangedEventArgs<T> : EventArgs
|
|||||||
/// - 生命周期管理(Singleton/Transient)
|
/// - 生命周期管理(Singleton/Transient)
|
||||||
/// - 服务变更通知机制
|
/// - 服务变更通知机制
|
||||||
/// - 支持工厂方法注册
|
/// - 支持工厂方法注册
|
||||||
|
///
|
||||||
|
/// ⚠️ 注意:此类已标记为非首选使用,建议新项目使用 Microsoft.Extensions.DependencyInjection。
|
||||||
|
/// 此类仅用于向后兼容和快速原型开发。
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
|
[Obsolete("不推荐使用Global容器。建议使用Microsoft.Extensions.DependencyInjection作为首选DI方案。此类保留用于向后兼容和快速原型开发。", false)]
|
||||||
public static class Global
|
public static class Global
|
||||||
{
|
{
|
||||||
private class ServiceDescriptor
|
private class ServiceDescriptor
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
|
||||||
|
<!-- 程序集元数据 -->
|
||||||
|
<Description>Sharp Framework 配置管理库 - 支持 JSON/XML/INI/TOML 多格式配置,提供热重载和验证功能</Description>
|
||||||
|
|
||||||
|
<!-- NuGet 包元数据 -->
|
||||||
|
<PackageTags>sharp;config;configuration;hotreload;json;xml;ini;toml</PackageTags>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
|
namespace Config;
|
||||||
|
|
||||||
|
public static class ConfigBuilderExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 创建默认配置(约定优于配置)
|
||||||
|
/// 自动查找 appsettings.json,启用热重载和环境变量
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="basePath">基础路径,默认为应用程序目录</param>
|
||||||
|
/// <param name="environmentPrefix">环境变量前缀,默认为 "APP_"</param>
|
||||||
|
/// <returns>配置管理器</returns>
|
||||||
|
public static ConfigManager CreateDefault(
|
||||||
|
string? basePath = null,
|
||||||
|
string? environmentPrefix = "APP_")
|
||||||
|
{
|
||||||
|
basePath ??= AppContext.BaseDirectory;
|
||||||
|
|
||||||
|
// 查找配置文件(appsettings.json, appsettings.Development.json 等)
|
||||||
|
var configFiles = new List<string> { "appsettings.json" };
|
||||||
|
|
||||||
|
// 根据环境添加特定配置
|
||||||
|
var environment = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT")
|
||||||
|
?? Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")
|
||||||
|
?? "Production";
|
||||||
|
|
||||||
|
var envConfigFile = $"appsettings.{environment}.json";
|
||||||
|
var envConfigPath = Path.Combine(basePath, envConfigFile);
|
||||||
|
if (File.Exists(envConfigPath))
|
||||||
|
{
|
||||||
|
configFiles.Add(envConfigFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建配置
|
||||||
|
return ConfigBuilder.BuildFromFiles(
|
||||||
|
configFiles.ToArray(),
|
||||||
|
reloadOnChange: true,
|
||||||
|
includeEnvironment: true,
|
||||||
|
environmentPrefix: environmentPrefix,
|
||||||
|
basePath: basePath
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建最小化配置(只有 appsettings.json)
|
||||||
|
/// </summary>
|
||||||
|
public static ConfigManager CreateMinimal(string? basePath = null)
|
||||||
|
{
|
||||||
|
basePath ??= AppContext.BaseDirectory;
|
||||||
|
|
||||||
|
return ConfigBuilder.BuildFromFiles(
|
||||||
|
new[] { "appsettings.json" },
|
||||||
|
reloadOnChange: false,
|
||||||
|
includeEnvironment: false,
|
||||||
|
basePath: basePath
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,503 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
using System;
|
|
||||||
using Sharp.Data.Access.Options;
|
|
||||||
|
|
||||||
namespace Sharp.Data.Access.Connections;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 构建 Access 连接字符串的工厂。
|
|
||||||
/// </summary>
|
|
||||||
public interface IAccessConnectionStringFactory
|
|
||||||
{
|
|
||||||
string Create(AccessDatabaseOptions options, IServiceProvider services);
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Sharp.Data.Context;
|
|
||||||
|
|
||||||
namespace Sharp.Data.Access;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Access 数据上下文工厂,避免直接依赖具体实现。
|
|
||||||
/// </summary>
|
|
||||||
public interface IAccessDataContextFactory<TContext> where TContext : DbContext
|
|
||||||
{
|
|
||||||
IDataContext Create();
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
using System;
|
|
||||||
using Sharp.Data.Access.Options;
|
|
||||||
|
|
||||||
namespace Sharp.Data.Access.Initialization;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Access 数据库初始化器(确保目录/数据库文件准备就绪)。
|
|
||||||
/// </summary>
|
|
||||||
public interface IAccessDatabaseInitializer
|
|
||||||
{
|
|
||||||
void EnsureDatabase(AccessDatabaseOptions options, IServiceProvider services);
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
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。");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
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";
|
|
||||||
}
|
|
||||||
@@ -1,374 +0,0 @@
|
|||||||
# 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,让调用者可以用更少的代码完成相同的功能!
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
# 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 支持可在任何项目中复用,同时保持代码简洁、低耦合。
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
namespace Sharp.Data.Access.Security;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 默认的密码解密器,直接返回原始值。
|
|
||||||
/// </summary>
|
|
||||||
public sealed class PassthroughAccessPasswordDecryptor : IAccessPasswordDecryptor
|
|
||||||
{
|
|
||||||
public string? Decrypt(string? encryptedPassword) => encryptedPassword;
|
|
||||||
}
|
|
||||||
@@ -5,22 +5,28 @@
|
|||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<RootNamespace>Sharp.Data</RootNamespace>
|
<RootNamespace>Sharp.Data</RootNamespace>
|
||||||
|
|
||||||
|
<!-- 程序集元数据 -->
|
||||||
|
<Description>Sharp Framework 数据访问库 - EF Core + Repository 模式,支持审计、软删除等企业级功能</Description>
|
||||||
|
|
||||||
|
<!-- NuGet 包元数据 -->
|
||||||
|
<PackageTags>sharp;data;efcore;repository;entity-framework</PackageTags>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<!-- EF Core -->
|
<!-- EF Core -->
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.0" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.0" />
|
||||||
|
|
||||||
<!-- 数据库提供者 -->
|
<!-- 数据库提供者 -->
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.0" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.0" />
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.0" />
|
||||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.0" />
|
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.0" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.0" />
|
||||||
|
|
||||||
<!-- 依赖注入 -->
|
<!-- 依赖注入 -->
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Sharp.Data.Repository;
|
||||||
|
|
||||||
|
namespace Data.DependencyInjection;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 数据访问层便捷注册扩展
|
||||||
|
/// </summary>
|
||||||
|
public static class DataServiceExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 添加 Sharp 数据库支持(约定优于配置)
|
||||||
|
/// 自动从配置读取数据库连接信息
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TContext">DbContext 类型</typeparam>
|
||||||
|
/// <param name="services">服务集合</param>
|
||||||
|
/// <param name="configuration">配置</param>
|
||||||
|
/// <param name="configSection">配置节名称,默认为 "Database"</param>
|
||||||
|
/// <returns>服务集合</returns>
|
||||||
|
public static IServiceCollection AddSharpDatabase<TContext>(
|
||||||
|
this IServiceCollection services,
|
||||||
|
IConfiguration configuration,
|
||||||
|
string configSection = "Database")
|
||||||
|
where TContext : DbContext
|
||||||
|
{
|
||||||
|
var provider = configuration[$"{configSection}:Provider"] ?? "SqlServer";
|
||||||
|
var connectionString = configuration[$"{configSection}:ConnectionString"]
|
||||||
|
?? throw new InvalidOperationException(
|
||||||
|
$"未找到数据库连接字符串配置: {configSection}:ConnectionString");
|
||||||
|
|
||||||
|
return AddSharpDatabase<TContext>(services, provider, connectionString);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 添加 Sharp 数据库支持(显式指定提供程序和连接字符串)
|
||||||
|
/// </summary>
|
||||||
|
public static IServiceCollection AddSharpDatabase<TContext>(
|
||||||
|
this IServiceCollection services,
|
||||||
|
string provider,
|
||||||
|
string connectionString)
|
||||||
|
where TContext : DbContext
|
||||||
|
{
|
||||||
|
services.AddDbContext<TContext>(options =>
|
||||||
|
{
|
||||||
|
switch (provider.ToLower())
|
||||||
|
{
|
||||||
|
case "sqlserver":
|
||||||
|
case "mssql":
|
||||||
|
options.UseSqlServer(connectionString, sqlOptions =>
|
||||||
|
{
|
||||||
|
sqlOptions.EnableRetryOnFailure(
|
||||||
|
maxRetryCount: 3,
|
||||||
|
maxRetryDelay: TimeSpan.FromSeconds(5),
|
||||||
|
errorNumbersToAdd: null);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "sqlite":
|
||||||
|
options.UseSqlite(connectionString);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "postgresql":
|
||||||
|
case "postgres":
|
||||||
|
options.UseNpgsql(connectionString, npgsqlOptions =>
|
||||||
|
{
|
||||||
|
npgsqlOptions.EnableRetryOnFailure(3, TimeSpan.FromSeconds(5), null);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "mysql":
|
||||||
|
var serverVersion = ServerVersion.AutoDetect(connectionString);
|
||||||
|
options.UseMySql(connectionString, serverVersion, mysqlOptions =>
|
||||||
|
{
|
||||||
|
mysqlOptions.EnableRetryOnFailure(3, TimeSpan.FromSeconds(5), null);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "inmemory":
|
||||||
|
options.UseInMemoryDatabase(connectionString);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new NotSupportedException($"不支持的数据库提供程序: {provider}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开发环境启用详细错误
|
||||||
|
if (Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") == "Development")
|
||||||
|
{
|
||||||
|
options.EnableDetailedErrors();
|
||||||
|
options.EnableSensitiveDataLogging();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 注册仓储
|
||||||
|
services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
namespace EventBus
|
|
||||||
{
|
|
||||||
public class Class1
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Sharp.EventBus.DependencyInjection;
|
||||||
|
|
||||||
|
public static class ServiceCollectionExtensions
|
||||||
|
{
|
||||||
|
public static IServiceCollection AddEventBus(this IServiceCollection services, Action<EventBusOptions>? configure = null)
|
||||||
|
{
|
||||||
|
var options = new EventBusOptions();
|
||||||
|
configure?.Invoke(options);
|
||||||
|
services.AddSingleton(options);
|
||||||
|
services.AddSingleton<IEventBus, InMemoryEventBus>();
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,18 @@
|
|||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
<AssemblyName>Sharp.EventBus</AssemblyName>
|
||||||
|
<RootNamespace>Sharp.EventBus</RootNamespace>
|
||||||
|
|
||||||
|
<!-- 程序集元数据 -->
|
||||||
|
<Description>Sharp Framework 事件总线库 - 进程内事件发布/订阅(规划中)</Description>
|
||||||
|
|
||||||
|
<!-- NuGet 包元数据 -->
|
||||||
|
<PackageTags>sharp;eventbus;pub-sub;events</PackageTags>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Sharp.EventBus;
|
||||||
|
|
||||||
|
public class EventBusOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 事件队列容量,默认 1024;满时 Publish 会等待。
|
||||||
|
/// </summary>
|
||||||
|
public int ChannelCapacity { get; set; } = 1024;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 全局错误回调。
|
||||||
|
/// </summary>
|
||||||
|
public Action<Exception, object>? OnError { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Sharp.EventBus;
|
||||||
|
|
||||||
|
internal sealed class EventSubscription : IDisposable
|
||||||
|
{
|
||||||
|
public Type EventType { get; }
|
||||||
|
public int Priority { get; }
|
||||||
|
public Func<object, Task> Handler { get; }
|
||||||
|
public Func<object, bool>? Filter { get; }
|
||||||
|
private readonly Action<EventSubscription> _onDispose;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public EventSubscription(Type eventType, Func<object, Task> handler, Func<object, bool>? filter, int priority, Action<EventSubscription> onDispose)
|
||||||
|
{
|
||||||
|
EventType = eventType;
|
||||||
|
Handler = handler;
|
||||||
|
Filter = filter;
|
||||||
|
Priority = priority;
|
||||||
|
_onDispose = onDispose;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CanHandle(object evt)
|
||||||
|
{
|
||||||
|
if (evt.GetType() != EventType) return false;
|
||||||
|
return Filter == null || Filter(evt);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed) return;
|
||||||
|
_disposed = true;
|
||||||
|
_onDispose(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Sharp.EventBus;
|
||||||
|
|
||||||
|
public interface IEventBus : IDisposable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 异步发布事件(入队后立即返回)。
|
||||||
|
/// </summary>
|
||||||
|
Task PublishAsync<TEvent>(TEvent @event, CancellationToken cancellationToken = default) where TEvent : class;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 同步包装,遇到队列满或取消会抛出。
|
||||||
|
/// </summary>
|
||||||
|
void Publish<TEvent>(TEvent @event) where TEvent : class;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订阅事件(同步处理器)。
|
||||||
|
/// </summary>
|
||||||
|
IDisposable Subscribe<TEvent>(Action<TEvent> handler, Func<TEvent, bool>? filter = null, int priority = 0) where TEvent : class;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订阅事件(异步处理器)。
|
||||||
|
/// </summary>
|
||||||
|
IDisposable Subscribe<TEvent>(Func<TEvent, Task> handler, Func<TEvent, bool>? filter = null, int priority = 0) where TEvent : class;
|
||||||
|
}
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Threading.Channels;
|
||||||
|
|
||||||
|
namespace Sharp.EventBus;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 基于 Channel 的进程内事件总线,线程安全,按优先级顺序分发。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class InMemoryEventBus : IEventBus
|
||||||
|
{
|
||||||
|
private readonly Channel<object> _channel;
|
||||||
|
private readonly ConcurrentDictionary<Type, List<EventSubscription>> _subscriptions = new();
|
||||||
|
private readonly CancellationTokenSource _cts = new();
|
||||||
|
private readonly Task _worker;
|
||||||
|
private readonly EventBusOptions _options;
|
||||||
|
|
||||||
|
public InMemoryEventBus(EventBusOptions? options = null)
|
||||||
|
{
|
||||||
|
_options = options ?? new EventBusOptions();
|
||||||
|
_channel = Channel.CreateBounded<object>(new BoundedChannelOptions(_options.ChannelCapacity)
|
||||||
|
{
|
||||||
|
SingleReader = true,
|
||||||
|
SingleWriter = false,
|
||||||
|
FullMode = BoundedChannelFullMode.Wait
|
||||||
|
});
|
||||||
|
_worker = Task.Run(ProcessAsync, _cts.Token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Publish<TEvent>(TEvent @event) where TEvent : class
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
_channel.Writer.WriteAsync(@event, _cts.Token).AsTask().GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task PublishAsync<TEvent>(TEvent @event, CancellationToken cancellationToken = default) where TEvent : class
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
return _channel.Writer.WriteAsync(@event, cancellationToken).AsTask();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IDisposable Subscribe<TEvent>(Action<TEvent> handler, Func<TEvent, bool>? filter = null, int priority = 0) where TEvent : class
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
if (handler == null) throw new ArgumentNullException(nameof(handler));
|
||||||
|
|
||||||
|
Task Wrapper(object evt)
|
||||||
|
{
|
||||||
|
handler((TEvent)evt);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
return AddSubscription(typeof(TEvent), Wrapper, evt => filter?.Invoke((TEvent)evt) ?? true, priority);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IDisposable Subscribe<TEvent>(Func<TEvent, Task> handler, Func<TEvent, bool>? filter = null, int priority = 0) where TEvent : class
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
if (handler == null) throw new ArgumentNullException(nameof(handler));
|
||||||
|
|
||||||
|
Task Wrapper(object evt) => handler((TEvent)evt);
|
||||||
|
|
||||||
|
return AddSubscription(typeof(TEvent), Wrapper, evt => filter?.Invoke((TEvent)evt) ?? true, priority);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IDisposable AddSubscription(Type eventType, Func<object, Task> handler, Func<object, bool>? filter, int priority)
|
||||||
|
{
|
||||||
|
var sub = new EventSubscription(eventType, handler, filter, priority, RemoveSubscription);
|
||||||
|
var list = _subscriptions.GetOrAdd(eventType, _ => new List<EventSubscription>());
|
||||||
|
lock (list)
|
||||||
|
{
|
||||||
|
list.Add(sub);
|
||||||
|
list.Sort((a, b) => a.Priority.CompareTo(b.Priority));
|
||||||
|
}
|
||||||
|
return sub;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveSubscription(EventSubscription subscription)
|
||||||
|
{
|
||||||
|
if (_subscriptions.TryGetValue(subscription.EventType, out var list))
|
||||||
|
{
|
||||||
|
lock (list)
|
||||||
|
{
|
||||||
|
list.Remove(subscription);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ProcessAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (await _channel.Reader.WaitToReadAsync(_cts.Token).ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
while (_channel.Reader.TryRead(out var evt))
|
||||||
|
{
|
||||||
|
await DispatchAsync(evt).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// 正常关闭
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DispatchAsync(object evt)
|
||||||
|
{
|
||||||
|
if (_subscriptions.TryGetValue(evt.GetType(), out var list))
|
||||||
|
{
|
||||||
|
List<EventSubscription> snapshot;
|
||||||
|
lock (list)
|
||||||
|
{
|
||||||
|
snapshot = list.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var sub in snapshot)
|
||||||
|
{
|
||||||
|
if (!sub.CanHandle(evt)) continue;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await sub.Handler(evt).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_options.OnError?.Invoke(ex, evt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThrowIfDisposed()
|
||||||
|
{
|
||||||
|
if (_cts.IsCancellationRequested) throw new ObjectDisposedException(nameof(InMemoryEventBus));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_cts.Cancel();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_channel.Writer.TryComplete();
|
||||||
|
_worker.Wait(TimeSpan.FromSeconds(1));
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
_cts.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,457 @@
|
|||||||
|
# Sharp.EventBus
|
||||||
|
|
||||||
|
> **🚧 开发中 / Under Construction**
|
||||||
|
|
||||||
|
轻量级进程内事件总线,用于解耦应用程序中的模块通信。
|
||||||
|
|
||||||
|
## 📋 为什么需要 EventBus?
|
||||||
|
|
||||||
|
### 场景对比
|
||||||
|
|
||||||
|
#### ❌ 不使用 EventBus - 紧耦合
|
||||||
|
```csharp
|
||||||
|
public class PlcMonitorService {
|
||||||
|
private readonly MainViewModel _mainViewModel;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
private readonly IRepository<PlcData> _repository;
|
||||||
|
|
||||||
|
// 需要注入所有订阅者,耦合严重
|
||||||
|
public PlcMonitorService(MainViewModel vm, ILogger logger, IRepository<PlcData> repo) { }
|
||||||
|
|
||||||
|
private void OnDataChanged(PlcTag tag, object value) {
|
||||||
|
_mainViewModel.UpdateValue(tag.Id, value); // UI更新
|
||||||
|
_logger.LogInformation($"Tag {tag.Id} changed"); // 日志
|
||||||
|
_repository.AddAsync(new PlcData { ... }); // 存储
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### ✅ 使用 EventBus - 解耦
|
||||||
|
```csharp
|
||||||
|
// 发布者:只管发布,不关心谁处理
|
||||||
|
public class PlcMonitorService {
|
||||||
|
private readonly IEventBus _eventBus;
|
||||||
|
|
||||||
|
private void OnDataChanged(PlcTag tag, object value) {
|
||||||
|
_eventBus.Publish(new PlcDataChangedEvent(tag.Id, value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 订阅者1:UI
|
||||||
|
public class MainViewModel {
|
||||||
|
public MainViewModel(IEventBus eventBus) {
|
||||||
|
eventBus.Subscribe<PlcDataChangedEvent>(e => UpdateValue(e.TagId, e.Value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 订阅者2:日志(后续添加,无需修改发布者)
|
||||||
|
public class PlcDataLogger {
|
||||||
|
public PlcDataLogger(IEventBus eventBus) {
|
||||||
|
eventBus.Subscribe<PlcDataChangedEvent>(e =>
|
||||||
|
_logger.LogInformation($"Tag {e.TagId} changed"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 核心优势
|
||||||
|
|
||||||
|
1. **解耦** - 发布者不需要知道订阅者
|
||||||
|
2. **可扩展** - 运行时动态添加/移除订阅者
|
||||||
|
3. **类型安全** - 强类型事件,编译期检查
|
||||||
|
4. **跨层通信** - ViewModels、Services、Repositories 之间自由通信
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 设计目标
|
||||||
|
|
||||||
|
1. **简单易用** - 一行注册,一行订阅
|
||||||
|
2. **类型安全** - 强类型事件,避免字符串魔法值
|
||||||
|
3. **性能优先** - 基于 `System.Threading.Channels`,高性能异步
|
||||||
|
4. **线程安全** - 支持多线程发布/订阅
|
||||||
|
5. **生命周期管理** - 自动清理订阅,防止内存泄漏
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 快速开始(未来)
|
||||||
|
|
||||||
|
### 1. 注册服务
|
||||||
|
```csharp
|
||||||
|
// App.xaml.cs
|
||||||
|
protected override void OnStartup(StartupEventArgs e) {
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
|
||||||
|
// 一行注册
|
||||||
|
services.AddEventBus();
|
||||||
|
|
||||||
|
var provider = services.BuildServiceProvider();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 定义事件
|
||||||
|
```csharp
|
||||||
|
// 事件:简单的 POCO 类
|
||||||
|
public record UserLoginEvent(string Username, DateTime LoginTime);
|
||||||
|
|
||||||
|
public record PlcDataChangedEvent(string TagId, object Value);
|
||||||
|
|
||||||
|
public record OrderCreatedEvent(int OrderId, decimal TotalAmount);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 发布事件
|
||||||
|
```csharp
|
||||||
|
public class LoginService {
|
||||||
|
private readonly IEventBus _eventBus;
|
||||||
|
|
||||||
|
public LoginService(IEventBus eventBus) => _eventBus = eventBus;
|
||||||
|
|
||||||
|
public async Task LoginAsync(string username, string password) {
|
||||||
|
// ... 登录逻辑
|
||||||
|
|
||||||
|
// 发布事件
|
||||||
|
_eventBus.Publish(new UserLoginEvent(username, DateTime.Now));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 订阅事件
|
||||||
|
```csharp
|
||||||
|
// 方式1:在构造函数中订阅
|
||||||
|
public class MainViewModel : ViewModelBase {
|
||||||
|
private IDisposable? _subscription;
|
||||||
|
|
||||||
|
public MainViewModel(IEventBus eventBus) {
|
||||||
|
_subscription = eventBus.Subscribe<UserLoginEvent>(OnUserLogin);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnUserLogin(UserLoginEvent e) {
|
||||||
|
WelcomeMessage = $"欢迎, {e.Username}!";
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing) {
|
||||||
|
_subscription?.Dispose(); // 取消订阅
|
||||||
|
base.Dispose(disposing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 方式2:异步订阅
|
||||||
|
public class DataSyncService {
|
||||||
|
public DataSyncService(IEventBus eventBus) {
|
||||||
|
eventBus.Subscribe<OrderCreatedEvent>(async e => {
|
||||||
|
await SyncToRemoteAsync(e.OrderId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 方式3:条件过滤
|
||||||
|
public class HighValueOrderMonitor {
|
||||||
|
public HighValueOrderMonitor(IEventBus eventBus) {
|
||||||
|
eventBus.Subscribe<OrderCreatedEvent>(e => e.TotalAmount > 10000, e => {
|
||||||
|
// 只处理大额订单
|
||||||
|
NotifyManager(e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 核心 API 设计
|
||||||
|
|
||||||
|
### IEventBus 接口
|
||||||
|
```csharp
|
||||||
|
public interface IEventBus {
|
||||||
|
/// <summary>
|
||||||
|
/// 发布事件(同步)
|
||||||
|
/// </summary>
|
||||||
|
void Publish<TEvent>(TEvent @event) where TEvent : class;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 发布事件(异步)
|
||||||
|
/// </summary>
|
||||||
|
Task PublishAsync<TEvent>(TEvent @event, CancellationToken ct = default) where TEvent : class;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订阅事件(同步处理器)
|
||||||
|
/// </summary>
|
||||||
|
IDisposable Subscribe<TEvent>(Action<TEvent> handler) where TEvent : class;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订阅事件(异步处理器)
|
||||||
|
/// </summary>
|
||||||
|
IDisposable Subscribe<TEvent>(Func<TEvent, Task> handler) where TEvent : class;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订阅事件(带过滤条件)
|
||||||
|
/// </summary>
|
||||||
|
IDisposable Subscribe<TEvent>(
|
||||||
|
Func<TEvent, bool> filter,
|
||||||
|
Action<TEvent> handler) where TEvent : class;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订阅事件(带优先级,数字越小优先级越高)
|
||||||
|
/// </summary>
|
||||||
|
IDisposable Subscribe<TEvent>(
|
||||||
|
Action<TEvent> handler,
|
||||||
|
int priority = 0) where TEvent : class;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### EventSubscription 类(流式API)
|
||||||
|
```csharp
|
||||||
|
public class EventSubscription<TEvent> : IDisposable where TEvent : class {
|
||||||
|
/// <summary>
|
||||||
|
/// 添加过滤条件
|
||||||
|
/// </summary>
|
||||||
|
public EventSubscription<TEvent> Where(Func<TEvent, bool> predicate);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设置优先级
|
||||||
|
/// </summary>
|
||||||
|
public EventSubscription<TEvent> WithPriority(int priority);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 在指定线程上执行(如UI线程)
|
||||||
|
/// </summary>
|
||||||
|
public EventSubscription<TEvent> OnThread(TaskScheduler scheduler);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 延迟执行
|
||||||
|
/// </summary>
|
||||||
|
public EventSubscription<TEvent> Delay(TimeSpan delay);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 最终处理器
|
||||||
|
/// </summary>
|
||||||
|
public IDisposable Handle(Action<TEvent> handler);
|
||||||
|
|
||||||
|
public IDisposable HandleAsync(Func<TEvent, Task> handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用示例
|
||||||
|
eventBus.CreateSubscription<PlcDataChangedEvent>()
|
||||||
|
.Where(e => e.TagId.StartsWith("Temperature"))
|
||||||
|
.Delay(TimeSpan.FromSeconds(1)) // 防抖
|
||||||
|
.OnThread(TaskScheduler.FromCurrentSynchronizationContext()) // UI线程
|
||||||
|
.Handle(e => UpdateChart(e.Value));
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 高级功能
|
||||||
|
|
||||||
|
### 1. 事件优先级
|
||||||
|
```csharp
|
||||||
|
// 优先级:数字越小越先执行
|
||||||
|
eventBus.Subscribe<OrderCreatedEvent>(e => ValidateOrder(e), priority: 1);
|
||||||
|
eventBus.Subscribe<OrderCreatedEvent>(e => SendEmail(e), priority: 10);
|
||||||
|
eventBus.Subscribe<OrderCreatedEvent>(e => UpdateStatistics(e), priority: 20);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 弱引用订阅(防止内存泄漏)
|
||||||
|
```csharp
|
||||||
|
// 自动模式:订阅者被GC回收时自动取消订阅
|
||||||
|
eventBus.SubscribeWeak<UserLoginEvent>(viewModel, (vm, e) => vm.OnLogin(e));
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 延迟发布
|
||||||
|
```csharp
|
||||||
|
// 延迟1秒发布(防抖场景)
|
||||||
|
eventBus.PublishDelayed(new SearchTextChangedEvent(text), TimeSpan.FromSeconds(1));
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 错误处理
|
||||||
|
```csharp
|
||||||
|
// 全局错误处理器
|
||||||
|
services.AddEventBus(options => {
|
||||||
|
options.OnError = (exception, @event) => {
|
||||||
|
logger.LogError(exception, "事件处理失败: {@Event}", @event);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// 单个订阅的错误处理
|
||||||
|
eventBus.Subscribe<OrderCreatedEvent>(e => {
|
||||||
|
try {
|
||||||
|
ProcessOrder(e);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
logger.LogError(ex, "订单处理失败");
|
||||||
|
// 不影响其他订阅者
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 事件日志(可选)
|
||||||
|
```csharp
|
||||||
|
services.AddEventBus(options => {
|
||||||
|
options.EnableEventLogging = true; // 记录所有事件发布
|
||||||
|
options.LogEventData = true; // 记录事件数据(调试用)
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 典型使用场景
|
||||||
|
|
||||||
|
### 场景1:跨 ViewModel 通信
|
||||||
|
```csharp
|
||||||
|
// LoginViewModel 发布登录成功事件
|
||||||
|
public class LoginViewModel {
|
||||||
|
private async Task LoginAsync() {
|
||||||
|
// ... 登录逻辑
|
||||||
|
_eventBus.Publish(new UserLoginSuccessEvent(user));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MainViewModel 订阅并更新UI
|
||||||
|
public class MainViewModel {
|
||||||
|
public MainViewModel(IEventBus eventBus) {
|
||||||
|
eventBus.Subscribe<UserLoginSuccessEvent>(e => {
|
||||||
|
CurrentUser = e.User;
|
||||||
|
LoadUserData();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景2:PLC数据监控
|
||||||
|
```csharp
|
||||||
|
// PLC监控服务发布数据变化
|
||||||
|
public class PlcMonitorService {
|
||||||
|
private void OnTagChanged(PlcTag tag, object value) {
|
||||||
|
_eventBus.Publish(new PlcDataChangedEvent(tag.Id, value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI订阅更新界面
|
||||||
|
public class DashboardViewModel {
|
||||||
|
public DashboardViewModel(IEventBus eventBus) {
|
||||||
|
eventBus.Subscribe<PlcDataChangedEvent>(
|
||||||
|
e => e.TagId == "Temperature1",
|
||||||
|
e => Temperature = (double)e.Value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 数据记录服务订阅存储数据
|
||||||
|
public class DataLoggerService {
|
||||||
|
public DataLoggerService(IEventBus eventBus) {
|
||||||
|
eventBus.Subscribe<PlcDataChangedEvent>(async e => {
|
||||||
|
await _repository.AddAsync(new DataLog {
|
||||||
|
TagId = e.TagId,
|
||||||
|
Value = e.Value,
|
||||||
|
Timestamp = DateTime.Now
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景3:报警系统
|
||||||
|
```csharp
|
||||||
|
// 报警事件
|
||||||
|
public record AlarmTriggeredEvent(string AlarmId, AlarmLevel Level, string Message);
|
||||||
|
|
||||||
|
// 多个订阅者处理报警
|
||||||
|
eventBus.Subscribe<AlarmTriggeredEvent>(e => e.Level >= AlarmLevel.High, e => {
|
||||||
|
PlayAlarmSound(); // UI线程
|
||||||
|
});
|
||||||
|
|
||||||
|
eventBus.Subscribe<AlarmTriggeredEvent>(async e => {
|
||||||
|
await SendEmailToManager(e); // 异步发送邮件
|
||||||
|
});
|
||||||
|
|
||||||
|
eventBus.Subscribe<AlarmTriggeredEvent>(e => {
|
||||||
|
LogAlarm(e); // 记录日志
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ 配置选项
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
services.AddEventBus(options => {
|
||||||
|
// 通道容量(默认1000)
|
||||||
|
options.ChannelCapacity = 10000;
|
||||||
|
|
||||||
|
// 错误处理
|
||||||
|
options.OnError = (ex, evt) => logger.LogError(ex, "事件错误");
|
||||||
|
|
||||||
|
// 事件日志
|
||||||
|
options.EnableEventLogging = true;
|
||||||
|
|
||||||
|
// 超时配置(异步处理器超时时间)
|
||||||
|
options.HandlerTimeout = TimeSpan.FromSeconds(30);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ 实现方案
|
||||||
|
|
||||||
|
### 基于 System.Threading.Channels
|
||||||
|
- **高性能**:异步无锁队列
|
||||||
|
- **线程安全**:内置并发支持
|
||||||
|
- **背压支持**:通道容量控制
|
||||||
|
|
||||||
|
### 架构设计
|
||||||
|
```
|
||||||
|
IEventBus(接口)
|
||||||
|
↓
|
||||||
|
InMemoryEventBus(默认实现)
|
||||||
|
↓ 内部使用
|
||||||
|
System.Threading.Channels.Channel<EventEnvelope>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 性能目标
|
||||||
|
|
||||||
|
- **发布延迟**:< 1ms(同步),< 10ms(异步)
|
||||||
|
- **吞吐量**:> 100,000 事件/秒
|
||||||
|
- **内存占用**:最小化,弱引用订阅防止泄漏
|
||||||
|
- **线程安全**:完全线程安全
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 与其他模式对比
|
||||||
|
|
||||||
|
| 模式 | 优点 | 缺点 | 适用场景 |
|
||||||
|
|-----|------|------|---------|
|
||||||
|
| **EventBus** | 完全解耦、多订阅者 | 略微增加复杂度 | 跨模块通信、插件架构 |
|
||||||
|
| **直接回调** | 简单直接 | 紧耦合、单订阅者 | 简单父子组件通信 |
|
||||||
|
| **事件聚合器** | 解耦 | 需要手动管理 | 中等复杂度应用 |
|
||||||
|
| **消息队列** | 跨进程、持久化 | 重量级、需要中间件 | 分布式系统 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 注意事项
|
||||||
|
|
||||||
|
1. **不要滥用**:简单的父子组件通信用直接回调即可
|
||||||
|
2. **事件命名**:使用过去式(如 `UserLoggedInEvent` 而非 `UserLoginEvent`)
|
||||||
|
3. **事件不可变**:推荐使用 `record` 定义事件
|
||||||
|
4. **及时取消订阅**:避免内存泄漏
|
||||||
|
5. **异常处理**:订阅者内部处理异常,不要抛出
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 未来扩展
|
||||||
|
|
||||||
|
### 可能的扩展方向
|
||||||
|
1. **分布式 EventBus**(基于 Redis Pub/Sub)
|
||||||
|
2. **事件溯源**(Event Sourcing)支持
|
||||||
|
3. **事件重放**(Replay)功能
|
||||||
|
4. **事件转换**(Transform)中间件
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📖 参考资料
|
||||||
|
|
||||||
|
- [MediatR](https://github.com/jbogard/MediatR) - .NET 消息中介库
|
||||||
|
- [Prism EventAggregator](https://prismlibrary.com/docs/event-aggregator.html) - WPF MVVM 事件聚合器
|
||||||
|
- [System.Threading.Channels](https://devblogs.microsoft.com/dotnet/an-introduction-to-system-threading-channels/) - .NET 高性能通道
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**状态**:设计阶段,未实现
|
||||||
|
**预计实现时间**:根据实际需求决定
|
||||||
|
**当前替代方案**:使用 Common 模块的 Global 容器 + 委托事件
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
# Sharp Project Context
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
**Sharp** is a modular C# .NET 8 solution designed for industrial automation and network services. It features a "Spring Boot-like" HTTP server framework, a robust PLC communication library, and WPF-based client/server applications. The architecture emphasizes dependency injection, functional error handling (Result pattern), and clean separation of concerns.
|
||||||
|
|
||||||
|
## Key Modules
|
||||||
|
|
||||||
|
### Core Infrastructure
|
||||||
|
- **Common**: A utility library providing a lightweight DI container (`Global`), functional `Result<T>` pattern, and extensions for Strings, DateTime, Encryption, and Reflection.
|
||||||
|
- **Config**: Configuration management.
|
||||||
|
- **Log**: Integration with `Microsoft.Extensions.Logging`.
|
||||||
|
- **EventBus**: Event-driven architecture components.
|
||||||
|
|
||||||
|
### Networking & Communications
|
||||||
|
- **NetServer**: A lightweight, attribute-based HTTP server framework built on `HttpListener`. Features include:
|
||||||
|
- Routing (`[Route]`, `[HttpGet]`), Authentication (`[Authorize]`), Rate Limiting, and Middleware support.
|
||||||
|
- **NetClient**: Typed HTTP client abstractions.
|
||||||
|
- **Plc**: Industrial communication abstraction layer.
|
||||||
|
- **Plc.Siemens**: Implementation for Siemens S7 CPUs using optimized batch reading strategies (merging adjacent tags to reduce network overhead).
|
||||||
|
- **Ports**: Serial/COM port abstractions.
|
||||||
|
|
||||||
|
### Applications (UI)
|
||||||
|
- **Client**: Main WPF Client application.
|
||||||
|
- **Server**: WPF Server application (likely a dashboard or local control interface).
|
||||||
|
- **Wpf**: Shared WPF UI logic, behaviors, and converters.
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- .NET 8 SDK
|
||||||
|
- Visual Studio 2022 or VS Code (with C# Dev Kit)
|
||||||
|
|
||||||
|
### Building & Running
|
||||||
|
- **Restore Dependencies**:
|
||||||
|
```bash
|
||||||
|
dotnet restore
|
||||||
|
```
|
||||||
|
- **Build Solution**:
|
||||||
|
```bash
|
||||||
|
dotnet build Sharp.sln -c Debug
|
||||||
|
```
|
||||||
|
- **Run WPF Client**:
|
||||||
|
```bash
|
||||||
|
dotnet run --project Client/Client.csproj
|
||||||
|
```
|
||||||
|
- **Run NetServer Example**:
|
||||||
|
```bash
|
||||||
|
dotnet run --project NetServer/Examples
|
||||||
|
# Note: Check specific project path if Examples is a folder or project
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- Tests are located in the `../../test/` directory (relative to `src/Sharp`).
|
||||||
|
- **Run All Tests**:
|
||||||
|
```bash
|
||||||
|
dotnet test Sharp.sln
|
||||||
|
```
|
||||||
|
- **Run Specific Module Tests**:
|
||||||
|
```bash
|
||||||
|
dotnet test ../../test/Common.Test/Common.Test.csproj
|
||||||
|
```
|
||||||
|
|
||||||
|
## Code Conventions & Standards
|
||||||
|
|
||||||
|
- **Language**: C# 12 / .NET 8 (`net8.0-windows` for UI).
|
||||||
|
- **Naming**:
|
||||||
|
- `PascalCase` for public members and types.
|
||||||
|
- `camelCase` for private fields and local variables.
|
||||||
|
- Async methods must end with `Async`.
|
||||||
|
- Interfaces prefixed with `I`.
|
||||||
|
- **Architecture**:
|
||||||
|
- Prefer **Dependency Injection** (DI) via constructor injection.
|
||||||
|
- Use the **Result Pattern** (`Result<T>`) from `Common` for flow control instead of exceptions for business logic.
|
||||||
|
- **Plc Communication**: Use `IPlcService` and `PlcTag` for managed, optimized data access.
|
||||||
|
- **Git Commit Messages**:
|
||||||
|
- Concise and imperative (e.g., `feat: add plc reconnection logic` or `修复: 优化批量读取算法`).
|
||||||
|
- Chinese commit messages are acceptable and common in history.
|
||||||
|
|
||||||
|
## Important Implementation Details
|
||||||
|
- **Plc Optimization**: The `SiemensOptimizer` automatically batches read requests based on `MaxGapBytes` (default 20 bytes) and PDU size to maximize throughput.
|
||||||
|
- **NetServer Middleware**: Middleware order matters. Default pipeline: Exception -> Logging -> Cors -> Auth -> RateLimit -> Cache -> Route.
|
||||||
|
- **Common Utilities**: extensively use `Global` for service location in legacy/static contexts, but prefer DI in modern components.
|
||||||
@@ -1,9 +1,15 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
|
||||||
|
<!-- 程序集元数据 -->
|
||||||
|
<Description>Sharp Framework 日志库 - 基于 Serilog 的结构化日志系统,支持文件、控制台等多种输出</Description>
|
||||||
|
|
||||||
|
<!-- NuGet 包元数据 -->
|
||||||
|
<PackageTags>sharp;log;logging;serilog;structured-logging</PackageTags>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
namespace NetClient
|
|
||||||
{
|
|
||||||
public class Class1
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,7 @@ using System;
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Config;
|
using Config;
|
||||||
using NetClient.Options;
|
using NetClient.Options;
|
||||||
using Sharp.Infrastructure.Configuration;
|
using Common.Configuration;
|
||||||
|
|
||||||
namespace NetClient.DependencyInjection;
|
namespace NetClient.DependencyInjection;
|
||||||
|
|
||||||
|
|||||||
@@ -4,19 +4,24 @@
|
|||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
|
||||||
|
<!-- 程序集元数据 -->
|
||||||
|
<Description>Sharp Framework HTTP 客户端库 - 集成 Polly 弹性策略的 HTTP 客户端</Description>
|
||||||
|
|
||||||
|
<!-- NuGet 包元数据 -->
|
||||||
|
<PackageTags>sharp;http;client;polly;resilience</PackageTags>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.Http" Version="8.*" />
|
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="8.*" />
|
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="9.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.*" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
|
||||||
<PackageReference Include="Polly.Contrib.WaitAndRetry" Version="1.*" />
|
<PackageReference Include="Polly.Contrib.WaitAndRetry" Version="1.*" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Common\Common.csproj" />
|
<ProjectReference Include="..\Common\Common.csproj" />
|
||||||
<ProjectReference Include="..\Config\Config.csproj" />
|
<ProjectReference Include="..\Config\Config.csproj" />
|
||||||
<ProjectReference Include="..\Sharp.Infrastructure\Sharp.Infrastructure.csproj" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using Config;
|
using Config;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
using Sharp.Infrastructure.Configuration;
|
using Common.Configuration;
|
||||||
using Sharp.NetServer.Server;
|
using Sharp.NetServer.Server;
|
||||||
|
|
||||||
namespace Sharp.NetServer.DependencyInjection;
|
namespace Sharp.NetServer.DependencyInjection;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Sharp.NetServer.Abstractions;
|
using Sharp.NetServer.Abstractions;
|
||||||
@@ -45,31 +46,48 @@ public class ResponseCacheMiddleware : IMiddleware
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 捕获响应
|
// 捕获响应
|
||||||
var originalStream = context.Response.OutputStream;
|
|
||||||
using var memoryStream = new MemoryStream();
|
|
||||||
|
|
||||||
// 临时替换输出流
|
|
||||||
var response = context.Response;
|
var response = context.Response;
|
||||||
var originalContentType = response.ContentType;
|
using var captured = new MemoryStream();
|
||||||
var originalStatusCode = response.StatusCode;
|
var originalStream = response.SwapOutputStream(captured);
|
||||||
|
|
||||||
// 执行下一个中间件
|
try
|
||||||
await next();
|
|
||||||
|
|
||||||
// 如果响应成功,缓存结果
|
|
||||||
if (response.StatusCode == 200)
|
|
||||||
{
|
{
|
||||||
// 注意:由于 HttpListenerResponse 的限制,我们无法直接拦截输出流
|
await next();
|
||||||
// 这里提供一个简化的实现,实际使用中可能需要在控制器层面配合
|
|
||||||
var cachedData = new CachedResponse
|
|
||||||
{
|
|
||||||
ContentType = response.ContentType ?? "application/json",
|
|
||||||
StatusCode = response.StatusCode,
|
|
||||||
Headers = new Dictionary<string, string>(),
|
|
||||||
// Body 需要在控制器中通过特殊处理来缓存
|
|
||||||
};
|
|
||||||
|
|
||||||
await _cache.SetAsync(cacheKey, cachedData, TimeSpan.FromSeconds(config.Duration));
|
// 只处理捕获后的数据
|
||||||
|
var bodyBytes = captured.ToArray();
|
||||||
|
response.ContentLength = bodyBytes.Length;
|
||||||
|
|
||||||
|
// MISS 标记
|
||||||
|
response.AddHeader("X-Cache", "MISS");
|
||||||
|
|
||||||
|
// 写回真实输出
|
||||||
|
await response.FlushCapturedBodyAsync(captured, context.CancellationToken);
|
||||||
|
|
||||||
|
if (response.StatusCode == 200)
|
||||||
|
{
|
||||||
|
var headers = new Dictionary<string, string>();
|
||||||
|
foreach (var key in response.Headers.AllKeys)
|
||||||
|
{
|
||||||
|
if (key is null) continue;
|
||||||
|
headers[key] = response.Headers[key]!;
|
||||||
|
}
|
||||||
|
|
||||||
|
var cachedData = new CachedResponse
|
||||||
|
{
|
||||||
|
ContentType = response.ContentType ?? "application/json",
|
||||||
|
StatusCode = response.StatusCode,
|
||||||
|
Headers = headers,
|
||||||
|
Body = bodyBytes
|
||||||
|
};
|
||||||
|
|
||||||
|
await _cache.SetAsync(cacheKey, cachedData, TimeSpan.FromSeconds(config.Duration));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// 还原输出流,避免影响后续逻辑
|
||||||
|
response.SwapOutputStream(originalStream);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,11 +129,9 @@ public class ResponseCacheMiddleware : IMiddleware
|
|||||||
context.Response.AddHeader(header.Key, header.Value);
|
context.Response.AddHeader(header.Key, header.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cachedResponse.Body != null)
|
var body = cachedResponse.Body ?? Array.Empty<byte>();
|
||||||
{
|
context.Response.ContentLength = body.Length;
|
||||||
context.Response.ContentLength = cachedResponse.Body.Length;
|
await context.Response.OutputStream.WriteAsync(body);
|
||||||
await context.Response.OutputStream.WriteAsync(cachedResponse.Body);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Collections.Specialized;
|
using System.Collections.Specialized;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
namespace Sharp.NetServer.Models;
|
namespace Sharp.NetServer.Models;
|
||||||
|
|
||||||
@@ -101,10 +102,12 @@ public class HttpRequestWrapper
|
|||||||
public class HttpResponseWrapper
|
public class HttpResponseWrapper
|
||||||
{
|
{
|
||||||
private readonly HttpListenerResponse _response;
|
private readonly HttpListenerResponse _response;
|
||||||
|
private Stream _outputStream;
|
||||||
|
|
||||||
public HttpResponseWrapper(HttpListenerResponse response)
|
public HttpResponseWrapper(HttpListenerResponse response)
|
||||||
{
|
{
|
||||||
_response = response;
|
_response = response;
|
||||||
|
_outputStream = response.OutputStream;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int StatusCode
|
public int StatusCode
|
||||||
@@ -125,7 +128,7 @@ public class HttpResponseWrapper
|
|||||||
set => _response.ContentLength64 = value;
|
set => _response.ContentLength64 = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Stream OutputStream => _response.OutputStream;
|
public Stream OutputStream => _outputStream;
|
||||||
public WebHeaderCollection Headers => _response.Headers;
|
public WebHeaderCollection Headers => _response.Headers;
|
||||||
public CookieCollection Cookies => _response.Cookies;
|
public CookieCollection Cookies => _response.Cookies;
|
||||||
|
|
||||||
@@ -134,6 +137,28 @@ public class HttpResponseWrapper
|
|||||||
_response.AddHeader(name, value);
|
_response.AddHeader(name, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 替换输出流(用于中间件包装),返回之前的流。
|
||||||
|
/// </summary>
|
||||||
|
public Stream SwapOutputStream(Stream stream)
|
||||||
|
{
|
||||||
|
var previous = _outputStream;
|
||||||
|
_outputStream = stream;
|
||||||
|
return previous;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将捕获的内容写回到底层 HttpListener 输出流。
|
||||||
|
/// </summary>
|
||||||
|
public async Task FlushCapturedBodyAsync(Stream captured, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (captured.CanSeek)
|
||||||
|
{
|
||||||
|
captured.Position = 0;
|
||||||
|
}
|
||||||
|
await captured.CopyToAsync(_response.OutputStream, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
public void Redirect(string url)
|
public void Redirect(string url)
|
||||||
{
|
{
|
||||||
_response.Redirect(url);
|
_response.Redirect(url);
|
||||||
|
|||||||
@@ -5,18 +5,23 @@
|
|||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<RootNamespace>Sharp.NetServer</RootNamespace>
|
<RootNamespace>Sharp.NetServer</RootNamespace>
|
||||||
|
|
||||||
|
<!-- 程序集元数据 -->
|
||||||
|
<Description>Sharp Framework HTTP 服务器库 - 轻量级 HTTP 服务器,支持中间件和路由</Description>
|
||||||
|
|
||||||
|
<!-- NuGet 包元数据 -->
|
||||||
|
<PackageTags>sharp;http;server;middleware;routing</PackageTags>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Common\Common.csproj" />
|
<ProjectReference Include="..\Common\Common.csproj" />
|
||||||
<ProjectReference Include="..\Config\Config.csproj" />
|
<ProjectReference Include="..\Config\Config.csproj" />
|
||||||
<ProjectReference Include="..\Sharp.Infrastructure\Sharp.Infrastructure.csproj" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Options" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Options" Version="9.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
@@ -294,20 +294,9 @@ public class HttpServer
|
|||||||
var controller = _serviceProvider.GetService(controllerType);
|
var controller = _serviceProvider.GetService(controllerType);
|
||||||
if (controller != null)
|
if (controller != null)
|
||||||
return controller;
|
return controller;
|
||||||
|
|
||||||
// 使用 ActivatorUtilities 创建实例(可以解析构造函数参数)
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return ActivatorUtilities.CreateInstance(_serviceProvider, controllerType);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger?.LogError(ex, "Failed to create controller {ControllerType} with DI", controllerType.Name);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 没有 DI 容器时,使用默认激活器创建(需要无参构造)
|
// 使用激活器创建
|
||||||
return Activator.CreateInstance(controllerType)!;
|
return Activator.CreateInstance(controllerType)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Plc.Abstractions;
|
||||||
|
using Plc.Services;
|
||||||
|
using Plc.Siemens;
|
||||||
|
using Plc.Siemens.Options;
|
||||||
|
using Plc.Options;
|
||||||
|
|
||||||
|
namespace Plc.Siemens.DependencyInjection;
|
||||||
|
|
||||||
|
public static class ServiceCollectionExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 添加西门子PLC服务到DI容器
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="services">服务集合</param>
|
||||||
|
/// <param name="configureOptions">配置选项的委托</param>
|
||||||
|
/// <returns>服务集合</returns>
|
||||||
|
public static IServiceCollection AddSiemensPlc(
|
||||||
|
this IServiceCollection services,
|
||||||
|
Action<SiemensPlcOptions> configureOptions)
|
||||||
|
{
|
||||||
|
// 使用标准的 IOptions 模式
|
||||||
|
services.Configure(configureOptions);
|
||||||
|
|
||||||
|
// 同时注册 SiemensPlcOptions 单例,供需要直接访问的地方使用(如 PlcViewModel)
|
||||||
|
// 注意:这是一个共享实例,从 IOptions 获取值
|
||||||
|
services.AddSingleton(sp => sp.GetRequiredService<IOptions<SiemensPlcOptions>>().Value);
|
||||||
|
|
||||||
|
// 将 SiemensPlcOptions.MonitorInterval 映射到 PlcMonitorOptions
|
||||||
|
services.Configure<PlcMonitorOptions>(options =>
|
||||||
|
{
|
||||||
|
// 这里我们无法直接获取 IOptions<SiemensPlcOptions>,因为 Configure 是在构建容器前执行的
|
||||||
|
// 所以我们创建一个临时的 SiemensPlcOptions 来获取配置
|
||||||
|
// 但更好的方式是让 PlcMonitorOptions 的配置源自 SiemensPlcOptions 的配置源
|
||||||
|
// 简单起见,我们在这里通过回调再次执行 configureOptions
|
||||||
|
var tempOptions = new SiemensPlcOptions();
|
||||||
|
configureOptions(tempOptions);
|
||||||
|
options.RefreshRate = TimeSpan.FromMilliseconds(tempOptions.MonitorInterval);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 注意:上面的映射方式有一个小缺陷,如果 SiemensPlcOptions 是通过 appsettings.json 绑定的,
|
||||||
|
// 那么这里的 configureOptions 可能只是部分配置或为空。
|
||||||
|
// 更稳健的方式是使用 PostConfigure
|
||||||
|
|
||||||
|
services.PostConfigure<PlcMonitorOptions>(options =>
|
||||||
|
{
|
||||||
|
// 这种方式需要通过 ServiceProvider 获取 SiemensPlcOptions,但在 PostConfigure 中拿不到 ServiceProvider
|
||||||
|
// 所以通常我们在注册服务工厂时手动转换,或者使用 OptionsMonitor
|
||||||
|
});
|
||||||
|
|
||||||
|
// 修正:在注册 IPlcService 时,其实不需要显式配置 PlcMonitorOptions,
|
||||||
|
// 只要我们确保 PlcMonitorOptions 被正确配置即可。
|
||||||
|
// 由于 configureOptions 委托是用户传进来的,我们再次利用它配置 PlcMonitorOptions 是最直接的。
|
||||||
|
// 但如果用户用了 Configuration Bind,那么 configureOptions 可能是空的。
|
||||||
|
|
||||||
|
// 更好的方案:使用 IPostConfigureOptions<PlcMonitorOptions>
|
||||||
|
services.AddSingleton<IPostConfigureOptions<PlcMonitorOptions>, SiemensPlcMonitorOptionsConfigurator>();
|
||||||
|
|
||||||
|
// 注册驱动程序(使用 IOptions)
|
||||||
|
services.AddSingleton<IPlcDriver>(sp =>
|
||||||
|
{
|
||||||
|
var options = sp.GetRequiredService<IOptions<SiemensPlcOptions>>().Value;
|
||||||
|
return new SiemensDriver(options);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 注册优化器(使用 IOptions 中的 MaxGapBytes)
|
||||||
|
services.AddSingleton<IPlcOptimizer>(sp =>
|
||||||
|
{
|
||||||
|
var options = sp.GetRequiredService<IOptions<SiemensPlcOptions>>().Value;
|
||||||
|
var logger = sp.GetService<Microsoft.Extensions.Logging.ILogger<SiemensOptimizer>>();
|
||||||
|
return new SiemensOptimizer(options.MaxGapBytes, logger);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 注册数据转换器
|
||||||
|
services.AddSingleton<IDataConverter, BigEndianConverter>();
|
||||||
|
|
||||||
|
// 注册PLC服务
|
||||||
|
services.AddSingleton<IPlcService, PlcMonitorService>();
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
using Plc.Abstractions;
|
||||||
|
|
||||||
|
namespace Plc.Siemens.Models;
|
||||||
|
|
||||||
|
public enum SiemensMemoryArea
|
||||||
|
{
|
||||||
|
Inputs = 0x81,
|
||||||
|
Outputs = 0x82,
|
||||||
|
Memory = 0x83,
|
||||||
|
DataBlock = 0x84,
|
||||||
|
Timer = 0x1D,
|
||||||
|
Counter = 0x1C
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SiemensAddress : IPlcAddress
|
||||||
|
{
|
||||||
|
public SiemensMemoryArea Area { get; set; }
|
||||||
|
public int DbNumber { get; set; } // Only for DataBlock
|
||||||
|
public int ByteOffset { get; set; }
|
||||||
|
public int BitOffset { get; set; }
|
||||||
|
|
||||||
|
// Helper to sort/group
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"{Area}:{DbNumber}:{ByteOffset}.{BitOffset}";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Plc.Options;
|
||||||
|
|
||||||
|
namespace Plc.Siemens.Options;
|
||||||
|
|
||||||
|
public class SiemensPlcMonitorOptionsConfigurator : IPostConfigureOptions<PlcMonitorOptions>
|
||||||
|
{
|
||||||
|
private readonly IOptionsMonitor<SiemensPlcOptions> _siemensOptions;
|
||||||
|
|
||||||
|
public SiemensPlcMonitorOptionsConfigurator(IOptionsMonitor<SiemensPlcOptions> siemensOptions)
|
||||||
|
{
|
||||||
|
_siemensOptions = siemensOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void PostConfigure(string? name, PlcMonitorOptions options)
|
||||||
|
{
|
||||||
|
// 从 SiemensPlcOptions 同步 MonitorInterval 到 PlcMonitorOptions
|
||||||
|
options.RefreshRate = TimeSpan.FromMilliseconds(_siemensOptions.CurrentValue.MonitorInterval);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
using S7.Net;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Net;
|
||||||
|
|
||||||
|
namespace Plc.Siemens.Options;
|
||||||
|
|
||||||
|
public class SiemensPlcOptions : IValidatableObject
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public string IpAddress { get; set; } = "127.0.0.1";
|
||||||
|
|
||||||
|
[Range(1, 65535)]
|
||||||
|
public int Port { get; set; } = 102;
|
||||||
|
|
||||||
|
[Range(0, 255)]
|
||||||
|
public short Rack { get; set; } = 0;
|
||||||
|
|
||||||
|
[Range(0, 255)]
|
||||||
|
public short Slot { get; set; } = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// S7-1200/1500 通常支持 960,S7-300 通常支持 240。
|
||||||
|
/// 默认保守值设为 240。
|
||||||
|
/// </summary>
|
||||||
|
[Range(128, 960)]
|
||||||
|
public int PduSize { get; set; } = 240;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// CPU 类型 (例如: S71200, S71500, S7300, S7400, S7200, S7200Smart).
|
||||||
|
/// 默认为 S71200。
|
||||||
|
/// </summary>
|
||||||
|
public CpuType CpuType { get; set; } = CpuType.S71200;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 优化器允许的最大间隔字节数。
|
||||||
|
/// 两个标签之间如果间隔小于此值,会合并为一次读取。
|
||||||
|
/// 默认为 20。
|
||||||
|
/// </summary>
|
||||||
|
[Range(0, 1000)]
|
||||||
|
public int MaxGapBytes { get; set; } = 20;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 监控循环刷新率 (毫秒)。
|
||||||
|
/// 默认为 200ms。
|
||||||
|
/// </summary>
|
||||||
|
[Range(10, 60000)]
|
||||||
|
public int MonitorInterval { get; set; } = 200;
|
||||||
|
|
||||||
|
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||||
|
{
|
||||||
|
// 验证IP地址格式
|
||||||
|
if (!IPAddress.TryParse(IpAddress, out _))
|
||||||
|
{
|
||||||
|
yield return new ValidationResult(
|
||||||
|
$"Invalid IP address format: {IpAddress}",
|
||||||
|
new[] { nameof(IpAddress) });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证PDU大小合理性
|
||||||
|
if (PduSize < 128 || PduSize > 960)
|
||||||
|
{
|
||||||
|
yield return new ValidationResult(
|
||||||
|
$"PDU size must be between 128 and 960, got: {PduSize}",
|
||||||
|
new[] { nameof(PduSize) });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证MaxGapBytes的合理性
|
||||||
|
if (MaxGapBytes < 0 || MaxGapBytes > PduSize)
|
||||||
|
{
|
||||||
|
yield return new ValidationResult(
|
||||||
|
$"MaxGapBytes ({MaxGapBytes}) must be between 0 and PduSize ({PduSize})",
|
||||||
|
new[] { nameof(MaxGapBytes) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loaded 100 of 331 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user