Udostępnij za pośrednictwem


Using MongoDB with ASP.NET Web API

MongoDB is a popular NoSQL database that makes it a great backend for Web APIs which lend themselves towards a document store rather than a relational store. In this blog we show how you can use MongoDB with ASP.NET Web API to build an ApiController with support for HTTP GET, PUT, POST, and DELETE. Our sample controller is a simple contact manager Web API which builds on many of the same concepts as in the tutorial “Creating a Web API that Supports CRUD Operations” so I would recommend that you skim that for background.

In particular, the URI patterns are very similar to that of the tutorial:

Action HTTP Method Relative URI
Get a list of all contacts GET /api/contacts
Get a filtered list of all contacts GET /api/contacts?$top=2
Get a contacts by ID GET /api/contacts/id
Create a new contact POST /api/contacts
Update a contact PUT /api/contacts/id
Delete a contact DELETE /api/contacts/id

 

However, there are two differences worth noting:

  1. We host the sample ApiController in selfhost with a base URI of https://localhost:8080
  2. We have added support for OData-style queries on GET requests so that you can filter the set of contacts returned using $top, $skip, etc.

But I am getting ahead of myself – let’s get back to building the controller…

Note: Please see List of ASP.NET Web API and HttpClient Samples for the complete sample solution. Note that you still have to set up the MongoDB as described in the next section.

Prerequisites

Before you start you obviously need to download and install MongoDB. I followed the quick start instructions which has all the information you need and got me going in 10 minutes or so.

Second you need the MongoDB driver for C#. Here we use the MongoDB C# driver provided by 10gen and which is available as a NuGet package.

As an optional third step once your MongoDB installation has been set up you can use a tool like MongoVUE which allows you to browse and interact directly with the data in a MongoDB database.

Defining the Contact Type

