[转]a-mongodb-tutorial-using-c-and-asp-net-mvc

本文转自:http://www.joe-stevens.com/2011/10/02/a-mongodb-tutorial-using-c-and-asp-net-mvc/

 

In this post I’m going to create a simple ASP.NET MVC website for a simple blog that uses MongoDB and the offical 10gen C# driver.

MongoDB is no NOSQL database that stores information as Binary JSON (BSON) in documents. I have been working with it now for around 6 months on an enterprise application and so far am loving it. Our application is currently in alpha phase but should be public early next year! If you are used to working with an RDBMS, it takes a little bit of getting used to as generally you work with a denormalized schema. This means thinking about things quite differently to how you would previously; you’re going to have repeating data which is a no-no in a relational database, but it’s going to give you awesome performance, sure you may need an offline process that runs nightly and goes and cleans up your data, but for the real time performance gains it’s worth it.

 

Download source

Our reasons for choosing MongoDB were performance and scalability. The application is dealing with a lot of data where a single page load would require many joins and become a very expesive query. Sure we could cache the result, but we really want our data in real time, and MongoDB allowed us to do this.

Another reason was scalibility; MongoDB supports automatic sharding, where once set up your data can be scaled horizontally across multiple machines (we’re hosting on Amazon EC2), so for a very large collection (table), you can split the data based on a key so that when making your query MongoDB knows which machine your data is stored on and can go straight there.

Replica Sets is also an important feature for us to manage redundancy and failover. We can have multiple MongoDB instances running which essentially mirror each other. If one node goes down another one takes over and the application continues to perform.

MongoDB is also the only NOSQL database I’m aware of that has commerical support from its creators, 10gen.

Anyway, on with the tutorial… I’d suggest reading the documentation on the MongoDB website and also the books by Kristina Chodorow. Also if you want a visual representation of your data I’d suggest having a look at MongoVue.

The first step is to get MongoDB installed on your machine, follow the quickstart guides on the MongoDB website. If you’re running windows, which you probably are as this blog is primarily about Microsoft technologies, you may also want to install MongoDB as a windows service.

Okay so once it’s installed lets fire up Visual Studio and create a new ASP.NET MVC 3 web project. The first thing we want to do is add a reference to the 10gen C# driver which you can do via nuget. Right click on the libaries folder under the web project and choose Add Libary Package Reference, then search online for ‘mongo’ and add a reference to the offical 10gen driver.

As mentioned MongoDB stores its data in documents. A simple C# POCO can be serialized as a document and stored by MongoDB. Documents can also contain other embedded documents or arrays of documents. Lets start by looking at my Post object that I will use for this blog tutorial. I have added a new class library to my soultion called Core where I will put my domain objects and services.

?
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
public class Post
{
     [ScaffoldColumn( false )]
     [BsonId]
     public ObjectId PostId { get ; set ; }
 
     [ScaffoldColumn( false )]
     public DateTime Date { get ; set ; }
 
     [Required]
     public string Title { get ; set ; }
 
     [ScaffoldColumn( false )]
     public string Url { get ; set ; }
 
     [Required]
     public string Summary { get ; set ; }
 
     [UIHint( "WYSIWYG" )]
     [AllowHtml]
     public string Details { get ; set ; }
 
     [ScaffoldColumn( false )]
     public string Author { get ; set ; }
 
     [ScaffoldColumn( false )]
     public int TotalComments { get ; set ; }
 
     [ScaffoldColumn( false )]
     public IList<Comment> Comments { get ; set ; }
}

As you can see above I’m also going to be using this domain model directly in my MVC views. Normally you’d want to separate your domain model and view model and use something like AutoMapper to map between then, but for simplicity in this tutorial I’m just going to use my domain model.

