65.9K
CodeProject 正在变化。 阅读更多。
Home

C# MongoDB - 带有通用 CRUD 的多态集合

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (9投票s)

2016年2月8日

CPOL

7分钟阅读

viewsIcon

53609

在 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,您基本上会做同样的事情,只不过您会传入 IOrganizationOrganizationOrganizationContext 类型。

提示:为了使转换更容易,我建议您编写自己的扩展类来转换这三个层架构类型。这会让您更容易,因为这些转换大多数都是可重用的。您只需调用 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”属性的解释。

我知道我遗漏了一些东西,但现在想不起来了。

欢迎任何评论!祝您编码愉快!

© . All rights reserved.