Introduction

There is a growing change in the software development world around considering the choice of a NoSQL database platform over the more typical SQL database. SQL databases are centered around the idea of relational tables. Data is normalized and separated, optimizing disk space and query speed, and then linked together with foreign keys to create relationships. In contrast, NoSQL deals primarily with documents. Documents consists of flattened data, that might typically be normalized. While this may result in duplication of data and increased disk space, NoSQL is able to optimize query speed, based upon a key/value query process. Of course, one of the most popular features of NoSQL, particularly for developers, is the lack of a database schema design. NoSQL databases allow automatic creation of the database schema purely from the C# .NET data types. Changes to the database design are driven completely from the software developer’s code (ie., type library). With a new database model, some slight changes to traditional software architecture is required.

In this tutorial, we’ll create a basic C# .NET application that creates and displays Dragons from a NoSQL MongoDb database. Our C# .NET architecture will utilize the repository pattern, combined with a global database context provider. We’ll create a 3-tier system for accessing the Dragons, creating, updating, and deleting.

Database Administrators - Do Not Read

One the more touted features of NoSQL, and MongoDb in particular, is the creation of database tables and automatic schemas purely from the software C# .NET classes. Rather than having a database administrator DBA create the initial database schema and then having the developer mirror the schema with a C# .NET class library, the chore can be done in a single location.

To begin a MongoDb implementation, the C# .NET software developer can create the C# classes for the entities to track in the database. Upon saving the entities to the NoSQL database, the data will automatically be stored and persisted, without a required schema.

MongoDb uses JSON to store documents. Any changes to the document format or the C# class will continue to operate as expected; ignoring new fields, and inserting new fields where applicable.

But, What About All That Wasted Disk Space?

A popular counter-argument to using a NoSQL MongoDb document database in place of an SQL relational database is the duplication of data, due to the flattened documents stored within. Where you would typically normalize the data to avoid replication and optimize every bit stored, NoSQL document databases take a different stance, often storing data within the cloud, where disk space and consumption is less of a restriction.

Database Storage in the Cloud

For our example C# .NET MongoDb application, we’ll be utilizing a free database, stored in the cloud, with MongoLab. Connection strings for MongoDb take the form of a url in the format mongodb://username:password@ds012345.mongolab.com:12345/yourdatabase Our web.config would define the connection string, as follows:

Choosing a MongoDb Driver

As with any communication protocol and database platform, we’ll need to select a driver to use for connecting to our MongoDb database. Popular choices for C# .NET applications include the 10gen MongoDb driver and NoRM. Since we’ll be using LINQ exclusively for persisting data, we’ll be using the NoRM driver for accessing our data.

Dragon Breath

To begin our example C# .NET MongoDb implementation, we’ll create our C# .NET class library to consist of a Dragon type. To demonstrate the document format that NoSQL uses, we’ll include an additional embedded type of Breath, which will contain its own set of fields. A copy of Breath will be stored inside Dragon in the database, demonstrating the flattened data document. Also note, once these C# .NET types are persisted to the MongoDb database, the documents are automatically created - no schema required. We can define our types as follows:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

publicclassDragon

{

public ObjectId Id { get; privateset; }

publicstring Name { get; set; }

publicint Age { get; set; }

publicstring Description { get; set; }

publicint Gold { get; set; }

publicint MaxHP { get; set; }

publicint HP { get; set; }

public Breath Weapon { get; set; }

public DateTime DateBorn { get; set; }

public DateTime? DateDied { get; set; }

publicDragon()

{

DateBorn = DateTime.Now;

}

}

publicclassBreath

{

publicenum BreathType

{

Fire,

Ice,

Lightning,

PoisonGas,

Darkness,

Light

};

publicstring Name { get; set; }

publicstring Description { get; set; }

public BreathType Type { get; set; }

}