All in all a pretty simple object, but you’ll notice my PostId property has a type of ObjectId. Most collections in MongoDB have a unique identifier which is stored in the field ‘_id’. A unique index is automatically created on this field which cannot be removed. MongoDB has a special BSON type called ObjectId which is a 12-byte values made up of a time stamp, the machine id, the process id and a sequence number. This should give a unique Id that can be used on your documents. If I named my property ‘Id’ it would automatically become the ‘_id’ element in the document, but as I decided to call it ‘PostId’ I must specify it’s the Id by using the BsonId attribute. You don’t have to use the ObjectId type, but you do need to ensure that whatever you choose to use is unique. In the above example I could have used Url as my ‘_id’ field by adding the BsonId attribute to that field. It’s worth noting that when serialized the field names will match the POCO properties except for the PostId which will actually be stored as ‘_id’.

So now I have my document I want to be able to store it. To do this I need to create a connection to my MongoDB server, then choose my database and collection I want the document to belong to. Using the C# driver I can do this with the following code.

?
1
2
3
var server = MongoServer.Create( "mongodb://127.0.0.1" );
var db = server.GetDatabase( "blog" );
var collection = db.GetCollection<Post>( "post" );

First I create an instance of the MongoServer object using a MongoDB connection string for my local instance of the server. Second I get an instance of MongoDatabase for my blog database from the server. Lastly I get the MongoCollection object for the collection I want to use. MongoCollection is the object you use to insert, update and query that collection. I’m using the generic version of MongoCollection which speficies the domain object y0u will using for the document.

If the database or collection do not exist, then they will be created for you automatically.

The C# driver also has a class called MongoConnectionStringHelper that you can use to easily get the connection string from your web.config file. I could add my connection string as follows.

?
1
2
3
< connectionStrings >
   < add name = "MongoDB" connectionString = "server=127.0.0.1;database=blog" />
</ connectionStrings >

I can then change my code to look like this.

?
1
2
3
4
5
var con = new MongoConnectionStringBuilder(ConfigurationManager.ConnectionStrings[ "MongoDB" ].ConnectionString);
 
var server = MongoServer.Create(con);
var db = server.GetDatabase(con.DatabaseName);
var collection = db.GetCollection<Post>( "post" );

Now the server and database name are coming from the web.config and can be changed at any time without having to touch the code.

In my Core project I have created a PostService class which has the following Create method.

?
1
2
3
4
5
6
7
8
9
10
11
12
13
public void Create(Post post)
{
     var con = new MongoConnectionStringBuilder(
         ConfigurationManager.ConnectionStrings[ "MongoDB" ].ConnectionString);
 
     var server = MongoServer.Create(con);
     var db = server.GetDatabase(con.DatabaseName);
     var collection = db.GetCollection<Post>( "post" );
 
     post.Comments = new List<Comment>();
 
     collection.Save(post);
}

The method accepts an instance of my post object as a parameter that I will save to MongoDB. I can do that easily by calling the Save method of MongoCollection passing it the object. MongoCollection also has Insert and Update methods which Save calls internally depending on the value of the _id field.

The only other thing I’m doing in this method is initializing my Comments property as a new list, which will create an empty array in MongoDB. Later I’ll explain how to push comment objects into this array, but if I didn’t initialize it the value would be null in the document and I couldn’t push to it.

The connection logic is simple, but I don’t want to have to repeat it in every service method, so I like to create a generic helper that wraps it up. Below is a class that does that called MongoHelper.

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MongoHelper<T> where T : class
{
     public MongoCollection<T> Collection { get ; private set ; }
 
     public MongoHelper()
     {
         var con = new MongoConnectionStringBuilder(
             ConfigurationManager.ConnectionStrings[ "MongoDB" ].ConnectionString);
 
         var server = MongoServer.Create(con);
         var db = server.GetDatabase(con.DatabaseName);
         Collection = db.GetCollection<T>( typeof (T).Name.ToLower());
     }
}

The helper can be used by giving the domain object type, which it also uses to derive the collection name. I’m using ToLower as I like my collection names to be lower case. I can then put the following variable and constructor in my PostService.

