23
MongoDB Atlas in C#

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

FIRSTNAMELASTNAMEPOSITIONRANKSHIP
JoshuaAshFuse Tender6th ClassFrieda's Glory
SallyBarkerPilot Scrat
SallyBarkerArms Master Scrat
BilCilantroCook's Mate Scrat
AlFarnsworthDiplomatHall MonitorFrieda's Glory
AlFarnsworthInterpreter Frieda's Glory
MajorMajorCook's MateMajorAthena Ascendant
BudPickoverCaptainCaptainAthena 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:

  • “Not Normal but Not Abnormal” talks a bit about data that doesn't fit well in a relational database but that does fit in a document database.
  • “XML, JSON, and BSON” gives a few details about the XML, JSON, and BSON formats that are commonly used by document databases.
  • “Install MongoDB Atlas” explains how to create a MongoDB Atlas database in the cloud.
  • “Find the Connection Code” tells how you can find the connection code that allows your program to connect to the database.

When you reach “Create the Program” in Chapter 22, return here and start on the following sections.

CREATE THE PROGRAM

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.

Install the MongoDB Database Adapter

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.

  1. Open the Project menu and select Manage NuGet Packages.
  2. Click the Browse tab and enter MongoDB.Driver in the search box. Click that entry in the list on the left.
  3. Click the Install button on the right. This shouldn't take too long, but it's still nice to have a fast Internet connection.

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.

Helper Methods

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

METHODPURPOSE
PersonStringFormats a string for a document
DeleteOldDataDeletes any old data from the database
CreateDdataAdds documents to the database
QueryDataDisplays a header and the records in the postings collection

The following sections describe these helper methods.

PersonString

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

DeleteOldData

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.

CreateData

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.

QueryData

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

Main Program

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

FIRSTNAMELASTNAMEPOSITIONRANKSHIP
JoshuaAshFuse Tender6th ClassFrieda's Glory
SallyBarkerPilot Scrat
SallyBarkerArms Master Scrat
BilCuminCook's Mate Scrat
AlFarnsworthDiplomatHall MonitorFrieda's Glory
AlFarnsworthInterpreter Frieda's Glory
MajorMajorCook's MateMajorAthena Ascendant
BudPickoverCaptainCaptainAthena 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.

SUMMARY

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.189.186.167