Click here to Skip to main content
15,881,715 members
Articles / Database Development / MongoDB

C# MongoDB - Polymorphic Collections with Generic CRUDs

Rate me:
Please Sign up or sign in to vote.
4.91/5 (9 votes)
8 Feb 2016CPOL7 min read 50.9K   23   6
Using the Mongo C# Driver in Visual Studio to connect to MongoLab and creating generic CRUDs.

Introduction

This article goes over an advanced architecture in Visual Studio 2015 C# in connecting to MongoDB hosted in MongoLab and creating polymorphic and generic CRUDs for reusability across multiple classes.

We will be using the three layer architecture: Interface/Contract Model, Database Model, and View Model Context.

Background

Before you get started on this article, I'm assuming that you know how to setup a free node database in MongoLab, know how to connect to MongoDB, writing MongoDB CRUDs, and using polymorphic and generic types in C#.

The Required Classes

To start out let's create the interface, database, and view models before proceeding to the Mongo database handler.

IMongoEntity<TId>

C#
public interface IMongoEntity<TId>
{
  TId Id { get; set; }
}

TId is a generic type. We'll be using type ObjectId from the Mongo library. If you want, you could use other types for the Id as well. Just make sure you represent your Id property as type ObjectId from Mongo's serialization attribute i.e. [BsonRepresentation(BsonType.ObjectId)].

IMongoCommon

C#
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 will be the base interface for all collection documents to inherit. These properties will be the common properties that are shared throughout all collection documents. Most importantly, you will see a lot of IMongoCommon in the generic CRUDs that we will be writing later in this article.

IAddress

C#
public interface IAddress
{
  string Street { get; set; }
  string City { get; set; }
  string State { get; set; }
  string Zip { get; set; }
  string Country { get; set; }
}

IAddress doesn't implement IMongoCommon because it will be an embedded document.

Address

C#
[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()] and [BsonKnownTypes()] lets the server know how to serialize/deserialize a document of this class type. You can read more about it in by clicking here

We are defaulting these strings to empty strings because when we pass a null string to a web page, we get it back as a string. Originally, I used [BsonIgnoreIfNull], but strings can never be null once it passes to the web page and it'll make it more complicated on the server side to account for this in the business logic. So basically, if the user doesn't specify anything for the string value, it is an empty string and the serializer will ignore it when we store it into the database.

AddressContext (Address view model)

C#
[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

C#
public interface IEmployee : IMongoCommon
{
  string FirstName { get; set; }
  string LastName { get; set; }
  IEnumerable<IAddress> Addresses { get; set; }
}

Employee

C#
[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();
}

We set the [BsonIgnoreIfNull] serialization attribute at Addresses because we don't want to store null list/arrays of Addresses into the database. ShouldSerializeAttribute is a boolean method from the Mongo C# driver that lets the server know to serialize the property if it is not empty. In this case, the attribute is Addresses.

EmployeeContext (Employee view model)

C#
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

C#
public interface IOrganization : IMongoCommon
{
  string Uri { get; set; }
  IEnumerable<IAddress> Addresses { get; set; }
}

Organization

C#
[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 (Organization view model)

C#
[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; }
}

The Mongo Connection Handler

Let's move on to creating the Mongo connection handler. We'll create the interface class for the main connection in which the inheriting database handler class will implement. This class will take in a generic type for the nature of the IMongoCollection.

IMyDatabase

C#
public interface IMyDatabase<T>
{
  IMongoDatabase Database { get; }
  IMongoCollection<T> Collection { get; }
}

IMongoCollection takes in a generic type T , which is the interface class for all document objects. To make it easier for you understand, once we get into the CRUDs, T is any class that implements IMongoCommon.

Next we will create the inheriting class for the Mongo connection handler.

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>();
  }
}

We are only registering the database layer classes because these are the class type that we will be storing into the Mongo database.

The Global CRUD Logic

Moving on the global CRUD logic, we will be creating a class called ILogic that takes in a generic type of T. This class will be the interface class for all logic to implement.

ILogic<T>

C#
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>

C#
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;
  }
}

This GlobalLogic class takes in two generic types, the type of the collection/interface and the type of the view model. I will explain each of these methods briefly and what they do.

1. GetAllAsync(TCollection collection)

This method gets gets all documents from the collection. You can specifiy f => true or new BsonDocument() for the filter.

2. GetOneAsync(TCollection collection, TContext context)

This method gets a document by Id. We passed in a new BsonDocument() because with polymorphic types, we won't be able to access the Id property with the LINQ lambda. This will be consistent throughout all the CRUDs.