?
1
2
3
4
5
6
private readonly MongoHelper<Post> _posts;
 
public PostService()
{
     _posts = new MongoHelper<Post>();
}

My Create method is now simplified to this.

?
1
2
3
4
5
public void Create(Post post)
{
     post.Comments = new List<Comment>();
     _posts.Collection.Save(post);
}

For the rest of the tutorial I will use this helper in my services. Next I need a page to add new blog posts. In the MVC project I’ve added a PostController with a Create method.

?
1
2
3
4
5
[HttpGet]
public ActionResult Create()
{
     return View( new Post());
}

This is just rendering a simple view shown below, and passing through a new instance of the Post object.

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@model Core.Domain.Post
 
@{
     ViewBag.Title = "Create";
}
 
< h2 >Create</ h2 >
 
@using (Html.BeginForm())
{
     @Html.EditorForModel()
 
     < p >
         < input type = "submit" value = "Create" />
     </ p >
}

This gives me a form to create a new Post that looks like this.

The WYSIWYG editor appears due to the UIHint attribute in my Post object. I have an editor template that renders a text area with a specific class which I then use in my layout page to hook up to CKEditor.

My post action for creating looks like this.

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[HttpPost]
public ActionResult Create(Post post)
{
     if (ModelState.IsValid)
     {
         post.Url = post.Title.GenerateSlug();
         post.Author = User.Identity.Name;
         post.Date = DateTime.Now;
 
         _postService.Create(post);
 
         return RedirectToAction( "Index" );
     }
 
     return View();
}

Here I set the Url, Author and Date properties which are not set by the form, then call Create on my PostService. GenerateSlug is an extension method that I got from here to slugify my title which I’ll use in routing to display the post.

Now I have successfully saved a document with MongoDB, easy!

So the next step would be display a list of posts which can be done with the following service method.

?
1
2
3
4
5
6
7
public IList<Post> GetPosts()
{
     return _posts.Collection.FindAll()
         .SetFields(Fields.Exclude( "Comments" ))
         .SetSortOrder(SortBy.Descending( "Date" ))
         .ToList();
}

Here I am using the FindAll method of MongoCollection which returns all documents in the collection. It’s probably not wise to use this method without paging in a production environment as it could be a very expensive query. I am also using the SetFields method which can be used to either include or exlude fields, which is analogous to choosing fields in a SELECT statement in SQL.

I am excluding the Comments array as this service method is used for listing posts where I don’t care about the comments, I’ll show those on the detail page. When writing queries it’s always worth thinking about how you are going to use the data so you can make choices on structuring them to give maximum performance. I am also using SetSortOrder which as it suggests allows you to choose which field the documents returned are ordered by. I’m sorting by Date descending so that the latest Posts will be at the top.

Now back to MVC. I’m going to display my posts on my index page so my action will look like this.

?
1
2
3
4
public ActionResult Index()
{
     return View(_postService.GetPosts());
}

And my view like this.

?
1
2
3
4
5
6
7
8
9
10
11
12
13
@model IList< Core.Domain.Post >
 
@{
     ViewBag.Title = "Index";
}
 
< h2 >Posts</ h2 >
 
< p >
     @Html.ActionLink("New Post", "Create")
</ p >
 
@Html.DisplayForModel()

I’ve added a link to my page to create new posts and then used DisplayForModel to show my display template that looks like this.

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@model Core.Domain.Post
 
< div >
     < h3 >@Html.ActionLink(Model.Title, "Detail", new { id = Model.Url })</ h3 >
 
     < p >
         < em >Posted at @Model.Date.ToLocalTime().ToString() by @Model.Author</ em >
     </ p >
 
     < p >
         @Model.Summary
     </ p >
 
     < p >
         (@Model.TotalComments comments)
     </ p >
 
     < p >
         @Html.ActionLink("Delete Post", "Delete", new { id = Model.PostId })
         |
         @Html.ActionLink("Edit Post", "Update", new { id = Model.Url })
     </ p >
