1. MongoDB 在 ASP .NET Core 中的集成
ASP .NET Core 应用程序与 MongoDB 的集成主要通过官方的 C# 驱动程序实现。
1.1 安装 MongoDB C# 驱动程序
首先,你需要通过 NuGet 包管理器安装官方驱动:
dotnet add package MongoDB.Driver
1.2 配置连接字符串
在 appsettings.json 文件中配置 MongoDB 的连接字符串和数据库名称:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"MongoDB": {
"ConnectionString": "mongodb://localhost:27017",
"DatabaseName": "MyApplicationDb"
}
}
1.3 注册 MongoDB 客户端(依赖注入)
在 Program.cs(或 Startup.cs)中,使用依赖注入(DI)注册 IMongoClient 和 IMongoDatabase,以便在整个应用程序中轻松访问:
using MongoDB.Driver;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
// ... other usings
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
// Configure MongoDB client
builder.Services.AddSingleton<IMongoClient>(sp =>
{
var configuration = sp.GetRequiredService<IConfiguration>();
var connectionString = configuration.GetValue<string>("MongoDB:ConnectionString");
return new MongoClient(connectionString);
});
// Configure MongoDB database
builder.Services.AddSingleton<IMongoDatabase>(sp =>
{
var client = sp.GetRequiredService<IMongoClient>();
var configuration = sp.GetRequiredService<IConfiguration>();
var databaseName = configuration.GetValue<string>("MongoDB:DatabaseName");
return client.GetDatabase(databaseName);
});
var app = builder.Build();
// ... other app configuration
1.4 定义 C# 对象模型(映射到 MongoDB 文档)
C# 对象将直接映射到 MongoDB 的 BSON 文档。你需要使用 MongoDB.Bson.Serialization.Attributes 命名空间下的属性来控制序列化行为。
关键点:
_id字段: MongoDB 文档的唯一标识符是_id,它通常是一个ObjectId类型。在 C# 类中,你可以用Id属性表示它,并使用[BsonId]和[BsonRepresentation(BsonType.ObjectId)]属性进行映射。- 其他字段: 其他属性会自动映射,除非你使用
[BsonElement("your_bson_field_name")]指定不同的字段名。
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using System.Collections.Generic;
// 领域层 (Domain Layer) - 实体定义
// 虽然BsonId是基础设施层的概念,但为了方便,通常直接加在实体上。
// 严格DDD可以考虑在基础设施层做映射,或使用Guid作为领域Id,再在基础设施层转换为ObjectId。
public class User
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; } // MongoDB的_id通常是ObjectId,但在C#中常映射为string
public string Username { get; set; }
public string Email { get; set; }
public string PasswordHash { get; set; }
public List<string> RoleIds { get; set; } = new List<string>(); // 存储角色ID,引用Role实体
// 领域方法
public void AssignRole(string roleId)
{
if (!RoleIds.Contains(roleId))
{
RoleIds.Add(roleId);
}
}
public void RemoveRole(string roleId)
{
RoleIds.Remove(roleId);
}
}
public class Role
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
}
2. DDD 分层设计中的对象增删改查
DDD 核心在于将业务逻辑与基础设施代码分离。我们将 User 和 Role 对象作为领域实体(或聚合根),并通过仓库(Repository)模式进行持久化操作。
2.1 DDD 核心分层回顾
-
表现层(Presentation Layer):
- 负责处理用户请求,展示信息。
- 在 ASP .NET Core 中,这通常是 API 控制器(Controllers)。
- 不包含业务逻辑,只协调应用服务。
- 使用 DTO(Data Transfer Object)与客户端交互。
-
应用层(Application Layer):
- 协调领域层对象执行业务操作。
- 定义和实现业务用例(Use Cases)或工作流。
- 不包含领域逻辑,但可以进行事务管理、安全检查等。
- 使用领域服务和仓库接口。
-
领域层(Domain Layer):
- 核心业务逻辑所在。
- 包含实体(Entities)、值对象(Value Objects)、聚合(Aggregates)、领域服务(Domain Services)、仓库接口(Repository Interfaces)。
- 与数据库技术无关。
-
基础设施层(Infrastructure Layer):
- 实现领域层定义的仓库接口。
- 负责与外部系统(如数据库、文件系统、消息队列)交互。
- 包含 MongoDB 相关的具体实现代码。
2.2 实现对象(User 和 Role)的增删改查
我们将以 User 和 Role 为例,逐步实现其在 DDD 架构中的 CRUD 操作。
2.2.1 领域层 (Domain Layer)
目标: 定义实体和持久化契约(接口)。
-
实体 (Entities) -
User.cs和Role.cs(如上所示)- 它们代表了业务概念,并封装了相关的行为和状态。
- 应尽量保持与持久化机制无关,但为了
_id的方便映射,我们直接在实体上添加了[BsonId]等属性。
-
仓库接口 (Repository Interfaces)
- 定义了对聚合根进行持久化操作的契约。
- 重要: 仓库应该面向聚合根,而不是单个实体。如果
User是一个聚合根,那么IUserRepository应该提供对User聚合的完整生命周期管理。
// Domain/Repositories/IUserRepository.cs public interface IUserRepository { Task AddAsync(User user); Task<User> GetByIdAsync(string id); Task<User> GetByUsernameAsync(string username); // 添加特定查询 Task UpdateAsync(User user); Task DeleteAsync(string id); Task<IEnumerable<User>> GetAllAsync(); } // Domain/Repositories/IRoleRepository.cs public interface IRoleRepository { Task AddAsync(Role role); Task<Role> GetByIdAsync(string id); Task<Role> GetByNameAsync(string name); // 添加特定查询 Task UpdateAsync(Role role); Task DeleteAsync(string id); Task<IEnumerable<Role>> GetAllAsync(); }
2.2.2 基础设施层 (Infrastructure Layer)
目标: 实现领域层定义的仓库接口,使用 MongoDB C# 驱动进行实际的数据操作。
-
MongoDB 上下文 (可选但推荐)
- 虽然 MongoDB 不像关系型数据库有严格的上下文概念,但为了封装
IMongoCollection<T>的获取逻辑,可以创建一个简单的IMongoContext或直接在每个 Repository 中注入IMongoDatabase。 - 这里我们直接在 Repository 中注入
IMongoDatabase以保持简洁。
- 虽然 MongoDB 不像关系型数据库有严格的上下文概念,但为了封装
-
仓库实现 (Repository Implementations)
using MongoDB.Driver; using System.Collections.Generic; using System.Threading.Tasks; using Domain.Repositories; // 引用领域层的接口 using Domain.Entities; // 引用领域层的实体 // Infrastructure/Repositories/UserRepository.cs public class UserRepository : IUserRepository { private readonly IMongoCollection<User> _users; public UserRepository(IMongoDatabase database) { _users = database.GetCollection<User>("users"); // "users" 是集合名称 } public async Task AddAsync(User user) { await _users.InsertOneAsync(user); } public async Task<User> GetByIdAsync(string id) { var filter = Builders<User>.Filter.Eq(u => u.Id, id); return await _users.Find(filter).FirstOrDefaultAsync(); } public async Task<User> GetByUsernameAsync(string username) { var filter = Builders<User>.Filter.Eq(u => u.Username, username); return await _users.Find(filter).FirstOrDefaultAsync(); } public async Task UpdateAsync(User user) { var filter = Builders<User>.Filter.Eq(u => u.Id, user.Id); // ReplaceOneAsync 会替换整个文档 await _users.ReplaceOneAsync(filter, user); // 如果只需要部分更新,可以使用 UpdateOneAsync 和 UpdateDefinitionBuilder /* var update = Builders<User>.Update .Set(u => u.Email, user.Email) .Set(u => u.PasswordHash, user.PasswordHash); await _users.UpdateOneAsync(filter, update); */ } public async Task DeleteAsync(string id) { var filter = Builders<User>.Filter.Eq(u => u.Id, id); await _users.DeleteOneAsync(filter); } public async Task<IEnumerable<User>> GetAllAsync() { return await _users.Find(_ => true).ToListAsync(); } } // Infrastructure/Repositories/RoleRepository.cs (类似UserRepository) public class RoleRepository : IRoleRepository { private readonly IMongoCollection<Role> _roles; public RoleRepository(IMongoDatabase database) { _roles = database.GetCollection<Role>("roles"); // "roles" 是集合名称 } public async Task AddAsync(Role role) { await _roles.InsertOneAsync(role); } public async Task<Role> GetByIdAsync(string id) { var filter = Builders<Role>.Filter.Eq(r => r.Id, id); return await _roles.Find(filter).FirstOrDefaultAsync(); } public async Task<Role> GetByNameAsync(string name) { var filter = Builders<Role>.Filter.Eq(r => r.Name, name); return await _roles.Find(filter).FirstOrDefaultAsync(); } public async Task UpdateAsync(Role role) { var filter = Builders<Role>.Filter.Eq(r => r.Id, role.Id); await _roles.ReplaceOneAsync(filter, role); } public async Task DeleteAsync(string id) { var filter = Builders<Role>.Filter.Eq(r => r.Id, id); await _roles.DeleteOneAsync(filter); } public async Task<IEnumerable<Role>> GetAllAsync() { return await _roles.Find(_ => true).ToListAsync(); } } -
注册仓库实现 (Dependency Injection)
在
Program.cs(或Startup.cs)中注册仓库实现:// ... in Program.cs builder.Services.AddSingleton<IUserRepository, UserRepository>(); builder.Services.AddSingleton<IRoleRepository, RoleRepository>(); // ...
2.2.3 应用层 (Application Layer)
目标: 协调领域层和基础设施层,实现具体的业务用例。
-
数据传输对象 (DTOs)
- 用于在应用层和表现层之间传递数据,避免直接暴露领域实体。
// Application/DTOs/UserDTO.cs public class UserDto { public string Id { get; set; } public string Username { get; set; } public string Email { get; set; } public List<string> RoleIds { get; set; } = new List<string>(); } // Application/DTOs/CreateUserRequest.cs public class CreateUserRequest { public string Username { get; set; } public string Email { get; set; } public string Password { get; set; } // 明文密码,在服务层进行哈希 } // Application/DTOs/UpdateUserRequest.cs public class UpdateUserRequest { public string Email { get; set; } // ... 其他可更新字段 } // Application/DTOs/RoleDTO.cs (类似 UserDTO) public class RoleDto { public string Id { get; set; } public string Name { get; set; } public string Description { get; set; } } -
应用服务 (Application Services)
- 封装了特定的业务用例逻辑。
- 通过构造函数注入领域层的仓库接口。
- 处理 DTO 到领域实体的映射,并调用领域实体的方法。
- 这里可以进行密码哈希、数据验证等。
using Domain.Repositories; using Domain.Entities; using Application.DTOs; using System.Collections.Generic; using System.Threading.Tasks; using System.Linq; using System.Security.Cryptography; // 用于密码哈希 using System.Text; // Application/Services/UserService.cs public class UserService { private readonly IUserRepository _userRepository; private readonly IRoleRepository _roleRepository; // 用于验证角色是否存在 public UserService(IUserRepository userRepository, IRoleRepository roleRepository) { _userRepository = userRepository; _roleRepository = roleRepository; } // 创建用户 public async Task<UserDto> CreateUserAsync(CreateUserRequest request) { // 业务逻辑:检查用户名或邮箱是否已存在 if (await _userRepository.GetByUsernameAsync(request.Username) != null) { throw new ArgumentException("Username already exists."); } // 密码哈希 var passwordHash = HashPassword(request.Password); var user = new User { Username = request.Username, Email = request.Email, PasswordHash = passwordHash, RoleIds = new List<string>() // 初始不分配角色 }; await _userRepository.AddAsync(user); return ToUserDto(user); } // 获取单个用户 public async Task<UserDto> GetUserByIdAsync(string id) { var user = await _userRepository.GetByIdAsync(id); return user == null ? null : ToUserDto(user); } // 获取所有用户 public async Task<IEnumerable<UserDto>> GetAllUsersAsync() { var users = await _userRepository.GetAllAsync(); return users.Select(ToUserDto); } // 更新用户 public async Task UpdateUserAsync(string id, UpdateUserRequest request) { var user = await _userRepository.GetByIdAsync(id); if (user == null) { throw new KeyNotFoundException($"User with ID {id} not found."); } user.Email = request.Email; // 可以根据需要更新其他字段,调用领域实体的方法 // user.UpdateProfile(request.Email, ...); await _userRepository.UpdateAsync(user); } // 删除用户 public async Task DeleteUserAsync(string id) { var user = await _userRepository.GetByIdAsync(id); if (user == null) { throw new KeyNotFoundException($"User with ID {id} not found."); } await _userRepository.DeleteAsync(id); } // 分配角色给用户 public async Task AssignRoleToUserAsync(string userId, string roleId) { var user = await _userRepository.GetByIdAsync(userId); if (user == null) throw new KeyNotFoundException($"User with ID {userId} not found."); var role = await _roleRepository.GetByIdAsync(roleId); if (role == null) throw new KeyNotFoundException($"Role with ID {roleId} not found."); user.AssignRole(roleId); // 调用领域实体的方法 await _userRepository.UpdateAsync(user); } // 辅助方法:将领域实体转换为 DTO private UserDto ToUserDto(User user) { return new UserDto { Id = user.Id, Username = user.Username, Email = user.Email, RoleIds = user.RoleIds }; } // 密码哈希方法 (简化版,生产环境应使用更安全的方案如 Argon2) private string HashPassword(string password) { using (var sha256 = SHA256.Create()) { var hashedBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(password)); return BitConverter.ToString(hashedBytes).Replace("-", "").ToLower(); } } } // Application/Services/RoleService.cs (类似 UserService) public class RoleService { private readonly IRoleRepository _roleRepository; public RoleService(IRoleRepository roleRepository) { _roleRepository = roleRepository; } public async Task<RoleDto> CreateRoleAsync(string name, string description) { if (await _roleRepository.GetByNameAsync(name) != null) { throw new ArgumentException("Role with this name already exists."); } var role = new Role { Name = name, Description = description }; await _roleRepository.AddAsync(role); return ToRoleDto(role); } public async Task<RoleDto> GetRoleByIdAsync(string id) { var role = await _roleRepository.GetByIdAsync(id); return role == null ? null : ToRoleDto(role); } public async Task<IEnumerable<RoleDto>> GetAllRolesAsync() { var roles = await _roleRepository.GetAllAsync(); return roles.Select(ToRoleDto); } public async Task UpdateRoleAsync(string id, string name, string description) { var role = await _roleRepository.GetByIdAsync(id); if (role == null) throw new KeyNotFoundException($"Role with ID {id} not found."); role.Name = name; role.Description = description; await _roleRepository.UpdateAsync(role); } public async Task DeleteRoleAsync(string id) { var role = await _roleRepository.GetByIdAsync(id); if (role == null) throw new KeyNotFoundException($"Role with ID {id} not found."); await _roleRepository.DeleteAsync(id); } private RoleDto ToRoleDto(Role role) { return new RoleDto { Id = role.Id, Name = role.Name, Description = role.Description }; } } -
注册应用服务 (Dependency Injection)
// ... in Program.cs builder.Services.AddScoped<UserService>(); // 应用服务通常是 Scoped builder.Services.AddScoped<RoleService>(); // ...
2.2.4 表现层 (Presentation Layer)
目标: 提供 RESTful API 接口,接收客户端请求,调用应用服务,并返回响应。
-
API 控制器 (Controllers)
- 通过构造函数注入应用服务。
- 处理 HTTP 请求的路由、参数绑定、验证等。
- 将 DTO 作为请求和响应的主体。
using Microsoft.AspNetCore.Mvc; using Application.Services; using Application.DTOs; using System.Collections.Generic; using System.Threading.Tasks; // Presentation/Controllers/UsersController.cs [ApiController] [Route("api/[controller]")] public class UsersController : ControllerBase { private readonly UserService _userService; public UsersController(UserService userService) { _userService = userService; } // GET: api/users [HttpGet] public async Task<ActionResult<IEnumerable<UserDto>>> Get() { return Ok(await _userService.GetAllUsersAsync()); } // GET: api/users/{id} [HttpGet("{id}")] public async Task<ActionResult<UserDto>> Get(string id) { var user = await _userService.GetUserByIdAsync(id); if (user == null) { return NotFound(); } return Ok(user); } // POST: api/users [HttpPost] public async Task<ActionResult<UserDto>> Post([FromBody] CreateUserRequest request) { try { var userDto = await _userService.CreateUserAsync(request); // Return 201 Created status with the new user's location return CreatedAtAction(nameof(Get), new { id = userDto.Id }, userDto); } catch (ArgumentException ex) { return BadRequest(ex.Message); } } // PUT: api/users/{id} [HttpPut("{id}")] public async Task<IActionResult> Put(string id, [FromBody] UpdateUserRequest request) { try { await _userService.UpdateUserAsync(id, request); return NoContent(); // 204 No Content } catch (KeyNotFoundException) { return NotFound(); } } // DELETE: api/users/{id} [HttpDelete("{id}")] public async Task<IActionResult> Delete(string id) { try { await _userService.DeleteUserAsync(id); return NoContent(); // 204 No Content } catch (KeyNotFoundException) { return NotFound(); } } // POST: api/users/{userId}/roles/{roleId} (示例:分配角色) [HttpPost("{userId}/roles/{roleId}")] public async Task<IActionResult> AssignRole(string userId, string roleId) { try { await _userService.AssignRoleToUserAsync(userId, roleId); return NoContent(); } catch (KeyNotFoundException ex) { return NotFound(ex.Message); } catch (ArgumentException ex) { return BadRequest(ex.Message); } } } // Presentation/Controllers/RolesController.cs (类似 UsersController) // ...
2.3 总结 DDD 与 MongoDB 的结合
- 领域层(Domain Layer): 保持纯净,定义业务规则和行为,不关心数据存储细节。实体使用普通的 C# 类,通过接口与外部解耦。
- 基础设施层(Infrastructure Layer): 实现了领域层的仓库接口,包含所有 MongoDB 特定的代码,如
IMongoCollection<T>的使用、FilterDefinitionBuilder、UpdateDefinitionBuilder等。这一层是 MongoDB 适配器。 - 应用层(Application Layer): 协调领域实体和仓库,执行应用程序的用例。它处理 DTO 到领域实体的转换,并封装了业务流程。
- 表现层(Presentation Layer): 作为 API 端点,负责 HTTP 请求和响应的序列化/反序列化,并调用应用服务。
这种分层设计确保了:
- 关注点分离: 每个层只负责特定的任务,提高了代码的可读性和可维护性。
- 可测试性: 领域层和应用层可以独立于数据库进行单元测试。基础设施层也可以进行集成测试。
- 灵活性: 如果未来需要更换数据库(例如从 MongoDB 切换到另一个 NoSQL 数据库),只需修改基础设施层的仓库实现,而不会影响领域层和应用层。
- 领域模型驱动: 业务逻辑是核心,数据库只是实现持久化的一种工具。
通过这种方式,ASP .NET Core 应用程序可以高效、优雅地利用 MongoDB 的强大功能,同时遵循 DDD 的最佳实践来构建健壮、可扩展的系统。
扩展思考
在 ASP .NET Core 中使用 MongoDB 时,尽管 MongoDB 本身是“无模式”(schemaless)的,但 C# 实体是强类型的,这确实引入了在数据演进(Schema Evolution)方面需要管理的复杂性。当我们为 C# 实体添加新字段时,我们需要思考如何让 MongoDB 集合中的现有文档也能适应这些变化。
1. MongoDB 的“无模式”特性与 C# 强类型的冲突点
首先,我们明确一下:
- MongoDB 的无模式: 意味着你可以在同一个集合中存储结构不同的文档。当你为 C# 实体添加一个新字段时,MongoDB 集合中已存在的文档并不会因此而“报错”,它们只是缺少这个新字段。MongoDB 驱动程序在反序列化时,会忽略 C# 实体中存在但在 BSON 文档中不存在的字段(通常会将其设置为
null或默认值)。 - C# 的强类型: 意味着你的应用程序代码需要一个明确的类型定义来操作数据。如果你在 C# 实体中添加了一个新属性,但对应的数据库文档中没有这个字段,那么当你读取旧文档时,这个新属性将是其类型的默认值(例如,引用类型为
null,数值类型为0,bool类型为false)。如果你的代码期望这个字段总是存在且有值,就可能导致NullReferenceException或逻辑错误。
因此,管理数据演进的关键在于协调 C# 代码的类型定义与 MongoDB 实际存储文档之间的结构差异。
2. 管理数据演进的策略(添加新字段为例)
当 User 或 Role 实体需要添加新字段时,通常遵循以下阶段:
2.1 阶段一:更新 C# 实体定义
这是第一步,在 C# 代码中直接修改你的实体类。
示例:为 User 实体添加 LastLoginDate 字段和 IsActive 字段。
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using System.Collections.Generic;
using System; // For DateTime
public class User
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; }
public string Username { get; set; }
public string Email { get; set; }
public string PasswordHash { get; set; }
public List<string> RoleIds { get; set; } = new List<string>();
// 新增字段
public DateTime? LastLoginDate { get; set; } // 允许为 null,表示尚未登录过
public bool IsActive { get; set; } = true; // 提供一个默认值
}
关键考虑点:
- Nullability (可空性): 如果新字段对现有文档来说不是必需的,并且可以接受在旧文档中缺失(即其值为
null),请将其定义为可空类型(DateTime?,string?)。这使得读取旧文档时不会抛出错误。 - 默认值: 如果新字段在业务逻辑上应该有一个默认值(例如
IsActive默认为true),在 C# 属性中设置默认值 (= true;)。当 MongoDB 驱动反序列化一个缺少此字段的文档时,C# 对象的新属性将拥有这个默认值。
在此阶段,数据库中的现有文档尚未改变。 它们仍然没有 LastLoginDate 和 IsActive 字段。但是,你的 ASP .NET Core 应用程序在读取这些文档时,C# User 对象的 LastLoginDate 将是 null,IsActive 将是 true。
2.2 阶段二:数据迁移(可选但通常推荐)
虽然 C# 驱动可以处理缺少字段的情况,但为了数据的一致性、查询的方便性以及避免潜在的 null 检查,通常建议运行一次数据迁移脚本,为所有现有文档添加这些新字段并赋予一个默认值。
执行数据迁移的两种方式:
2.2.2.1 使用 MongoDB Shell 或 Compass
这是最直接的方式,特别是对于简单的批量更新。
示例:为 users 集合中的所有文档添加 LastLoginDate 和 IsActive。
// 为所有没有 LastLoginDate 字段的文档添加该字段,并设置默认值为 Unix 纪元(1970-01-01)
db.users.updateMany(
{ LastLoginDate: { $exists: false } },
{ $set: { LastLoginDate: ISODate("1970-01-01T00:00:00Z") } }
)
// 为所有没有 IsActive 字段的文档添加该字段,并设置默认值为 true
db.users.updateMany(
{ IsActive: { $exists: false } },
{ $set: { IsActive: true } }
)
{ $exists: false }:这是一个强大的查询操作符,用于查找缺少特定字段的文档。$set:用于添加或更新字段。
2.2.2.2 通过 C# 代码进行数据迁移
你也可以编写一个一次性的 C# 脚本或一个专门的“数据迁移服务”来执行这些更新。这在自动化部署和更复杂的迁移场景中非常有用。
using MongoDB.Driver;
using System.Threading.Tasks;
using System;
// ... using Domain.Entities;
public class DataMigrationService
{
private readonly IMongoCollection<User> _usersCollection;
public DataMigrationService(IMongoDatabase database)
{
_usersCollection = database.GetCollection<User>("users");
}
public async Task MigrateUsersSchemaAsync()
{
Console.WriteLine("Starting User schema migration...");
// 迁移 LastLoginDate 字段
var lastLoginDateFilter = Builders<User>.Filter.Exists(u => u.LastLoginDate, false);
var lastLoginDateUpdate = Builders<User>.Update.Set(u => u.LastLoginDate, DateTime.MinValue); // 设置一个合理的默认值
var lastLoginDateResult = await _usersCollection.UpdateManyAsync(lastLoginDateFilter, lastLoginDateUpdate);
Console.WriteLine($"Migrated LastLoginDate for {lastLoginDateResult.ModifiedCount} users.");
// 迁移 IsActive 字段
var isActiveFilter = Builders<User>.Filter.Exists(u => u.IsActive, false);
var isActiveUpdate = Builders<User>.Update.Set(u => u.IsActive, true); // 设置默认值
var isActiveResult = await _usersCollection.UpdateManyAsync(isActiveFilter, isActiveUpdate);
Console.WriteLine($"Migrated IsActive for {isActiveResult.ModifiedCount} users.");
Console.WriteLine("User schema migration completed.");
}
}
- 执行时机: 这个服务可以在应用程序启动时(例如在
Program.cs中)执行一次,或者作为单独的命令行工具运行。 - 幂等性: 确保你的迁移脚本是幂等的(多次运行结果相同),例如,通过
{$exists: false}过滤,只更新那些确实缺少字段的文档。
2.3 阶段三:更新应用逻辑(使用新字段)
一旦 C# 实体和数据库中的数据都已更新,你的应用逻辑就可以安全地开始使用这些新字段了。
// 在 UserService 中,现在可以根据 IsActive 过滤用户
public async Task<IEnumerable<UserDto>> GetActiveUsersAsync()
{
var filter = Builders<User>.Filter.Eq(u => u.IsActive, true);
var users = await _userRepository.Find(filter); // 假设仓库有Find方法
return users.Select(ToUserDto);
}
// 在用户登录时更新 LastLoginDate
public async Task LoginUserAsync(string username, string password)
{
var user = await _userRepository.GetByUsernameAsync(username);
if (user == null || !VerifyPassword(password, user.PasswordHash))
{
throw new UnauthorizedAccessException("Invalid credentials.");
}
user.LastLoginDate = DateTime.UtcNow; // 更新领域实体
await _userRepository.UpdateAsync(user); // 持久化更新
// ... 其他登录逻辑
}
3. 更复杂的场景和最佳实践
3.1 字段重命名
如果需要重命名字段,情况会稍微复杂一点。你需要:
-
C# 实体: 使用
[BsonElement("oldFieldName")]属性保持向后兼容,或者直接重命名属性并在迁移时处理。 -
数据迁移: 使用
$rename操作符。db.users.updateMany( {}, // 作用于所有文档 { $rename: { "oldFieldName": "newFieldName" } } )通常,这是一个有风险的操作,因为在
$rename完成之前,应用程序代码需要同时识别新旧字段名。更好的做法可能是:- 阶段1: 添加新字段,同时保持旧字段。
- 阶段2: 编写代码,在读旧字段时写入新字段。
- 阶段3: 迁移数据,将旧字段数据复制到新字段,并更新所有读取旧字段的代码去读取新字段。
- 阶段4: 删除旧字段(在 C# 和 MongoDB 中)。
3.2 字段删除
-
C# 实体: 直接删除属性。
-
数据迁移(可选): 如果想节省空间,可以使用
$unset操作符删除数据库中的字段。db.users.updateMany( {}, { $unset: { "oldUnusedField": "" } } )
3.3 嵌套文档和数组的变化
处理嵌套文档或数组的结构变化同样遵循上述原则:
- 添加/删除字段: 直接修改 C# 类,并考虑数据迁移。
- 改变类型: 这可能导致反序列化错误。需要先将旧字段重命名(
$rename),然后添加新类型字段,再将数据转换过去。
3.4 版本控制数据模式
对于大型或演进频繁的系统,可以考虑在文档中引入一个 _schemaVersion 字段:
{
"_id": "...",
"Username": "...",
"Email": "...",
"_schemaVersion": 1, // 当前模式版本
"CreatedAt": "..."
}
你的应用层或数据访问层可以在读取文档时检查 _schemaVersion:
- 如果
_schemaVersion小于当前应用期望的版本,则在加载到内存中时对数据进行“升级”转换,或者触发一个后台任务来实际更新数据库中的文档。 - 这允许应用代码和数据库数据在一段时间内保持不同的模式版本,并在后台逐步进行数据迁移。
3.5 永远保持向后兼容性
在进行模式演进时,核心原则是尽量保持向后兼容性。这意味着在部署新代码之前,旧代码应该能够继续读取和写入数据,即使这些数据可能缺少新字段。通常的做法是:
- 部署数据库迁移脚本 (如果需要)。
- 部署支持新旧模式的代码 (新字段可空,或有默认值)。
- 逐步弃用旧字段/逻辑。
4. 总结
MongoDB 的无模式特性赋予了极大的灵活性,使得添加新字段通常比关系型数据库更容易。然而,在强类型的 ASP .NET Core 应用程序中,这种灵活性需要通过细致的 C# 实体设计(可空性、默认值)和必要的数据迁移步骤来管理。
核心流程:
- C# 实体更新: 为新字段添加属性,并考虑其可空性或默认值。
- 数据迁移(推荐): 运行一次性脚本,为现有文档添加新字段并赋予默认值,以确保数据一致性。
- 应用逻辑更新: 在新字段存在且一致后,安全地在业务逻辑中使用它们。
通过这种分阶段、谨慎的方法,可以有效地在 ASP .NET Core 和 DDD 架构中管理 MongoDB 的数据演进。