3. GetOneAsync(TCollection collection, string id)

This method is a passthrough method to method #2 to eliminate redundancy in our business logic.

4. GetManyAsync(TCollection collection, IEnumerable<TContext> contexts)

This is also a pass through method to method #2, except this time we process it as a foreach loop and add it to a list to return. If the underlying context is null, it won't add it to the return list.

5. GetManyAsync(TCollection collection, IEnumerable<string> ids)

This is the same as method #4, except its pass through method is method #3.

6. RemoveOneAsync(TCollection collection, TContext)

We are implementing soft-delete in our system.  The flag to indicate this is the IsActive property in each of the documents. We mark the document as inactive and then update the modified date.

7. RemoveOneAsync(TCollection collection, string id)

This is a pass through method to method #6 if we want to pass the parameter in as a string rather than the context model.

8. RemoveManyAsync(TCollection collection, IEnumerable<TContext> contexts

9. RemoveManyAsync(TCollection collection, IEnumerable<string> ids

These two methods are basically the same as method #4 and #5 except they refer to their underlying methods, #6, and #7.

Employee Logic

For this next class, we will be creating the EmployeeLogic class.

EmployeeLogic

C#
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;
  }
}

This EmployeeLogic class is also known as the services class for Employees. Basically, it inherits the ILogic<T> class in which T is OrganizationContext. We then get the database instance passing in the interface IEmployee as the polymoprhic IMongoCollection type. If you recall in MyDatabase class, Employees = new MyDatabase<IEmployee>("employee") returns the collection called "employee" of type IEmployee. We then declare the GlobalLogic<IEmployee, Employee> class to get the Mongo logic for our EmployeeLogic class. Of course, when IEmployee implements ILogic<EmployeeContext>, it'll inherit all of ILogic's members. Moving onto GetAllAsync(), when the type comes back from GlobalLogic, it is returned as IEnumerable<IEmployee>, but the method returns IEnumerable<EmployeeContext>. That's where our translation comes in at the return statement.

To implement OrganizationLogic, you would basically do the same thing, except you'll pass in the type IOrganization, Organization, and OrganizationContext.

TIP: To make the translation easier, I suggest you write your own extension class to convert these three layer architecture type. This will make it easier for you because most of these translation are reusable. You can just call employee.ToEmployeeContext(). Refer to the SaveOneAsync() method. For example:

C#
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

To use the code in other projects such as in an MVC Controller, Windows Forms App, or Console App, just call a new instance of EmployeeLogic and/or OrganizationLogic. Using the code in MVC Controller is a bit different. You'll have to register the service at startup, in the controller, and in the controller constructor. After declaring a new instance of the logic/service, all you need to do is _logic.GetAllAsync();. To look at this in a console app, refer to the code below:

C#
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 });
  }
}

The Data in Json Format

{
  "_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" is the type of the document, also known as the database layer type. This is why we do the Bson Class Mappings in the database connection handler. This puts a strict layer on the client side so that if any data were altered, it would reject the resulting altered data and will only accept valid data.

Points of Interest

1. This design approach allows code to be reusable throughout all logic. As long as you have three layers:

  • Interface/Contract
  • Database Model
  • View Model

...it'll just be plug and play from there. Oh yea, don't forget the extensions too. These extensions are extremely helpful for class translation.

2. Using interface/contracts allows data to be more easily tested in the NUnit framework. This allows us to mock data instead of having to create multiple tests for multiple types.

3. Using polymorphic collection type allows data to be rejected if the serialization/deserialization fails because the document is not the right type. Refer back to the Json for an explanation of the "_t" attribute.

I know I am missing some more stuff, but can't really think of them right now.

Any comments are appreciated! Happy coding!

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralFor those asking, it should be IMongoCommon instead of IDocumentCommon Pin
Member 1395819324-Feb-19 11:39
Member 1395819324-Feb-19 11:39 
QuestionIDocumentCommon interface is missing Pin
koolkarthik8-Feb-19 6:42
professionalkoolkarthik8-Feb-19 6:42 
Questiondownload Pin
PreethamGowda20-Feb-18 13:22
PreethamGowda20-Feb-18 13:22 
Questionwhere can I get the code Pin
Member 1050370113-Dec-16 6:31
Member 1050370113-Dec-16 6:31 
QuestionGreat article. Very well thought out. Pin
Erik Bartlow10-Aug-16 19:56
Erik Bartlow10-Aug-16 19:56 
AnswerRe: Great article. Very well thought out. Pin
Shi Her12-Aug-16 8:51
professionalShi Her12-Aug-16 8:51 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.