</ div >

The model for my view was a list of Posts, so this display template is repeated for each item. It shows a summary of the post as well as having links to the full post when clicking the title and links for delete and edit which I’ll implement next. My list of posts now looks like this.

For editing a post, if you’re used to Linq2Sql or Entity Framework you may do something like this.

?
1
2
3
4
5
6
7
8
9
10
public void Edit(Post post)
{
     var originalPost = GetPost(post.PostId);
     originalPost.Title = post.Title;
     originalPost.Url = post.Url;
     originalPost.Summary = post.Summary;
     originalPost.Details = post.Details;
 
     _posts.Collection.Save(originalPost);
}

Here I’m passing an updated Post object into the method, I query the database to get the most up to date document, update some of the properties to the new values, then save the updated document back to the database. In doing this the database is being hit twice. Another way to update MongoDB in one query is like this:

?
1
2
3
4
5
6
7
8
9
public void Edit(Post post)
{
     _posts.Collection.Update(
         Query.EQ( "_id" , post.PostId),
         Update.Set( "Title" , post.Title)
             .Set( "Url" , post.Url)
             .Set( "Summary" , post.Summary)
             .Set( "Details" , post.Details));
}

Here I’m using the Update method of MongoCollection. The first argument is a MongoDB query which allows you to specify what documents should be matched. With the C# driver most operations can be done using the query builder as above. I’m using Query.EQ which matches the given value against the specified field. The second argument is an update document which can also be created using the Update builder object. This objects wraps up the various MongoDB update methods. Above I am using Update.Set which simply sets a new value for the given field. Each update method returns another instance of the UpdateBuilder object so you can chain different methods together.

If you want to update more than one document you need to use one of the overloads that takes the UpdateFlags enum and use UpdateFlags.Multi, otherwise it will it will only update the first document matched by the query.

MongoDB also supports the FindAndModify command which the C# driver wraps up with a FindAndModify method on MongoCollection. This command allows you to find a document, update it, then return the document either updated or before the update, in a single operation. My edit service method could be amended to return the updated post like so.

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public Post Edit(Post post)
{
     var updatedPost =
         _posts.Collection.FindAndModify(
             Query.EQ( "_id" , post.PostId),
             null ,
             Update.Set( "Title" , post.Title)
                 .Set( "Url" , post.Url)
                 .Set( "Summary" , post.Summary)
                 .Set( "Details" , post.Details),
             true ).GetModifiedDocumentAs<Post>();
 
     return updatedPost;
}

FindAndModify returns a FindAndModiftyResult which has a ModifiedDocument which is a BsonDocument. Above I’m using the GetModifiedDocumentAs method which deserializes the BsonDocument to the correct type. By choosing the overload with returnNew and setting it to true I get the updated document, otherwise it would be the document before it was updated.

Another cool feature of MongoDB is upserts. This is where if the document exists it is updated, otherwise a new document is inserted. Upserts can be done using the FindAndModify method and using the overload with the upsert argument and setting it to true.

Getting back to MVC again; the action methods for my edit page looks like this.

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[HttpGet]
public ActionResult Update( string id)
{
     return View(_postService.GetPost(id));
}
 
[HttpPost]
public ActionResult Update(Post post)
{
     if (ModelState.IsValid)
     {
         post.Url = post.Title.GenerateSlug();
 
         _postService.Edit(post);
 
         return RedirectToAction( "Index" );
     }
 
     return View();
}

All pretty simple; I’m generating the slug from the title again incase it was updated. The view is also very simple.

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@model Core.Domain.Post
 
@{
     ViewBag.Title = "Update";
}
 
< h2 >Update</ h2 >
 
@using (Html.BeginForm())
{
     @Html.HiddenFor(m => m.PostId);
     @Html.EditorForModel()
 
     < p >
         < input type = "submit" value = "Update" />
     </ p >
}

