Channels ▼
RSS

Embedded Systems

MongoDB with C#: Deep Dive


The SetFields method uses a typed builder to specify the fields to be retrieved (Fields<Game>.Include). Thus, the code uses the properties defined in the Game class to provide the desired fields with lambda expressions. In this case, each Game document doesn't include a huge amount of additional data, but in other cases where documents include embedded documents, it is a good practice to retrieve only what you need to display. Notice that it is very easy to limit the information you require because you are still providing instances of the same Game class in the results.

Finally, add the PlayerService class (that implements EntityService<Player>), within the Services folder:

namespace RetrogamesWeb.Data.Services
{
    using System.Collections.Generic;

    using Entities;
    using MongoDB.Bson;
    using MongoDB.Driver;
    using MongoDB.Driver.Builders;

    public class PlayerService: EntityService<Player>
    {
        public void AddScore(string playerId, Score score)
        {
            var playerObjectId = new ObjectId(playerId);

            var updateResult = this.MongoConnectionHandler.MongoCollection.Update(
                    Query<Player>.EQ(p => p.Id, playerObjectId), 
                    Update<Player>.Push(p => p.Scores, score),
                    new MongoUpdateOptions
                    {
                        WriteConcern = WriteConcern.Acknowledged
                    });

            if (updateResult.DocumentsAffected == 0)
            {
                //// Something went wrong
                
            }
        }

        public IEnumerable<Player> GetPlayersDetails(int limit, int skip)
        {
            var playersCursor = this.MongoConnectionHandler.MongoCollection.FindAllAs<Player>()
                .SetSortOrder(SortBy<Player>.Ascending(p => p.Name))
                .SetLimit(limit)
                .SetSkip(skip)
                .SetFields(Fields<Player>.Include(p => p.Id, p => p.Name));
            return playersCursor;
        }

        public override void Update(Player entity)
        {
            var updateResult = this.MongoConnectionHandler.MongoCollection.Update(
                    Query<Player>.EQ(p => p.Id, entity.Id),
                    Update<Player>.Set(p => p.Name, entity.Name)
                        .Set(p => p.Gender, entity.Gender),
                    new MongoUpdateOptions
                    {
                        WriteConcern = WriteConcern.Acknowledged
                    });

            if (updateResult.DocumentsAffected == 0)
            {
                //// Something went wrong

            }
        }
    }
}

The PlayerService class adds a GetPlayersDetails method that uses the same technique I discussed for the GameService.GetGamesDetails method. In this case, the GetPlayersDetails method only retrieves the Id and Name fields. Because each Player document includes the scores, it is very important to avoid retrieving the embedded documents when not necessary.

A similar problem appears when the user wants to update a field for a Player document. Imagine the user wants to change the value for the name: An inefficient update operation would save the new document with all the embedded score documents and the new name. The result of an edit view can modify just two fields, Name and Gender, without changing anything related to the scores. This is why the most convenient option is to write an Update method that updates only these two fields. The method makes chained calls to the Set method to specify the new values for these fields.

When you have to register a new score for a player, you want to use the same $push operator you used when working with the MongoDB shell because you don't want to write the entire scores array with the new score document. The AddScore method calls the Update<Player>.Push method to add the new score received as a parameter to the player document that matched the specified Id.

Adding Controllers and Views

Now, it is necessary to go back to the RetrogamesWeb ASP.NET MVC Web application project. Add a reference to MongoDB.Bson.dll, because the entities used as ViewModels have properties with the MongoDB.Bson.ObjectId type. In addition, add a reference to the previously created class library project, RetrogamesWeb.Data.

The ASP.NET MVC Web application project includes the following folders and files that weren't part of the selected template when you created the project (see Figure 5):

  • Controllers folder
    • Controllers\GameController.cs
    • Controllers\PlayerController.cs
  • CustomModelBinders folder
    • CustomModelBinders\BsonObjectIdBinder.cs
  • Models folder
    • Models\PlayerGames.cs
  • Views folder
    • Views\Game folder
      • Views\Game\Create.cshtml
      • Views\Game\Index.cshtml
    • Views\Player folder
      • Views\Player\Create.cshtml
      • Views\Player\Delete.cshtml
      • Views\Player\Details.cshtml
      • Views\Player\Edit.cshtml
      • Views\Player\Index.cshtml
      • Views\Player\PlayGames.cshtml

The RetrogamesWeb ASP.NET MVC Web application project structure in Solution Explorer
Figure 5: The RetrogamesWeb ASP.NET MVC Web application project structure in Solution Explorer.

Create the CustomModelBinders folder and add a new implementation of the IModelBinder interface, BsonObjectIdBinder:

namespace RetrogamesWeb.CustomModelBinders
{
    using System.Web.Mvc;

    using MongoDB.Bson;

    public class BsonObjectIdBinder : IModelBinder
    {
        public object BindModel(
            ControllerContext controllerContext, 
            ModelBindingContext modelBindingContext)
        {
            //// Retrieve a value object using modelBindingContext.ModelName as the key
            var valueProviderResult = modelBindingContext.ValueProvider.GetValue(modelBindingContext.ModelName);
            //// Now, create and return a new instance of MongoDB.Bson.ObjectId with the raw string retrieved from the model's property
            return new ObjectId(valueProviderResult.AttemptedValue);
        }
    }
}

It is necessary to create the BsonObjectIdBinder in order to convert from string to BsonObjectId for the ViewModel instances. The IModelBinder implementation wouldn't be necessary if I created ViewModels that were independent from the data domain classes. If you don't add a custom model binder, but you use properties of the BsonObjectId type in your ViewModels, you will see the following System.InvalidOperationException exception message:

System.InvalidOperationException: The parameter
conversion from type 'System.String' 
to type 'MongoDB.Bson.ObjectId' failed because 
no type converter can convert between these types.

Register the BsonObjectIdBinder custom model binder in the MvcApplication.Application_Start method within Global.asax.cs. Just add the following lines to the code in the Application_Start method:

//// Register the BsonObjectIdBinder custom model binder
ModelBinders.Binders.Add(typeof(ObjectId), new BsonObjectIdBinder());

Add a new class, PlayerGames, within the Models folder:

namespace RetrogamesWeb.Models
{
    using System.Collections.Generic;

    using Data.Entities;

    public class PlayerGames
    {
        public Player Player { get; set; }

        public List<Game> AvailableGames { get; set; }
    }
}

The ViewModel references the Player and Game entities to allow a view to display the active player and the games that he/she can play.

Add a new class, GameController, within the Controllers folder:

namespace RetrogamesWeb.Controllers
{
    using System;
    using System.Web.Mvc;

    using Data.Entities;
    using Data.Services;

    public class GameController : Controller
    {
        //
        // GET: /Game/Create

        public ActionResult Create()
        {
            return View(
                new Game()
                {
                    ReleaseDate = DateTime.Today,
                    Played = false
                });
        }

        //
        // POST: /Game/Create

        [HttpPost]
        public ActionResult Create(Game game)
        {
            try
            {
                if (ModelState.IsValid)
                {
                    var gameService = new GameService();
                    gameService.Create(game);
                    return RedirectToAction("Index");
                }

                return View();
            }
            catch
            {
                return View();
            }
        }

        //
        // GET: /Game/

        public ActionResult Index()
        {
            var gameService = new GameService();
            var gamesDetails = gameService.GetGamesDetails(100, 0);

            return View(gamesDetails);
        }
    }
}


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 

Video