博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
[转]a-mongodb-tutorial-using-c-and-asp-net-mvc
阅读量:5876 次
发布时间:2019-06-19

本文共 32708 字,大约阅读时间需要 109 分钟。

本文转自:

 

In this post I’m going to create a simple ASP.NET MVC website for a simple blog that uses and the offical .

MongoDB is no NOSQL database that stores information as (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.

 

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 , 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.

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, .

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

The first step is to get MongoDB installed on your machine, follow the 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 .

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 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 . 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 . 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 object using a for my local instance of the server. Second I get an instance of for my blog database from the server. Lastly I get the 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 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 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 .

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 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 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 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 . 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 command which the C# driver wraps up with a 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 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 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 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 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 which only adds the document to the array if it doesn’t already exist.

In the same update I’m also using 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 , 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 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 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 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 . 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!

转载地址:http://oxzix.baihongyu.com/

你可能感兴趣的文章
使用 maven 自动将源码打包并发布
查看>>
Spark:求出分组内的TopN
查看>>
Python爬取豆瓣《复仇者联盟3》评论并生成乖萌的格鲁特
查看>>
关于跨DB增量(增、改)同步两张表的数据小技巧
查看>>
飞秋无法显示局域网好友
查看>>
学员会诊之03:你那惨不忍睹的三层架构
查看>>
vue-04-组件
查看>>
Golang协程与通道整理
查看>>
解决win7远程桌面连接时发生身份验证错误的方法
查看>>
C/C++ 多线程机制
查看>>
js - object.assign 以及浅、深拷贝
查看>>
python mysql Connect Pool mysql连接池 (201
查看>>
Boost在vs2010下的配置
查看>>
android camera(四):camera 驱动 GT2005
查看>>
一起谈.NET技术,ASP.NET伪静态的实现及伪静态的意义
查看>>
20款绝佳的HTML5应用程序示例
查看>>
string::c_str()、string::c_data()及string与char *的正确转换
查看>>
11G数据的hive初测试
查看>>
如何使用Core Text计算一段文本绘制在屏幕上之后的高度
查看>>
==和equals区别
查看>>