I then get an edit page that looks very similar to the create post page.

My list of posts also has a link to delete a post, so let’s looks at the service method for that.

?
1
2
3
4
public void Delete(ObjectId postId)
{
     _posts.Collection.Remove(Query.EQ( "_id" , postId));
}

It takes the ID of the post to delete and uses the Remove method of MongoCollection with a query that matches the ID. When deleting I’m redirecting to a page to allow the user to confirm the delete. The action methods used look like this.

?
1
2
3
4
5
6
7
8
9
10
11
12
13
[HttpGet]
public ActionResult Delete(ObjectId id)
{
     return View(_postService.GetPost(id));
}
 
[HttpPost, ActionName( "Delete" )]
public ActionResult ConfirmDelete(ObjectId id)
{
     _postService.Delete(id);
 
     return RedirectToAction( "Index" );
}

And the view.

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@model Core.Domain.Post
 
@{
     ViewBag.Title = "Delete";
}
 
< h2 >Delete</ h2 >
 
< p >
     Are you sure you want to delete this post?
</ p >
 
< h2 >@Model.Title</ h2 >
 
@using (Html.BeginForm())
{
     < input type = "submit" value = "Delete" />
}
 
@Html.ActionLink("Back to posts", "Index")

Next let’s do the detail page that displays the full post. On this page I’m going to allow adding comments as well as displaying the current comments and loading more via ajax. Here is the service method that gets a single post via the Url.

?
1
2
3
4
5
6
public Post GetPost( string url)
{
     var post = _posts.Collection.Find(Query.EQ( "Url" , url)).SetFields(Fields.Slice( "Comments" , -5)).Single();
     post.Comments = post.Comments.OrderByDescending(c => c.Date).ToList();
     return post;
}

In this method I use the Find method of MongoCollection which takes a query object just like the Update method used previously. Here I’m matching the Url field against the url parameter passed into the service method.

The for the full post I want to see comments, but I only want the latest 5, and I’ll load the rest via ajax. To do this I’m using the SetFields method again, but this time using Fields.Slice which returns a subset of the documents in an array. There are two overloads of the Slice method, one allowing to select first or last x number of documents by using a positive or negative value, and the other allowing you to do paging with skip/limit.

I’m getting the last 5 comments, but I want them ordered with the latest comment as the top; at the beginning of my list. For this reason I’m sorting the comments descending by date. I’m doing this after querying MongoDB as at the time of writing this article there is no way to sort documents in a nested array using MongoDB.

The action method to display my detail page looks like this.

?
1
2
3
4
5
6
7
8
[HttpGet]
public ActionResult Detail( string id)
{
     var post = _postService.GetPost(id);
     ViewBag.PostId = post.PostId;
 
     return View(post);
}

You can see I’m adding the PostId to the ViewBag; I’ll use this later when I add a comment so that I know which post the comment is against. The view for the detail page looks like this.

?
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
@model Core.Domain.Post
 
@{
     ViewBag.Title = "Detail" ;
}
 
<h2>@Model.Title</h2>
 
<p>
     <em>Posted at @Model.Date.ToLocalTime().ToString() by @Model.Author</em>
</p>
 
<p>
     @Html.Raw(Model.Details)
</p>
 
<div id= "add-comment" >
     @Html.Partial( "AddComment" , new Core.Domain.Comment())
</div>
 
<h3>Comments</h3>
<div id= "comment-list" >
     @ if (Model.Comments != null )
     {
         Html.RenderPartial( "CommentList" , Model.Comments);
     }
</div>

I’m using two partials in this view, one to allow adding a new comment and one to display the list of comments for the post. Below is the comment domain object.

?
1
2
3
4
5
6
7
8
9
10
11
12
public class Comment
{
     [BsonId]
     public ObjectId CommentId { get ; set ; }
 
     public DateTime Date { get ; set ; }
 