We use the same basic Contact type used in “Creating a Web API that Supports CRUD Operations” but decorate it with an BsonIdAttribute (see the Mongo C# driver reference documentation for details) indicating which field is the id:

    1: public class Contact
    2: {
    3:     [BsonId]
    4:     public string Id { get; set; }
    5:  
    6:     public string Name { get; set; }
    7:  
    8:     public string Phone { get; set; }
    9:  
   10:     public string Email { get; set; }
   11:  
   12:     public DateTime LastModified { get; set; }
   13: }

Using MongoVUE we can see how Contact instances show up in the MongoDB database. The sample Contact data we use in this sample looks something like this

MongoVUE

Defining the Contact Repository

We use the common repository pattern to interact with the backend data source so the next step is to define the shape of the repository. We do this by defining an IContactRepository interface which we then implement so that it targets MongoDB. First, the interface is defined like this:

    1: public interface IContactRepository
    2: {
    3:     IEnumerable<Contact> GetAllContacts();
    4:  
    5:     Contact GetContact(string id);
    6:  
    7:     Contact AddContact(Contact item);
    8:  
    9:     bool RemoveContact(string id);
   10:  
   11:     bool UpdateContact(string id, Contact item);
   12: }

Implementing the Repository

It’s when implementing IContactRepository that we use the MongoDB C# driver API to connect to the data and to do the various CRUD operations defined by IContactRepository. First we set up the connection to database and get a Contact collection. By default we use the connection string “mongodb://localhost:27017” pointing at the localhost database we set up above.

For the purpose of this sample we also reset the database and add some default entries so we have something to start with. Note that the classes MongoServer, MongoDatabase, and MongoCollection<T> are all thread-safe so they can be used simultaneously by separate threads.

    1: public ContactRepository(string connection)
    2: {
    3:     if (string.IsNullOrWhiteSpace(connection))
    4:     {
    5:         connection = "mongodb://localhost:27017";
    6:     }
    7:  
    8:     _server = MongoServer.Create(connection);
    9:     _database = _server.GetDatabase("Contacts", SafeMode.True);
   10:     _contacts = _database.GetCollection<Contact>("contacts");
   11:  
   12:     // Reset database and add some default entries
   13:     _contacts.RemoveAll();
   14:     for (int index = 1; index < 5; index++)
   15:     {
   16:         Contact contact1 = new Contact
   17:         {
   18:             Email = string.Format("test{0}@example.com", index),
   19:             Name = string.Format("test{0}", index),
   20:             Phone = string.Format("{0}{0}{0} {0}{0}{0} {0}{0}{0}{0}", index)
   21:         };
   22:         AddContact(contact1);
   23:     }
   24: }

We now add the actual CRUD implementations as follows:

    1: public IEnumerable<Contact> GetAllContacts()
    2: {
    3:     return _contacts.FindAll();
    4: }
    5:  
    6: public Contact GetContact(string id)
    7: {
    8:     IMongoQuery query = Query.EQ("_id", id);
    9:     return _contacts.Find(query).FirstOrDefault();
   10: }
   11:  
   12: public Contact AddContact(Contact item)
   13: {
   14:     item.Id = ObjectId.GenerateNewId().ToString();
   15:     item.LastModified = DateTime.UtcNow;
   16:     _contacts.Insert(item);
   17:     return item;
   18: }
   19:  
   20: public bool RemoveContact(string id)
   21: {
   22:     IMongoQuery query = Query.EQ("_id", id);
   23:     SafeModeResult result = _contacts.Remove(query);
   24:     return result.DocumentsAffected == 1;
   25: }
   26:  
   27: public bool UpdateContact(string id, Contact item)
   28: {
   29:     IMongoQuery query = Query.EQ("_id", id);
   30:     item.LastModified = DateTime.UtcNow;
   31:     IMongoUpdate update = Update
   32:         .Set("Email", item.Email)
   33:         .Set("LastModified", DateTime.UtcNow)
   34:         .Set("Name", item.Name)
   35:         .Set("Phone", item.Phone);
   36:     SafeModeResult result = _contacts.Update(query, update);
   37:     return result.UpdatedExisting;
   38: }

Creating the Contact Controller

That’s it for the repository so we can now implement the actual ApiController. We base the implementation on the common pattern for supporting GET, PUT, POST, and DELETE as follows:

    1: public class ContactsController : ApiController
    2: {
    3:     private static readonly IContactRepository _contacts = new ContactRepository();
    4:  
    5:     public IQueryable<Contact> Get()
    6:     {
    7:         return _contacts.GetAllContacts().AsQueryable();
    8:     }
    9:  
   10:     public Contact Get(string id)
   11:     {
   12:         Contact contact = _contacts.GetContact(id);
   13:         if (contact == null)
   14:         {
   15:             throw new HttpResponseException(HttpStatusCode.NotFound);
   16:         }
   17:  
   18:         return contact;
   19:     }
   20:  
   21:     public Contact Post(Contact value)
   22:     {
   23:         Contact contact = _contacts.AddContact(value);
   24:         return contact;
   25:     }
   26:  
   27:     public void Put(string id, Contact value)
   28:     {
   29:         if (!_contacts.UpdateContact(id, value))
   30:         {
   31:             throw new HttpResponseException(HttpStatusCode.NotFound);
   32:         }
   33:     }
   34:  
   35:     public void Delete(string id)
   36:     {
   37:         if (!_contacts.RemoveContact(id))
   38:         {
   39:             throw new HttpResponseException(HttpStatusCode.NotFound);
   40:         }
   41:     }
   42: }

Note: We use IQueryable<Contact> as the return type of the Get() method. This enables automatic query-support using OData query syntax including $top and $skip. That is, if you use a URI like “/api/contacts?$top=2” with a “?$top=2” query component then you will only get the first two entries back.

Hosting the Controller

Now that we have the controller we can either host it in ASP or as selfhost. Here we use selfhost to host the controller in a simple console application but it would work exactly the same if hosted in ASP. As usual we create a HttpSelfHostConfiguration, add a route, then create a server, and start it:

    1: static void Main(string[] args)
    2: {
    3:     HttpSelfHostServer server = null;
    4:     try
    5:     {
    6:         // Set up server configuration
    7:         HttpSelfHostConfiguration config = new HttpSelfHostConfiguration("https://localhost:8080");
    8:  
    9:         config.Routes.MapHttpRoute(
   10:             name: "DefaultApi",
   11:             routeTemplate: "api/{controller}/{id}",
   12:             defaults: new { id = RouteParameter.Optional }
   13:         );
   14:  
   15:         // Create server
   16:         server = new HttpSelfHostServer(config);
   17:  
   18:         // Start listening
   19:         server.OpenAsync().Wait();
   20:  
   21:         Console.WriteLine("Hit ENTER to exit...");
   22:         Console.ReadLine();
   23:  
   24:     }
   25:     finally
   26:     {
   27:         if (server != null)
   28:         {
   29:             // Stop listening
   30:             server.CloseAsync().Wait();
   31:         }
   32:     }
   33: }

Note: In order to successfully start the selfhost server you have to run as admin (or configure http.sys with the appropriate URI prefix using netsh).

Trying out the Controller

When running the controller can be accessed using any HTTP client. In the full sample I show how to use HttpClient to do GET and POST but Fiddler is often very useful for trying out various combinations of GET, PUT, POST, and DELETE by using the Composer tab manually to create requests. For example, you can create a POST request like this to insert a new Contact and then hit Execute:

AddContact

Similarly you can create a GET request to ask for the two first entries which will yield a result like this where the response body contains the first two Contacts:

GetTopContactsResult

As mentioned above, when you modify the data you can track the contents of the MongoDB database using MongoVUE.

Have fun!

Henrik

del.icio.us Tags: asp.net,webapi,mvc,rest,httpclient,mongodb

Comments

  • Anonymous
    February 19, 2012
    Some questions:
  1. Can you have a default $top value? Returning ALL the documents in the database seems like a very bad idea.
  2. Can you return a more web friendly viewmodel instead of the actual database object and still use IQueryable<>? In most cases you have a lot of data in the documents that you don't wan't to expose to the api.
  • Anonymous
    February 19, 2012
  1. There's no out-of-the-box way to have a default $top value, but it's fairly easy to implement with an action filter (see code below). And on the operation you want the default $top value, you'd apply something like [DefaultQueryCompValues(DefaultTop = 10)]
  2. You can always create some data transfer objects (DTOs) and map the values from the DB to the DTO prior to returning them (_contacts.GetAllContacts().Select(c => ToDTO(c)).AsQueryable())    public class DefaultQueryCompValuesAttribute : ActionFilterAttribute    {        private int defaultTop = -1;        public int DefaultTop        {            get { return this.defaultTop; }            set { this.defaultTop = value; }        }        public override void OnActionExecuting(HttpActionContext actionContext)        {            if (this.DefaultTop >= 0)            {                HttpRequestMessage request = actionContext.Request;                NameValueCollection queryParams = HttpUtility.ParseQueryString(request.RequestUri.Query);                if (string.IsNullOrEmpty(queryParams.Get("$top")))                {                    UriBuilder uriBuilder = new UriBuilder(request.RequestUri);                    if (string.IsNullOrEmpty(uriBuilder.Query))                    {                        uriBuilder.Query = "$top=" + this.DefaultTop;                    }                    else                    {                        uriBuilder.Query = uriBuilder.Query + "&$top=" + this.DefaultTop;                    }                    request.RequestUri = uriBuilder.Uri;                }            }        }    }
  • Anonymous
    February 20, 2012
    Hi Henrik, I'm currently trying out your tutorial, looks great so far. I'm wondering how to go about authenticating calls to the API. I understand how this is done using forms, but here I'm not sure what the best approach is. Would you need to set up a secure connection using https and then pass the username/password on each call? Thanks

  • Anonymous
    February 20, 2012
    Nice job Henrik... You've provided a nice simple example that follows real-world needs as opposed to so many of Microsoft's "samples" which don't provide any level of separation of concerns.

  • Anonymous
    February 24, 2012
    return _contacts.GetAllContacts().AsQueryable(); Doesn't that pull down ALL contacts?  I don't think AsQueryable does what you think it does.

  • Anonymous
    February 25, 2012
    There is cool alternative REST-framework at restservices.codeplex.com.

  • Anonymous
    February 25, 2012
    Pulling all the docs to support AsQueryable is not going to scale! Mongo isn't good at paging requests, but at the very least I'd recommend using fluent mongo here as it exposes a Queryable<> that maps to native mongo commands for skip and take.

  • Anonymous
    March 02, 2012
    Why is Id string and not an ObjectId in the controller methods?  I have been trying this with Custom Model Binders and I can't get it to work so I am wondering whether there is some functionality I am missing.  Works great with MVC.

  • Anonymous
    October 12, 2012
    Have you tried Couchbase Server?

  • Anonymous
    February 03, 2014
    The comment has been removed

  • Anonymous
    February 04, 2014
    Hi, Thanks for the article, very good for starters like me. however, in order to remove an entry, I had to use  Query.EQ("_id", new ObjectId(id)) instead of Query.EQ("_id", id). Otherwise it returned DocumentsAffected == 0 Any idea ?? Greg

  • Anonymous
    February 27, 2014
    This seems to actual let MongoDb handle the query and not return the full set to the app       [Queryable]        public IQueryable<Form> Get()        {            return _collection.AsQueryable();        }

  • Anonymous
    January 15, 2015
    Any further thought to write WebAPi and MongoDB articles? especially best practices

  • Anonymous
    March 26, 2015
    Wow!!! Very good post. I am really so happy to read your post. I have no idea about MongoDB with ASP.NET Web API. I only knew about asp.net to read myasp.net . But now getting little knowledge. your post is so quality, so always read your every post. thanks to share.