This chapter's example uses C# to build a NoSQL document database in the cloud. It uses the MongoDB Atlas database to save and query the data shown in Table 23.1 about assignments for East Los Angeles Space Academy graduates.
TABLE 23.1: Assignment data
FIRSTNAME | LASTNAME | POSITION | RANK | SHIP |
---|---|---|---|---|
Joshua | Ash | Fuse Tender | 6th Class | Frieda's Glory |
Sally | Barker | Pilot | Scrat | |
Sally | Barker | Arms Master | Scrat | |
Bil | Cilantro | Cook's Mate | Scrat | |
Al | Farnsworth | Diplomat | Hall Monitor | Frieda's Glory |
Al | Farnsworth | Interpreter | Frieda's Glory | |
Major | Major | Cook's Mate | Major | Athena Ascendant |
Bud | Pickover | Captain | Captain | Athena Ascendant |
If you skipped Chapter 22, “MongoDB Atlas in Python,” which built a similar example in Python, return to that chapter and read the beginning and the first four sections, which are described in the following list:
When you reach “Create the Program” in Chapter 22, return here and start on the following sections.
To create a C# program to work with the Atlas ship assignment database, create a new C# Console App (.NET Framework) and then add the code described in the following sections.
Now that the database is waiting for you in the cloud, you need to install a database driver for it. Then you can start writing code.
This example uses the MongoDB database adapter to allow your program to communicate with the MongoDB Atlas database sitting in the cloud. To add the adapter to your project, follow these steps.
To make using the database driver easier, add the following code to the program just below the other using
statements:
using MongoDB.Bson;
using MongoDB.Driver;
The following sections describe some helper methods that the program will use. The last section in this part of the chapter describes the main program.
These methods do all of the program's real work. The main program just connects to the database and then calls them.
Table 23.2 lists the helper methods and gives their purposes.
TABLE 23.2: Helper methods and their purposes
METHOD | PURPOSE |
---|---|
PersonString | Formats a string for a document |
DeleteOldData | Deletes any old data from the database |
CreateDdata | Adds documents to the database |
QueryData | Displays a header and the records in the postings collection |
The following sections describe these helper methods.
The following PersonString
method returns a string holding the first name, last name, ship, rank, and position for a person's record all formatted nicely in the final output.
static private string PersonString(BsonDocument doc)
{
// These should always be present.
string name = doc["FirstName"].AsString + " " + doc["LastName"].AsString;
string ship = doc["Ship"].AsString;
// This might be a single value or a list.
string position = "";
if (doc["Position"].IsBsonArray)
{
position = string.Join(", ", doc["Position"].AsBsonArray);
}
else
{
position = doc["Position"].AsString;
}
// Rank may be missing.
string rank = "";
if (doc.Contains("Rank"))
rank = doc["Rank"].AsString;
else
rank = "---";
return $"{name,-16}{ship,-19}{rank,-15}{position}";
}
The code first gets the FirstName
and LastName
fields from the record and concatenates them. Notice how it uses the AsString
property to convert the value, which starts out as an object, into a string. That property throws an InvalidCastException
if the value is something other than a string, such as a number or date. Your program is responsible for knowing what kinds of values the document holds and handling them appropriately.
The code also gets the Ship
value, again as a string.
Because this kind of database doesn't have a fixed format for its records the way a relational database does, a field can hold more than one kind of information. In this example, Joshua Ash has one Position
value (Fuse Tender), but Sally Barker has two (Pilot and Arms Master).
The database doesn't care what values are in a document, so your program must handle the issue. That's why the method gets the Position
value and then uses the IsBsonArray
function to see if that value is an array. If it is an array, then the code uses string.Join
to join the position values, separating them with commas.
If the Position
value isn't an array, then the code just saves its value in the position
variable.
Just as the database driver doesn't care if different documents store different values in a field, it also doesn't care if a field is missing. If the code tries to access a value that isn't there, the data adapter panics and throws a KeyNotFoundException
.
In this example, some of the documents do not have a Rank
value, so to protect against the KeyNotFoundException
, the code uses the statement if (doc.Contains("Rank"))
to see if the Rank
value is present. If the value is there, then the method gets it. If the value is missing, the method uses three dashes in its place.
Having gathered all of the values that it needs into variables, the method composes the person's result string and returns it.
The following shows the result for Sally Barker:
Sally Barker Scrat --- Pilot, Arms Master
The following code shows the DeleteOldData
method:
static private void DeleteOldData(IMongoCollection<BsonDocument> collection)
{
FilterDefinition<BsonDocument>
deleteFilter = Builders<BsonDocument>.Filter.Empty;
collection.DeleteMany(deleteFilter);
Console.WriteLine("Deleted old data ");
}
This method takes a collection as a parameter. In MongoDB, a collection is somewhat analogous to a table in a relational database except that it holds documents instead of records. This example uses the postings
collection.
The code gets a special predefined filter that is empty. It passes that filter to the collection's DeleteMany
method to delete multiple documents. Passing the empty filter into the method makes it match every document, so they are all deleted.
The method finishes by displaying a message to show that it did something.
The following code shows how the CreateData
method adds documents to the postings
collection:
// Create sample data
static private void CreateData(IMongoCollection<BsonDocument> collection)
{
BsonDocument josh = new BsonDocument
{
{ "FirstName" , "Joshua" },
{ "LastName" , "Ash" },
{ "Position" , "Fuse Tender" },
{ "Rank" , "6th Class" },
{ "Ship" , "Frieda's Glory" }
};
BsonDocument sally = new BsonDocument
{
{ "FirstName" , "Sally" },
{ "LastName" , "Barker" },
{ "Position" , new BsonArray { "Pilot", "Arms Master" } },
{ "Ship" , "Scrat" }
};
BsonDocument bil = new BsonDocument
{
{ "FirstName" , "Bil" },
{ "LastName" , "Cumin" },
{ "Position" , "Cook's Mate" },
{ "Ship" , "Scrat" }
};
BsonDocument al = new BsonDocument
{
{ "FirstName" , "Al" },
{ "LastName" , "Farnsworth" },
{ "Position" , new BsonArray { "Diplomat", "Interpreter" } },
{ "Rank" , "Hall Monitor" },
{ "Ship" , "Frieda's Glory" }
};
BsonDocument major = new BsonDocument
{
{ "FirstName" , "Major" },
{ "LastName" , "Major" },
{ "Position" , "Cook's Mate" },
{ "Rank" , "Major" },
{ "Ship" , "Athena Ascendant" }
};
BsonDocument bud = new BsonDocument
{
{ "FirstName" , "Bud" },
{ "LastName" , "Pickover" },
{ "Position" , "Captain" },
{ "Rank" , "Captain" },
{ "Ship" , "Athena Ascendant" }
};
// Insert josh individually.
collection.InsertOne(josh);
// Insert the others as a group.
BsonDocument[] others = { sally, bil, al, major, bud };
collection.InsertMany(others);
Console.WriteLine("Created data ");
}
The method first creates several BsonDocument
objects to hold data for the people we will create. The initializer is similar to the one that you can use to initialize a dictionary in C#. It's also similar (although less so) to a JSON file or the way a Python program defines dictionaries. (Of course, the C# version adds some curly brackets. It's not for nothing that C# is called one of the “curly bracket languages”!)
Notice that the Position
value is a string in Joshua's data but it's a new BsonArray
in Sally's data. Notice also that Sally's data does not include a Rank
.
After it defines the people documents, the code executes the following code to insert a single document into the collection:
// Insert josh individually.
collection.InsertOne(josh);
It then executes the following code to show how you can insert multiple documents all at once:
// Insert the others as a group.
BsonDocument[] others = { sally, bil, al, major, bud };
collection.InsertMany(others);
In general, performing many operations with fewer commands saves network bandwidth, which is particularly important in a cloud application.
The method finishes by displaying a message so that you know it did something.
The QueryData
method displays a header and then repeats the same pattern several times. It uses a Language-Integrated Query (LINQ, which is pronounced “link”) query to find documents that match a pattern and then loops through the results to display information about the documents that were returned. The following code shows how the method begins. You'll see the rest of the method shortly.
static private void QueryData(IMongoCollection<BsonDocument> collection)
{
Console.WriteLine("{0,-16}{1,-19}{2,-15}{3}",
"Name", "Ship", "Rank", "Position");
Console.WriteLine(
"------------- ---------------- ------------ -----------");
// List everyone.
Console.WriteLine(" *** Everyone ***");
var selectAll =
from e in collection.AsQueryable<BsonDocument>()
select e;
foreach (BsonDocument doc in selectAll)
{
Console.WriteLine(PersonString(doc));
}
LINQ is a set of tools that let you embed SQL-like statements in .NET programs. It takes a little getting used to, but it's not too bad once you get the hang of it.
The LINQ statement in the preceding code starts with the clause from e in collection.AsQueryable<BsonDocument>()
. This makes variable e
represent documents chosen from the collection. There's no where
clause (you'll see those shortly), so this matches all of the documents in the collection.
The select e
piece of the statement tells LINQ that the statement should select the entire document e
.
When you use a LINQ statement, you save the result in a variable, in this case called selectAll
. LINQ doesn't actually evaluate the statement until you do something with it, such as convert it into an array or iterate over it. The QueryData
method uses a foreach
loop to iterate through the returned documents and display them.
The rest of the method repeats those steps but with different LINQ statements. In the next few paragraphs, I'll describe those statements without the extra code that displays their headers and the results. After I finished covering those, I'll show you the complete method.
Here's the second LINQ statement:
var selectScrat =
from e in collection.AsQueryable<BsonDocument>()
where e["Ship"] == "Scrat"
select e;
The clause where e["Ship"] == "Scrat"
makes LINQ match documents that have a Ship
value equal to Scrat
. This syntax is simpler than the dictionaries used by the Python example described in Chapter 22.
Here's the next find
statement:
var selectHasRank =
from e in collection.AsQueryable<BsonDocument>()
where e["Rank"] != BsonNull.Value
select e;
This statement makes a document match if its Rank
value is not null, so it returns people who have a rank.
The following statement matches documents where the Rank
value is null, so it returns people who have no rank:
var selectHasNoRank =
from e in collection.AsQueryable<BsonDocument>()
where e["Rank"] == BsonNull.Value
select e;
So far these matches have been fairly simple, but LINQ supports more complex queries that use the logical operators &&
(and) and ||
(or). The following statement matches documents where Position
is Cook's Mate
or Ship
is Frieda's Glory
:
var selectMateOrFrieda =
from e in collection.AsQueryable<BsonDocument>()
where e["Position"] == "Cook's Mate"
|| e["Ship"] == "Frieda's Glory"
select e;
This is much easier to understand than the corresponding find
statement used by the Python example described in Chapter 22.
The last two matches in the method repeat the preceding matches and sort the results. Here's the next one:
var selectMateOrFriedaSorted =
from e in collection.AsQueryable<BsonDocument>()
where e["Position"] == "Cook's Mate"
|| e["Ship"] == "Frieda's Glory"
orderby e["Ship"] ascending
select e;
This is similar to the previous LINQ statement but with an orderby
clause tacked on at the end to order the selected documents sorted by their Ship
values. Optionally, you can add the keyword ascending
(the default) or descending
.
The following code shows the QueryData
method's final query:
var selectMateOrFriedaSorted2 =
from e in collection.AsQueryable<BsonDocument>()
where e["Position"] == "Cook's Mate"
|| e["Ship"] == "Frieda's Glory"
orderby e["Ship"] ascending, e["FirstName"] ascending
select e;
This time the orderby
clause sorts first by Ship
and then by FirstName
(if two documents have the same Ship
).
The following code shows the whole method in one piece:
static private void QueryData(IMongoCollection<BsonDocument> collection)
{
Console.WriteLine("{0,-16}{1,-19}{2,-15}{3}",
"Name", "Ship", "Rank", "Position");
Console.WriteLine(
"------------- ---------------- ------------ -----------");
// List everyone.
Console.WriteLine(" *** Everyone ***");
var selectAll =
from e in collection.AsQueryable<BsonDocument>()
select e;
foreach (BsonDocument doc in selectAll)
{
Console.WriteLine(PersonString(doc));
}
// People posted to Scrat.
Console.WriteLine(" *** Assigned to Scrat ***");
var selectScrat =
from e in collection.AsQueryable<BsonDocument>()
where e["Ship"] == "Scrat"
select e;
foreach (BsonDocument doc in selectScrat)
{
Console.WriteLine(PersonString(doc));
}
// People with Rank values.
Console.WriteLine(" *** Has a Rank ***");
var selectHasRank =
from e in collection.AsQueryable<BsonDocument>()
where e["Rank"] != BsonNull.Value
select e;
foreach (BsonDocument doc in selectHasRank)
{
Console.WriteLine(PersonString(doc));
}
// People with no Rank.
Console.WriteLine(" *** Has no Rank ***");
var selectHasNoRank =
from e in collection.AsQueryable<BsonDocument>()
where e["Rank"] == BsonNull.Value
select e;
foreach (BsonDocument doc in selectHasNoRank)
{
Console.WriteLine(PersonString(doc));
}
// Cook's Mates or people on Frieda's Glory.
Console.WriteLine(" *** Cook's Mates or on Frieda's Glory ***");
var selectMateOrFrieda =
from e in collection.AsQueryable<BsonDocument>()
where e["Position"] == "Cook's Mate"
|| e["Ship"] == "Frieda's Glory"
select e;
foreach (BsonDocument doc in selectMateOrFrieda)
{
Console.WriteLine(PersonString(doc));
}
// Cook's Mates or people on Frieda's Glory, sorted by Ship.
Console.WriteLine(
" *** Cook's Mates or on Frieda's Glory, sorted ***");
var selectMateOrFriedaSorted =
from e in collection.AsQueryable<BsonDocument>()
where e["Position"] == "Cook's Mate"
|| e["Ship"] == "Frieda's Glory"
orderby e["Ship"] ascending
select e;
foreach (BsonDocument doc in selectMateOrFriedaSorted)
{
Console.WriteLine(PersonString(doc));
}
// Cook's Mates or people on Frieda's Glory, sorted by Ship and FirstName.
Console.WriteLine(
" *** Cook's Mates or on Frieda's Glory, sorted ***");
var selectMateOrFriedaSorted2 =
from e in collection.AsQueryable<BsonDocument>()
where e["Position"] == "Cook's Mate"
|| e["Ship"] == "Frieda's Glory"
orderby e["Ship"] ascending, e["FirstName"] ascending
select e;
foreach (BsonDocument doc in selectMateOrFriedaSorted2)
{
Console.WriteLine(PersonString(doc));
}
}
Compared to the QueryData
method, the main program is relatively simple:
Static void Main(string[] args)
{
// Connect to MongoDB.
String user = "Rod";
string password = "EnterYourSecretPasswordHere";
string url = "postingsdb.b1bprz5.mongodb.net";
string connectString =
$"mongodb+srv://{user}:{password}@{url}/?retryWrites=true&w=majority";
MongoClient client = new MongoClient(connectString);
ImongoDatabase db = client.GetDatabase("personnel");
ImongoCollection<BsonDocument> collection =
db.GetCollection<BsonDocument>("postings");
// Delete any existing documents.
DeleteOldData(collection);
// Create new data.
CreateData(collection);
// Query the data.
QueryData(collection);
// Don't close the database connection.
// MongoDB uses a connection pool so it will be reused as needed.
Console.Write(" Press Enter to quit.");
Console.ReadLine();
}
This code stores the username, password, and database URL in variables. You should replace the username and password with the values that you used when you set up the database.
The code then uses them to compose the database connect string. In this example that string is:
mongodb+srv://Rod:[email protected]/
?retryWrites=true&w=majority
The program uses the connect string to create a new MongoClient
, uses the client to get the personnel
database, and uses that database to get the postings
collection that will contain the documents.
Next, the program calls the DeleteOldData
, CreateData
, and QueryData
methods to do the interesting work.
This database adapter keeps database connections in a pool so that it can reuse them when they are needed. To make the pool more efficient, the program does not close its connection; that way, it can be reused.
As a reminder, Table 23.3 contains the data inserted by the CreateData
method.
TABLE 23.3: Data inserted by the CreateData
method
FIRSTNAME | LASTNAME | POSITION | RANK | SHIP |
---|---|---|---|---|
Joshua | Ash | Fuse Tender | 6th Class | Frieda's Glory |
Sally | Barker | Pilot | Scrat | |
Sally | Barker | Arms Master | Scrat | |
Bil | Cumin | Cook's Mate | Scrat | |
Al | Farnsworth | Diplomat | Hall Monitor | Frieda's Glory |
Al | Farnsworth | Interpreter | Frieda's Glory | |
Major | Major | Cook's Mate | Major | Athena Ascendant |
Bud | Pickover | Captain | Captain | Athena Ascendant |
Finally, here's the program's output:
Deleted old data
Created data
Name Ship Rank Position
------------- ---------------- ------------ -----------
*** Everyone ***
Joshua Ash Frieda's Glory 6th Class Fuse Tender
Sally Barker Scrat --- Pilot, Arms Master
Bil Cumin Scrat --- Cook's Mate
Al Farnsworth Frieda's Glory Hall Monitor Diplomat, Interpreter
Major Major Athena Ascendant Major Cook's Mate
Bud Pickover Athena Ascendant Captain Captain
*** Assigned to Scrat ***
Sally Barker Scrat --- Pilot, Arms Master
Bil Cumin Scrat --- Cook's Mate
*** Has a Rank ***
Joshua Ash Frieda's Glory 6th Class Fuse Tender
Al Farnsworth Frieda's Glory Hall Monitor Diplomat, Interpreter
Major Major Athena Ascendant Major Cook's Mate
Bud Pickover Athena Ascendant Captain Captain
*** Has no Rank ***
Sally Barker Scrat --- Pilot, Arms Master
Bil Cumin Scrat --- Cook's Mate
*** Cook's Mates or on Frieda's Glory ***
Joshua Ash Frieda's Glory 6th Class Fuse Tender
Bil Cumin Scrat --- Cook's Mate
Al Farnsworth Frieda's Glory Hall Monitor Diplomat, Interpreter
Major Major Athena Ascendant Major Cook's Mate
*** Cook's Mates or on Frieda's Glory, sorted ***
Major Major Athena Ascendant Major Cook's Mate
Joshua Ash Frieda's Glory 6th Class Fuse Tender
Al Farnsworth Frieda's Glory Hall Monitor Diplomat, Interpreter
Bil Cumin Scrat --- Cook's Mate
*** Cook's Mates or on Frieda's Glory, sorted ***
Major Major Athena Ascendant Major Cook's Mate
Al Farnsworth Frieda's Glory Hall Monitor Diplomat, Interpreter
Joshua Ash Frieda's Glory 6th Class Fuse Tender
Bil Cumin Scrat --- Cook's Mate
Press Enter to quit.
You can look through the output to see how the different LINQ queries worked. The first did not use a where
clause, so it returned every document. Notice how the PersonString
method displayed three dashes for missing Rank
values and how it concatenated values when a document had multiple Position
values.
The next query picked out the documents where Ship
is Scrat
.
The two after that found documents that had a Rank
value and that did not have a Rank
value, respectively.
The first statement that used ||
found information about Cook's Mates and those assigned to Frieda's Glory (admittedly a combination that you might never find useful).
The next example sorts those results by Ship
. The final query sorts by Ship
first and then FirstName
, so Al Farnsworth comes before Joshua Ash.
This chapter shows how you can use C# and a NoSQL document database to store and retrieve BSON documents. As you work with the example, you might notice that some operations are relatively slow, particularly if you have a slow network connection. This is generally true of cloud applications. Network communications tend to be slower than local calculations.
The example uses BsonDocument
objects to define its data. It then uses collection methods such as InsertOne
and InsertMany
to add the data to the database. Later, it uses LINQ queries to find documents.
The next two chapters show how to use a NoSQL key-value database in the cloud. Chapter 24 shows a Python example and Chapter 25 shows a similar example in C#. Before you move on to those chapters, however, use the following exercises to test your understanding of the material covered in this chapter. You can find the solutions to these exercises in Appendix A.
18.189.186.167