     public string Author { get ; set ; }
 
     [Required]
     public string Detail { get ; set ; }
}

The object contains the date the comment was made, the person who made it, and the comment itself. When adding a comment I create a new instance of the Comment object and pass that as the model to the partial. In doing this I can utilise the validation attributes in the model.

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@model Core.Domain.Comment
 
@using (Html.BeginForm("AddComment", "Post", FormMethod.Post))
{
     < input name = "postId" type = "hidden" value = "@ViewBag.PostId" />
     < div >
         Add Comment
         < br />
         @Html.TextAreaFor(m => m.Detail)
         @Html.ValidationMessageFor(m => m.Detail)
         < br />
         < input type = "submit" value = "Add Comment" />
     </ div >
}

You can see I’m putting the PostId into a hidden input field which will then be posted with the  rest of the form data allowing me to store the comment against the correct post. The service method to add a comment is shown below.

?
1
2
3
4
5
public void AddComment(ObjectId postId, Comment comment)
{
     _posts.Collection.Update(Query.EQ( "_id" , postId),
         Update.PushWrapped( "Comments" , comment).Inc( "TotalComments" , 1));
}

When I did the update query for a post I used Update.Set to set a particular field. Here I am using Update.PushWrapped. In MongoDB push appends to an exisiting array within a document (remember when creating the post I initialized the comment collection to be a new list). The C# driver has the helper methods Push, which pushes a BsonDocument, or PushWrapped, which allows you to push an instance of the object used when instanciating the MongoCollection. Here I am adding the new comment object to the Comments field on the post, which is an array. Another useful function for dealing with arrays in AddToSet which only adds the document to the array if it doesn’t already exist.

In the same update I’m also using Inc which increments a number field; here the total number of comments the post has. To decrement you can use a negative value. I could perform a count on the number of documents in the post collection, but this could have a negative effect on performance. If I had millions of documents in the collection it would have to touch each one to work out the count. Performance wise it’s better to maintain the count in a field yourself, and just query that.

To submit the comment, I’m going to hijax the form submission and perform the addition using an ajax call. Due to this my action method for adding a comment returns JSON data. Normally if you want to return the rendered HTML for a partial you can use the PartialViewResult, but here if the comment textbox is empty I need to return an error; I can do this easily by rending the AddComment partial again using the invalid comment object as it’s model.

?
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
[HttpPost]
public ActionResult AddComment(ObjectId postId, Comment comment)
{
     if (ModelState.IsValid)
     {
         var newComment = new Comment()
                                 {
                                     CommentId = ObjectId.GenerateNewId(),
                                     Author = User.Identity.Name,
                                     Date = DateTime.Now,
                                     Detail = comment.Detail
                                 };
 
         _commentService.AddComment(postId, newComment);
 
         ViewBag.PostId = postId;
         return Json(
             new
                 {
                     Result = "ok" ,
                     CommentHtml = RenderPartialViewToString( "Comment" , newComment),
                     FormHtml = RenderPartialViewToString( "AddComment" , new Comment())
                 });
     }
 
     ViewBag.PostId = postId;
     return Json(
         new
             {
                 Result = "fail" ,
                 FormHtml = RenderPartialViewToString( "AddComment" , comment)
             });
}

As you can see if the model state is valid I create a new comment object with the current user and date and save it to MongoDB. I put the PostId in ViewBag again so it’s ready for another comment to be added, then I return a JsonResult containing a string stating everything was ‘ok’, the HTML from the Comment partial view rendered against the new comment and the HTML from the AddComment partial for a new comment. In the callback for my ajax call I check the result string, and if everything is okay I replace the add comment form and append the new comment to the list of comments.

If the model state is invalid the JsonResult contains the result string ‘fail’ as well as the rendered HTML from the AddComment partial, but using the comment object which has an invalid state, which will therefore render the required error message due to the required validation attribute on the model.

The RenderPartialViewToString method came from here and renders a partial to string using the Razor view engine.