In the above code, we’ve simply defined two basic models for our Dragon’s data. This data will be persisted to the MongoDb database exactly as defined. Our unique identifier (defined as an ObjectId) can be considered as the primary key for our Dragon table. MongoDb will generate an Id upon saving.

Adding Some Enterprise to Your Repository

We could call the C# .NET MongoDb driver’s direct methods for reading and saving data. However, in this tutorial, we’ll create something a little more robust. We’ll implement a basic repository pattern for accessing the data, utilizing LINQ for queries. We’ll also provide a thread-safe global data context that can be used in a desktop application or web application for accessing the database.

In the above concrete provider code for the MongoDb NoRM driver, we implement each interface method by calling LINQ compatible methods. Querying the database returns IQueryable interfaces, allowing us to refine the query before actually sending it to the database. MongoDb will take care of optimizing the call and executing the query, before returning the data back to our C# .NET application.

Not All Global Variables Are Evil

We can enhance our repository pattern with a global database context provider. This will allow us to access the database provider from any point within our C# .NET application and optimize the usage of the same application pool thread and database provider call. For web applications, we’ll store the provider context within the HttpContext model. For desktop and console applications (.exe), we’ll store the provider in a local HashTable, per thread. This will allow us to call the MongoDb database by using a call such as:

1

var dragons = DbContext.Current.All<Dragon>().ToList();

Our DbContext database context provider appears, as follows:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

publicstaticclassDbContext

{

privateconststring HTTPCONTEXTKEY = "Session.Base.HttpContext.Key";

privatestaticreadonly Hashtable _threads = new Hashtable();

///<summary>

/// Returns a database context or creates one if it doesn't exist.

///</summary>

publicstatic IRepository Current

{

get

{

return GetOrCreateSession();

}

}

///<summary>

/// Returns true if a database context is open.

///</summary>

publicstaticbool IsOpen

{

get

{

IRepository session = GetSession();

return (session != null);

}

}

#region Private Helpers

privatestatic IRepository GetOrCreateSession()

{

IRepository session = GetSession();

if (session == null)

{

session = ObjectFactory.GetInstance<IRepository>();

SaveSession(session);

}

return session;

}

privatestatic IRepository GetSession()

{

if (HttpContext.Current != null)

{

if (HttpContext.Current.Items.Contains(HTTPCONTEXTKEY))

{

return (IRepository)HttpContext.Current.Items[HTTPCONTEXTKEY];

}

returnnull;

}

else

{

Thread thread = Thread.CurrentThread;

if (string.IsNullOrEmpty(thread.Name))

{

thread.Name = Guid.NewGuid().ToString();

returnnull;

}

else

{

lock (_threads.SyncRoot)

{

return (IRepository)_threads[Thread.CurrentThread.Name];

}

}

}

}

privatestaticvoidSaveSession(IRepository session)

{

if (HttpContext.Current != null)

{

HttpContext.Current.Items[HTTPCONTEXTKEY] = session;

}

else

{

lock (_threads.SyncRoot)

{

_threads[Thread.CurrentThread.Name] = session;

}

}

}

#endregion

}

Note in the above code, the property “Current” allows us to access our database provider to load or save data to MongoDb. This property, in turn, calls GetOrCreateSession() which will retrieve the currently active database connection or create a new one, if one does not exist.

You may notice our DbContext provider contains no reference to a concrete provider type. This allows us to easily swap in and out different MongoDb providers. To change providers, simply reference the associated DLLs and implement a new IRepository class for your concrete provider. Then specify which concrete provider to load.

Spice it Up With Some Dependency Injection

The choice of concrete provider in our database context provider is performed in the call to ObjectFactory.GetInstance(). We’re actually using StructureMap as our dependency injection tool. This lets us specify the concrete class to load, for our repository, in our main program. Creating new drivers becomes an easy task.

Our main program will include a simple Setup class, which initializes StructureMap to tell it which concrete provider class to load upon startup. In our case, this will be our MongoRepository class, which uses the NoRM MongoDb driver.

