C# MongoDB - 带有通用 CRUD 的多态集合
在 Visual Studio 中使用 Mongo C# Driver 连接到 MongoLab 并创建通用的 CRUD 操作。
引言
本文将介绍 Visual Studio 2015 C# 中的一种高级架构,用于连接到托管在 MongoLab 上的 MongoDB,并创建多态且通用的 CRUD 操作,以便在多个类中重用。
我们将使用三层架构:接口/契约模型、数据库模型和视图模型上下文。
背景
在开始阅读本文之前,我假设您知道如何在 MongoLab 中设置免费节点数据库,知道如何连接到 MongoDB,知道如何编写 MongoDB CRUD 操作,以及了解 C# 中多态和泛型类型的使用。
所需类
首先,在进行数据库处理器之前,让我们创建接口、数据库和视图模型。
IMongoEntity<TId>
public interface IMongoEntity<TId>
{
TId Id { get; set; }
}
TId
是一个泛型类型。我们将使用 Mongo 库中的 ObjectId
类型。如果您愿意,也可以使用其他类型作为 Id。只需确保将您的 Id 属性表示为 Mongo 序列化属性的 ObjectId
类型,即 [BsonRepresentation(BsonType.ObjectId)]
。
IMongoCommon
public interface IMongoCommon : IMongoEntity<ObjectId>
{
string Name { get; set; }
bool IsActive { get; set; }
string Description { get; set; }
DateTime Created { get; set; }
DateTime Modified { get; set; }
}
IMongoCommon
将是所有集合文档继承的基接口。这些属性将是所有集合文档共有的属性。最重要的是,您将在本文后面编写的通用 CRUD 操作中看到很多 IMongoCommon
。
IAddress
public interface IAddress
{
string Street { get; set; }
string City { get; set; }
string State { get; set; }
string Zip { get; set; }
string Country { get; set; }
}
IAddress
不实现 IMongoCommon
,因为它将是一个嵌入式文档。
地址
[Serializable, JsonObject]
[BsonDiscriminator(Required = true)]
[BsonKnownTypes(typeof(Address))]
public class Address : IAddress
{
[BsonDefaultValue("")]
[BsonIgnoreIfDefault]
public string Street { get; set; }
[BsonDefaultValue("")]
[BsonIgnoreIfDefault]
public string City { get; set; }
[BsonDefaultValue("")]
[BsonIgnoreIfDefault]
public string State { get; set; }
[BsonDefaultValue("")]
[BsonIgnoreIfDefault]
public string Zip { get; set; }
[BsonDefaultValue("")]
[BsonIgnoreIfDefault]
public string Country { get; set; }
}
[BsonDiscriminator()]
和 [BsonKnownTypes()]
允许服务器知道如何序列化/反序列化此类类型的文档。您可以通过点击 这里 阅读更多相关信息。
我们将这些字符串默认设置为空字符串,因为当我们向网页传递 null 字符串时,它会以字符串的形式返回。最初,我使用了 [BsonIgnoreIfNull]
,但一旦字符串传递到网页,它就永远不会是 null,这会在服务器端使业务逻辑的考虑更加复杂。所以基本上,如果用户没有为字符串值指定任何内容,它就是一个空字符串,当我们将它存储到数据库时,序列化器会忽略它。
AddressContext (地址视图模型)
[Serializable]
public class AddressContext
{
public string Street { get; set; }
public string City { get; set; }
public string State { get; set; }
public string Zip { get; set; }
public string Country { get; set; }
}
IEmployee
public interface IEmployee : IMongoCommon
{
string FirstName { get; set; }
string LastName { get; set; }
IEnumerable<IAddress> Addresses { get; set; }
}
员工
[Serializable, JsonObject]
[BsonDiscriminator(Required = true)]
[BsonKnownTypes(typeof(Employee))]
public class Employee : IEmployee
{
private IEnumerable<IAddress> _addresses;
public Employee()
{
IsActive = true;
Created = DateTime.UtcNow;
Modified = DateTime.UtcNow;
}
[BsonId]
public ObjectId Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Name => FirstName + " " + LastName;
public bool IsActive { get; set; }
[BsonDefaultValue("")]
[BsonIgnoreIfDefault]
public string Description { get; set;
[BsonIgnoreIfNull]
public IEnumerable<IAddress> Addresses
{
get { return _addresses ?? (_addresses = new List<IAddress>(); }
set { _addresses = value; }
}
public DateTime Created { get; set; }
public DateTime Modified { get; set; }
private bool ShouldSerializeAddresses() => Addresses.Any();
}
我们在 Addresses
上设置了 [BsonIgnoreIfNull]
序列化属性,因为我们不希望将 Addresses
的 null 列表/数组存储到数据库中。ShouldSerializeAttribute
是 Mongo C# 驱动程序中的一个布尔方法,它允许服务器知道在属性不为空时是否应序列化该属性。在这种情况下,属性是 Addresses。
EmployeeContext (员工视图模型)
public class EmployeeContext
{
public EmployeeContext()
{
IsActive = true;
Addresses = new List<AddressContext>();
Created = DateTime.UtcNow;
Modified = DateTime.UtcNow;
}
public string Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Name => FirstName + " " + LastName;
public bool IsActive { get; set; }
public string Description { get; set; }
public IEnumerable<AddressContext> Addresses { get; set; }
public DateTime Created { get; set; }
public DateTime Modified { get; set; }
}
IOrganization
public interface IOrganization : IMongoCommon
{
string Uri { get; set; }
IEnumerable<IAddress> Addresses { get; set; }
}
Organization
[Serializable, JsonObject]
[BsonDiscriminator(Required = true)]
[BsonKnownTypes(typeof(Organization))]
public class Organization : IOrganization
{
private IEnumerable<IAddress> _addresses;
public Organization()
{
IsActive = true;
Created = DateTime.UtcNow;
Modified = DateTime.UtcNow;
}
[BsonId]
public ObjectId Id { get; set; }
public string Name { get; set; }
public bool IsActive { get; set; }
[BsonDefaultValue("")]
[BsonIgnoreIfDefault]
public string Uri { get; set; }
[BsonDefaultValue("")]
[BsonIgnoreIfDefault]
public string Description { get; set; }
[BsonIgnoreIfNull]
public IEnumerable<IAddress> Addresses
{
get { return _addresses ?? (_addresses = new List<IAddress>(); }
set { _addresses = value; }
}
public DateTime Created { get; set; }
public DateTime Modified { get; set; }
private bool ShouldSerializeAddresses() => Addresses.Any();
}
OrganizationContext (组织视图模型)
[Serializable]
public class OrganizationContext
{
public OrganizationContext()
{
IsActive = true;
Addresses = new List<AddressContext>();
Created = DateTime.UtcNow;
Modified = DateTime.UtcNow;
}
public string Id { get; set; }
public string Name { get; set; }
public bool IsActive { get; set; }
public string Uri { get; set; }
public string Description { get; set; }
public IEnumerable<AddressContext> Addresses { get; set; }
public DateTime Created { get; set; }
public DateTime Modified { get; set; }
}
Mongo 连接处理程序
接下来,让我们创建 Mongo 连接处理程序。我们将创建主连接的接口类,继承的数据库处理程序类将实现该接口。此类将接受一个泛型类型来处理 IMongoCollection
。
IMyDatabase
public interface IMyDatabase<T>
{
IMongoDatabase Database { get; }
IMongoCollection<T> Collection { get; }
}
IMongoCollection
接受一个泛型类型 T
,它是所有文档对象的接口类。为了方便您理解,当我们进入 CRUD 操作时,T
是任何实现 IMongoCommon
的类。
接下来,我们将创建 Mongo 连接处理程序的继承类。
MyDatabase
public class MyDatabase<T> : IMyDatabase<T> { public IMongoDatabase { get; } public IMongoCollection<T> Collection { get; } public MyDatabase(string collectionName) { var client = new MongoClient("mongodb://username:password@ds012345.mongolab.com:12345/demo"); Database = client.GetDatabase("demo"); Collection = Database.GetCollection<T>(collectionName); RegisterMapIfNeeded<Address>(); RegisterMapIfNeeded<Employee>(); RegisterMapIfNeeded<Organization>(); } // Check to see if map is registered before registering class map // This is for the sake of the polymorphic types that we are using so Mongo knows how to deserialize public void RegisterMapIfNeeded<TClass>() { if (!BsonClassMap.IsClassMapRegistered(typeof(TClass))) BsonClassMap.RegisterMapClass<TClass>(); } }
我们只注册数据库层类,因为这些是我们将会存储到 Mongo 数据库中的类类型。
全局 CRUD 逻辑
接下来是全局 CRUD 逻辑,我们将创建一个名为 ILogic
的类,该类接受泛型类型 T
。此类将是所有逻辑实现的接口类。
ILogic<T>
public interface ILogic<T>
{
Task<IEnumerable<T>> GetAllAsync();
Task<T> GetOneAsync(T context);
Task<T> GetOneAsync(string id);
Task<T> GetManyAsync(IEnumerable<T> contexts);
Task<T> GetManyAsync(IEnumerable<string> ids);
Task<T> SaveOneAsync(T Context);
Task<T> SaveManyAsync(IEnumerable<T> contexts);
Task<bool> RemoveOneAsync(T context);
Task<bool> RemoveOneAsync(string id);
Task<bool> RemoveManyAsync(IEnumerable<T> contexts);
Task<bool> RemoveManyAsync(IEnumerable<string> ids);
}
GlobalLogic<TCollection, TContext>
public class GlobalLogic<TCollection, TContext>
where TCollection : IDocumentCommon
where TContext : IDocumentCommon, new()
{
public async Task<IEnumerable<TCollection>> GetAllAsync(IMongoCollection<TCollection> collection)
{
return await collection.Find(f => true).ToListAsync();
}
public async Task<TCollection> GetOneAsync(IMongoCollection<TCollection> collection, TContext context)
{
return await collection.Find(new BsonDocument("_id", context.Id)).FirstOrDefaultAsync();
}
public async Task<TCollection> GetOneAsync(IMongoCollection<TCollection> collection, string id)
{
return await GetOneAsync(collection, new TContext { Id = new ObjectId(id) });
}
public async Task<IEnumerable<TCollection>> GetManyAsync(IMongoCollection<TCollection> collection,
IEnumerable<TContext> contexts)
{
var list = new List<TCollection>();
foreach (var context in contexts)
{
var doc = await GetOneAsync(collection, context);
if (doc == null) continue;
list.Add(doc);
}
return list;
}
public async Task<IEnumerable<TCollection>> GetManyAsync(IMongoCollection<TCollection> collection,
IEnumerable<string> ids)
{
var list new List<TCollection();
foreach (var id in ids)
{
var doc = await GetOneAsync(collection, id);
if (doc == null) continue;
list.Add(doc);
}
return list;
}
public async Task<bool> RemoveOneAsync(IMongoCollection<TCollection> collection, TContext context)
{
if (context == null || string.IsNullOrEmpty(context.Name)) return false;
await collection.UpdateOneAsync(
new BsonDocument("_id", context.Id),
new BsonDocument("$set", new BsonDocument { { nameof(IDocumentCommon.IsActive, false },
{ nameof(IDocument.Modified), DateTime.UtcNow } }));
return true;
}
public Task<bool> RemoveOneAsync(IMongoCollection<TCollection> collection, string id)
{
return await RemoveOneAsync(collection, new TContext { Id = new ObjectId(id) });
}
public async Task<bool> RemoveManyAsync(IMongoCollection<TCollection> collection,
IEnumerable<TContext> contexts)
{
foreach (var context in contexts)
await RemoveOneAsync(collection, context);
return true;
}
public async Task<bool> RemoveManyAsync(IMongoCollection<TCollection> collection,
IEnumerable<string> ids)
{
foreach (var id in ids)
await RemoveOneAsync(collection, id);
return true;
}
}
这个 GlobalLogic
类接受两个泛型类型,集合/接口的类型和视图模型的类型。我将简要解释每个方法及其功能。
1. GetAllAsync(TCollection collection)
此方法从集合中获取所有文档。您可以指定 f => true
或 new BsonDocument()
作为过滤器。
2. GetOneAsync(TCollection collection, TContext context)
此方法通过 Id 获取文档。我们传入了一个 new BsonDocument()
,因为使用多态类型,我们将无法使用 LINQ lambda 访问 Id 属性。这将与所有 CRUD 操作保持一致。
3. GetOneAsync(TCollection collection, string id)
此方法是方法 #2 的传递方法,用于消除业务逻辑中的冗余。
4. GetManyAsync(TCollection collection, IEnumerable<TContext> contexts)
这也是方法 #2 的传递方法,只不过这次我们将其作为 foreach
循环处理并添加到列表中返回。如果底层上下文为 null,则不会将其添加到返回列表中。
5. GetManyAsync(TCollection collection, IEnumerable<string> ids)
这与方法 #4 相同,只不过它的传递方法是方法 #3。
6. RemoveOneAsync(TCollection collection, TContext)
我们在系统中实现了软删除。指示此状态的标志是每个文档中的 IsActive
属性。我们将文档标记为不活动,然后更新修改日期。
7. RemoveOneAsync(TCollection collection, string id)
这是方法 #6 的传递方法,如果我们想以字符串而不是上下文模型的形式传递参数。
8. RemoveManyAsync(TCollection collection, IEnumerable<TContext> contexts
9. RemoveManyAsync(TCollection collection, IEnumerable<string> ids
这两个方法与方法 #4 和 #5 基本相同,只不过它们引用了它们底层的方法 #6 和 #7。
员工逻辑
对于下一个类,我们将创建 EmployeeLogic
类。
EmployeeLogic
public class EmployeeLogic : ILogic<EmployeeContext>
{
// Get the database connection instance
protected readonly MyDatabase<IEmployee> Employees;
// Get the GlobalLogic class so we can call them in our methods
protected readonly GlobalLogic<IEmployee, Employee> GlobalLogic = new GlobalLogic<IEmployee, Employee>();
// Get the database connection instance in the constructor
public EmployeeLogic()
{
Employees = new MyDatabase<IEmployee>("employee");
}
public async Task<IEnumerable<EmployeeContext>> GetAllAsync()
{
var employee = await GlobalLogic.GetAllAsync(Employees.Collection);
return employee.Select(e => new EmployeeContext
{
Id = employee.Id.ToString(),
FirstName = employee.FirstName,
LastName = employee.LastName,
Name = employee.Name,
IsActive = employee.IsActive,
Description = employee.Description,
Addresses = employee.Addresses?.Select(a => new AddressContext
{
Street = a.Street,
City = a.City,
State a.State,
Zip = a.Zip,
Country = a.Country
},
Created = employee.Created,
Modified = employee.Modified
};
}
public async Task<EmployeeContext> GetOneAsync(EmployeeContext context)
{
return new NotImplementedException();
}
...
public async Task<EmployeeContext> SaveOneAsync(EmployeeContext context)
{
if (string.IsNullOrEmpty(context.Id))
{
var employee = context.AsNewEmployee();.
await Employees.InsertOneAsync(employee);
context.Id = employee.Id.ToString();
return context;
}
var update = context.ToEmployee();
await Employees.ReplaceOneAsync(new BsonDocument("_id", new ObjectId(context.Id)), update);
return context;
}
}
这个 EmployeeLogic
类也称为员工的服务类。基本上,它继承了 ILogic<T>
类,其中 T
是 OrganizationContext
。然后,我们通过将 IEmployee
接口作为多态 IMongoCollection
类型传递来获取数据库实例。如果您还记得 MyDatabase
类,Employees = new MyDatabase<IEmployee>("employee")
会返回一个名为“employee”的 IEmployee
类型集合。然后,我们声明 GlobalLogic<IEmployee, Employee>
类来获取 EmployeeLogic 类的 Mongo 逻辑。当然,当 IEmployee
实现 ILogic<EmployeeContext>
时,它将继承 ILogic 的所有成员。接着看 GetAllAsync()
,当类型从 GlobalLogic
返回时,它是 IEnumerable
<IEmployee>
,但该方法返回 IEnumerable<EmployeeContext>
。这时就需要我们在 return 语句中进行转换。
要实现 OrganizationLogic
,您基本上会做同样的事情,只不过您会传入 IOrganization
、Organization
和 OrganizationContext
类型。
提示:为了使转换更容易,我建议您编写自己的扩展类来转换这三个层架构类型。这会让您更容易,因为这些转换大多数都是可重用的。您只需调用 employee.ToEmployeeContext()
。例如,请参考 SaveOneAsync()
方法。
public static class EmployeeExtensions
{
public static Employee AsNewEmployee(this EmployeeContext context)
{
return new Employee
{
// translation
}
}
public static EmployeeContext ToEmployeeContext(this IEmployee employee)
{
return new EmployeeContext
{
// translation
};
}
public static IEnumerable<EmployeeContext> ToEmployeeContextList(this IEnumerable<IEmployee> contexts)
{
return contexts.Select(ToEmployeeContext);
}
public static Employee ToEmployee(this EmployeeContext context)
{
return new Employee
{
// translation
}
}
}
Using the Code
要将代码用于其他项目,例如 MVC Controller、Windows Forms 应用程序或 Console 应用程序,只需调用 EmployeeLogic
和/或 OrganizationLogic
的新实例。在 MVC Controller 中使用代码会有些不同。您需要在启动时、在 Controller 中以及在 Controller 的构造函数中注册该服务。声明逻辑/服务的实例后,您只需要 _logic.GetAllAsync();
。要查看其在控制台应用程序中的用法,请参考下面的代码。
internal class Program
{
protected readonly EmployeeLogic Employees = new EmployeeLogic();
private static void Main(string[] args)
{
var m = MainAsync();
m.Wait();
Console.ReadLine();
}
private static async Task MainAsync()
{
var employees = await Employees.GetAllAsync();
Console.WriteLine(employees.ToJson(new JsonWriterSettings { Indent = true });
}
}
JSON 格式的数据
{
"_id": ObjectId("...");
"_t": "Employee",
"FirstName": "Your",
"LastName": "Name",
"Name": "Your Name",
"IsActive": true,
"Addresses" [
{
"_t": "Address",
"Street": "123 Street Street",
"City": "Your City",
"State": "Your State",
"Zip: "12345",
"Country": "United States"
}
],
"Created": ISO(...),
"Modified": ISO(...),
}
"_t"
是文档的类型,也称为数据库层类型。这就是为什么我们在数据库连接处理程序中进行 Bson 类映射。这会在客户端施加严格的层,因此如果任何数据被篡改,它将拒绝结果被篡改的数据,只接受有效数据。
关注点
1. 这种设计方法允许代码在所有逻辑中重用。只要您有三个层
- 接口/契约
- 数据库模型
- ViewModel
...那么就可以即插即用。哦,对了,别忘了扩展。这些扩展对于类转换非常有帮助。
2. 使用接口/契约可以更轻松地在 NUnit 框架中测试数据。这允许我们模拟数据,而无需为多种类型创建多个测试。
3. 使用多态集合类型可以拒绝序列化/反序列化失败的数据,因为文档的类型不正确。请参考上面的 Json 以了解“_t”属性的解释。
我知道我遗漏了一些东西,但现在想不起来了。
欢迎任何评论!祝您编码愉快!