Here is the jQuery that hijaxes the form and updates the DOM.

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$( "form[action$='AddComment']" ).live( "submit" , function () {
     $.post(
         $( this ).attr( "action" ),
         $( this ).serialize(),
         function (response) {
             if (response.Result == "ok" ) {
                 $(response.CommentHtml).hide().prependTo( "#comment-list" ).fadeIn(1000);
                 $( "#add-comment" ).html(response.FormHtml);
                 $( "#Detail" ).val( "" );
             }
             else {
                 $( "#add-comment" ).html(response.FormHtml);
             }
         });
     return false ;
});

And now I can add comments to my posts using ajax.

Here is the partial view that’s used for rendering a comment.

?
1
2
3
4
5
@model Core.Domain.Comment
< div >< strong >By @Model.Author at @Model.Date.ToLocalTime().ToString()</ strong >
(< a class = "remove-comment" href = "javascript:void()" data-id = "@Model.CommentId" >Remove</ a >)@Model.Detail
 
</ div >

You can see that it has a remove link to delete a comment. Lets look at the service method used to do this.

?
1
2
3
4
5
public void RemoveComment(ObjectId postId, ObjectId commentId)
{
     _posts.Collection.Update(Query.EQ( "_id" , postId),
         Update.Pull( "Comments" , Query.EQ( "_id" , commentId)).Inc( "TotalComments" , -1));
}

The method is similar to adding a comment but I use Update.Pull which will remove matching documents from the Comments array. Update.Pull allows you to use a mongo query to select the documents to remove, or you can use Update.PullWrapped if you have an instance of the object you want to remove. Im also decrementing the TotalComments field using the Inc method with a negative number.

I will also use ajax to remove a comment from MongoDB and the DOM. My action method looks like this.

?
1
2
3
4
5
public ActionResult RemoveComment(ObjectId postId, ObjectId commentId)
{
_commentService.RemoveComment(postId, commentId);
return new EmptyResult();
}

As this is just a tutorial, I’m being a bit lazy and returning an EmptyResult. If I had proper error handling in place I’d probably want to return a JsonResult that indicates whether the remove was successful or not.

The jQuery to call this action and remove the comment from the DOM looks like this.

?
1
2
3
4
5
6
7
8
9
10
$(".remove-comment").live("click", function () {
     var comment = $(this).parent();
     $.post(
         '@Url.Action("RemoveComment")',
         { postId : '@Model.PostId', commentId : $(this).data("id") },
         function () {
             comment.fadeOut(1000, function() { $(this).remove(); });
         }
     );
});

The only part left to do on this page is to load more comments via an ajax call. If you remember when I initially load the post for the detail page I only load the latest 5 comments, so I want to be able to load more and add them to do the DOM. This is quite straight forward using SetFields and Slice with skip/limit as I mentioned earlier.

The only tricky part is that new documents may have been added to the end of the array since the page loaded which could result in duplicate comments being displayed. Let’s say I have 20 comments in my array, and I want the latest documents first, I use -5 for the limit on the initial slice to give me comments 16, 17, 18, 19 and 20. For the next page I’d want to use -5 for skip and -5 for limit to get the next set of 5 comments. This should give me 11, 12, 13, 14 and 15, but what happens if since loading the page somebody else added a comment to the same post? When I go to get the next page there would be 21 comments in total, and my query doing a skip -5 limit -5 would actually return me comments 12, 13, 14, 15 and 16, so comment 16 would be returned twice.

To resolve this I put the total number of comments in the ViewBag when rendering the page, then when loading the next page of comments I compare this against the actual number of comments at that point and adjust the skip with the offset. This does mean that to load a page of comments I hit MongoDB twice, but both calls are very small so it won’t really add much overhead.

My Detail action method has been amended to look like this.