When using in a C# ASP .NET web application, you’ll want to dispose of the MongoDb database connection at the end of each request. You can do this by editing your Global.asax.cs and overriding protected void Application_EndRequest(object sender, EventArgs e) to call the Setup.Close() method, as shown above.

Creating our Business Logic Layer

We’ve finished implementing our database tier layer, which consisted of our MongoDb database provider, a repository pattern, and our db context. We can now implement the mid-tier (2nd-tier) which will contain our business logic for accessing the database. All calls to the MongoDb database will go through our business logic layer first.

We can create a simple DragonManager class for accessing the Dragon data, as follows:

Note, in the above code, we’ve wrapped calls to our C# .NET MongoDb database provider to encapsulate business logic. We’ve provided a method for retrieving all Dragons from the database. We’ve also provided a method for searching the dragons by keyword. The Save and Delete methods are also included.

Here Come The Dragons

With our C# .NET MongoDb repository complete, we can now access our database. First, we’ll create some Dragons with the following code:

We can then save the database directly to our MongoDb database, without even creating a schema, with the following code:

1

DragonManager.Save(dragon);

We can query the dragons with code similar to the following simple C# .NET MongoDb console program:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

static void Main(string[] args)

{

stringkeyword = "";

// Initialize our database provider.

Setup.Initialize();

while (true)

{

// Search for dragons.

List<Dragon> dragons = DragonManager.Find(keyword);

// Display the dragons.

DisplayDragons(dragons);

// Get input from the user.

Console.Write("Enter text to search by or Q to quit:>");

keyword = Console.ReadLine();

// Check the input.

if (keyword.ToUpper() == "Q")

break;

}

Setup.Close();

}

Note, the above code first initializes our dependency injector to specify the concrete MongoDb driver to use, which in our case is our MongoRepository class (using NoRM). We then call our business logic tier’s DragonManager.Find() method to retrieve a list of dragons.

Peeking Under the Covers of MongoDb

For a sneak peak at what happens when you issue a query to MongoDb, we can look in the NoSQL profiler and record a query command. We can see the following JSON command passed to the MongoDb server:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

{

"ts" : new Date("Thu, 01 Jan 2012 01:01:01 GMT -01:00"),

"op" : "query",

"ns" : "dragons.Dragon",

"query" : {

"_id" : ObjectId("9ea7c100d38281e40a000000")

},

"ntoreturn" : 1,

"idhack" : true,

"responseLength" : 259,

"millis" : 0,

"client" : "1.1.1.1",

"user" : ""

}

Note in the above JSON packet, the query includes an ObjectId, representing the primary key (unique identifier) for our Dragon. Since unique id’s are generated automatically, we can ensure compatibility across database servers in the cloud if sharding is used to scale the database implementation. This is in contrast to using auto-incremented identifiers, which could result in conflicts when sharding across multiple database servers.

LINQ queries to manipulate the data are also executed on the MongoDb database server, in a similar fashion to the above JSON query packet. The data is then returned to the C# .NET application layer.

Taking a look in our MongoLab administrator panel, we can view the contents of a Dragon record. Our Dragon is also stored in JSON format, as the following record demonstrates:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

{

"_id": {

"$oid": "3d0b95030ad4cdc00e000000"

},

"Name": "Black Serpent",

"Age": 99,

"Description": "A big dragon.",

"Gold": 781,

"MaxHP": 11,

"HP": 11,

"Weapon": {

"Name": "Breath",

"Description": "A breath attack.",

"Type": 2

},

"DateBorn": {

"$date": "2012-01-09T16:41:39.376Z"

},

"DateDied": null

}

Notice how the Weapon (our Breath class) is embedded in the record. A single call for a Dragon can return its Breath type as well. Of course, you can filter the results and select individual columns to return. This can allow you to limit the bandwidth consumed per request.

Here is another example MongoDb record, this time filtering results by searching for records containing the Name “Dark” within it:

Relational Data in a Non-Relational World

Up until now, we’ve implemented a strict NoSQL document-style flat record for a Dragon and its Breath. However, you can also implement relational data in the non-relation MongoDb database. We can do this by including foreign key references to serve as links, connecting tables. For example, to add a Realm for our dragons to inherit from, we could create a new Realm data type, as follows:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

publicclassRealm

{

publicenum RegionType

{

Mist,

Love,

Abyss,

Hatred,

Hell

};

public ObjectId Id { get; privateset; }

publicstring Name { get; set; }

public RegionType Region { get; set; }

publicRealm()

{

}

publicRealm(RegionType region)

{

Name = region.ToString();

Region = region;

}

}

So far, nothing has changed from our Dragon implementation. We’ve created a simple type, Realm. It contains a primary key of type ObjectId (a unique generated id from MongoDb).

We can add the Realm to our Dragon in a one-to-many relationship by including a reference to the Realm foreign key, as follows:

1

2

3

4

5

6

7

8

9

10

11

publicclassDragon

{

public ObjectId Id { get; privateset; }

public ObjectId RealmId { get; set; }

publicstring Name { get; set; }

publicint Age { get; set; }

publicstring Description { get; set; }

...

}

Our foreign key is simple a copy of the Realm table’s primary identifier. Of course, we’ll need to track the Realm object and Id property, which we can do with the help of a lazy-load property.

Lazy Loading a Foreign Key in MongoDb

We can add the following property to the Dragon class to include a lazy-load property as a foreign key reference:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

private Realm _realm;

[MongoIgnore]

public Realm Realm

{

get

{

// Lazy-load.

if (_realm == null)

{

_realm = DbContext.Current.Single<Realm>(r => r.Id == RealmId);

}

return _realm;

}

set

{

RealmId = value.Id;

_realm = value;

}

}

Note, since we’ll be maintaining this property ourselves, within the code, we include a MongoIgnore attribute. This tells MongoDb to ignore persisting the Realm property within the Dragon document. Instead, we’ll simply link to our Realm record, rather than including a copy of it within. When we’re reading to access the Realm property, we’ll automatically fetch the Realm data, using its primary key (which is stored on the Dragon document instead of the Realm data). If we’ve already fetched the Realm, we just return it.

A Simple Manager Class for a Static List

Since our dragon’s Realm object is just a static list of data, similar to the contents of a drop-down selector, we can create a business logic manager class to retrieve the data, as follows:

Note, in our CreateRandom() method, when creating a new Realm (or any item from a static list), we first check if it exists by trying to load it. If it does not yet exist, we create the new entry and return it.

Under the Covers of a Relational Record in MongoDb

So what does our new Dragon record look like, with its new relational link to Realm?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

{

"_id": {

"$oid": "be0b95030ad4cdc00e090000"

},

"RealmId": {

"$oid": "ac1b94030ad4cd0c1b060000"

},

"Name": "Golden Spirit",

"Age": 84,

"Description": "A big dragon.",

"Gold": 621,

"MaxHP": 10,

"HP": 10,

"Weapon": {

"Name": "Breath",

"Description": "A breath attack.",

"Type": 3

},

"DateBorn": {

"$date": "2012-01-01T01:01:01.001Z"

},

"DateDied": null

}

Note the new property “RealmId” included in our document. This is a relational reference to another MongoDb document. Looking in our MongoLab administrative area, we can find the associated Realm document, defined as follows:

1

2

3

4

5

6

7

{

"_id": {

"$oid": "ac1b94030ad4cd0c1b060000"

},

"Name": "Abyss",

"Region": 2

}

See It In Action

Download @ GitHub

You can download the project source code on GitHub by visiting the project home page.

About the Author

This article was written by Kory Becker, software developer and architect, skilled in a range of technologies, including web application development, machine learning, artificial intelligence, and data science.rs.