?
1
2
3
4
5
6
7
8
9
10
11
[HttpGet]
public ActionResult Detail( string id)
{
     var post = _postService.GetPost(id);
     ViewBag.PostId = post.PostId;
 
     ViewBag.TotalComments = post.TotalComments;
     ViewBag.LoadedComments = 5;
 
     return View(post);
}

As you can see the TotalComments is added to ViewBag, as well as the number of comments I currently have loaded which I will use for the initial skip value.

My comments get rendered by the CommentList partial view that looks like this.

?
1
2
3
4
5
6
7
8
9
10
11
12
13
@model IList< Core.Domain.Comment >
 
@foreach (var comment in Model)
{
     Html.RenderPartial("Comment", comment);
}
 
@if (ViewBag.TotalComments > ViewBag.LoadedComments)
{
     < div style = "margin-bottom: 20px;" >
         < a id = "load-more" data-loadedComments = "@ViewBag.LoadedComments" href = "javascript:void(0)" >Load more...</ a >
     </ div >
}

The view renders each comment, then if the number of TotalComments is greater than the LoadedComments, ie. there are more comments to load, it renders a ‘Load more’ link which will load more comments using ajax. Here I’m using a data attribute in the anchor tag which contains the number of loaded comments. This is so I can easily get this number from my jQuery method to load the  next page.

?
1
2
3
4
5
6
7
8
9
$( "#load-more" ).live( "click" , function () {
     $.post(
         '@Url.Action("CommentList")' ,
         { postId: '@Model.PostId' , skip : $( this ).data( "loadedComments" ), limit : 5, totalComments: @ViewBag.TotalComments },
         function (response) {
             $( "#comment-list" ).find( "#load-more" ).parent().replaceWith($(response).fadeIn(1000));
         }
     );
});

The method above calls an action, CommentList passing through the PostId, the number of comments current loaded, the limit of 5, which is how many comments I want per page, and the total number of comments from when the page was first rendered. The action method returns HTML rendered using the CommentList partial used above which gets added to the DOM, replacing the load more link.

?
1
2
3
4
5
6
[HttpPost] public ActionResult CommentList(ObjectId postId, int skip, int limit, int totalComments)
{
     ViewBag.TotalComments = totalComments;
     ViewBag.LoadedComments = skip + limit;
     return PartialView(_commentService.GetComments(postId, ViewBag.LoadedComments, limit, totalComments));
}

I need to put the TotalComments in ViewBag again, as well as the  new value for LoadedComments. I then return a PartialViewResult from the result of the GetComments service method.

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public IList<Comment> GetComments(ObjectId postId, int skip, int limit, int totalComments)
{
     var newComments = GetTotalComments(postId) - totalComments;
     skip += newComments;
 
     var post = _posts.Collection.Find(Query.EQ( "_id" , postId)).SetFields(Fields.Exclude( "Date" , "Title" , "Url" , "Summary" , "Details" , "Author" , "TotalComments" ).Slice( "Comments" , -skip, limit)).Single();
     return post.Comments.OrderByDescending(c => c.Date).ToList();
}
 
public int GetTotalComments(ObjectId postId)
{
     var post = _posts.Collection.Find(Query.EQ( "_id" , postId)).SetFields(Fields.Include( "TotalComments" )).Single();
     return post.TotalComments;
}

The GetComments method first calls GetTotalComments to work out of any new comments have been added and adjusts the skip parameter accordingly. It then queries the post collection excluding all fields except the Comments array which it then performs a Slice on to get the next page of comments. Again I then reorder those comments at the client before returning them.

I think that’s about it for this tutorial; there is a  lot more I want to write about with MongoDB but this post seems to have got fairly large, so I think the other subjects will have to go into seperate posts. At least I’ve actually finished this post which I started about a month ago; keeping my blog up to date is getting increasingly hard with a 6 month old baby!

I hope this helps follow Microsoft developers who are looking into MongoDB!

Download source

转载于:https://www.cnblogs.com/freeliver54/p/7079292.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值