TLDR :Full code is Running Sandbox is .
Writing a crappy, subpar clone of seems to have become the latest FizzBuzz fad.
In the last week, I've seen:
And the list goes. All of these implement simply implement posting and voting, and with the exception of the PHP one, don't use any form of persistance (The perl one might, I can't read it).
I wanted to play, but I also wanted to provide a little more features, as I don't believe any of those actually count as a "Reddit Clone". I decided on the following features:
- View Links ordered by score
- Submit Links
- Vote on links
- Comment on links in typical nested reddit fashion
- Vote on comments
- Order comments by score
- Use a real database as persistance
These features make my project a bit more ambitious than the others, but also makes it a nice technical showcase for how concise a program can be.
Since Java was already done, I decided to use C# and ASP.NET MVC as the language and framework for my clone.
The first order of busines is building the data model. Because I am going for simplicity I am going to define the two tables I need using C# classes and Linq to SQL attributes. Using this Linq to SQL will generate the database for me the first time the application is ran.
My model is simple:
The submissions class:
1 [Table(Name = "Submissions")]
2 public class Submission
3 {
4 private EntitySet<Comment> _comments
5 = new EntitySet<Comment>();
6
7 public Submission()
8 {
9 this.Score = 1;
10 }
11
12 [Column
13 (IsPrimaryKey = true,
14 IsDbGenerated = true,
15 DbType = "INT NOT NULL IDENTITY")]
16 public int ID { get; set; }
17
18 [Column]
19 public string Title { get; set; }
20
21 [Column]
22 public string Link { get; set; }
23
24 [Column]
25 public int Score { get; set; }
26
27 [Association(Storage = "_comments", OtherKey = "SubmissionID")]
28 public EntitySet<Comment> Comments
29 {
30 get { return this._comments; }
31 set { this._comments.Assign(value); }
32 }
33 }
And the Comment class:
1 public class Comment
2 {
3 private EntitySet<Comment> _childComments
4 = new EntitySet<Comment>();
5
6 public Comment()
7 {
8 this.Score = 1;
9 }
10
11 [Column
12 (IsPrimaryKey = true,
13 IsDbGenerated = true,
14 DbType = "INT NOT NULL IDENTITY")]
15 public int ID { get; set; }
16
17 [Column]
18 public Nullable<int> ParentCommentID { get; set; }
19
20 [Column]
21 public int SubmissionID { get; set; }
22
23 [Column]
24 public string Text { get; set; }
25
26 [Column]
27 public int Score { get; set; }
28
29 [Association(Storage = "_childComments", OtherKey="ParentCommentID", ThisKey = "ID")]
30 public EntitySet<Comment> ChildComments
31 {
32 get { return this._childComments; }
33 set { this._childComments.Assign(value); }
34 }
35 }
With those two classes defined, all I need to do now is create a DataContext:
1 [Database(Name = "RedditClone")]
2 public class RedditModel : DataContext
3 {
4 private static string _connString =
5 ConfigurationManager.ConnectionStrings["RedditClone"].ConnectionString;
6
7 public RedditModel() : base(_connString)
8 {
9 if (!this.DatabaseExists())
10 this.CreateDatabase();
11 }
12
13 public Table<Submission> Submissions
14 {
15 get { return this.GetTable<Submission>();}
16 }
17
18 public Table<Comment> Comments
19 {
20 get { return this.GetTable<Comment>(); }
21 }
22 }
The first time a RedditModel class is instanciated, Linq to SQL will build a database with the following schema:
This approach works very well for a simple project, as it makes installation very simple. All you have to do is drop the files onto a server and browse to it and you have the full database built and ready.
At this point, I have a full persistance layer completely finished and have used roughly 50 lines of code...Not bad.
However, I can't do much with a persistance layer without a frontend. So I need to define some MVC Controllers to handle requests. My design calls for two controllers:
- SubmissionController
- CommentController
SubmissionController will perform the following tasks:
- List all submissions
- Load a single submission
- Submit a submission
- Vote (Up and Down) on submissions
CommentController will perform the following tasks:
- Create a commment
- Vote (Up and down) on comments
Because of how I structured the data model, I don't need a action to request comments, they lazily load when you access a submission.
I'll start with the "Index" action on SubmissionController. This action responds to GET requests to the Submissions controller and will return a list of submisssions ordered by score:
1 public ActionResult Index()
2 {
3 RedditModel model = new RedditModel();
4 return View(
5 "Index",
6 model.Submissions
7 .OrderByDescending(s => s.Score)
8 .ToList());
9 }
As you can see, the heavily lifting of data manipulation is easily abstracted into the model. All this actually has to do is grab the data and it needs and pass it to the view (Which I have yet to define).
The "View" action is equally simple:
1 public ActionResult View(int id)
2 {
3 RedditModel model = new RedditModel();
4 return View(
5 "Comments",
6 model.Submissions
7 .Where(s => s.ID == id)
8 .FirstOrDefault());
9 }
With these two actions defined, I have handled the read-only aspect of my Reddit Clone. However, I want the user to be able to submit new links, as well as vote. So I'll add three more actions:
1 [ValidateInput(false)]
2 [AcceptVerbs(HttpVerbs.Post)]
3 public ActionResult Submit(FormCollection collection)
4 {
5 RedditModel model = new RedditModel();
6 Submission newSub = new Submission();
7
8 UpdateModel(newSub, new[] { "Title", "Link" });
9
10 model.Submissions.InsertOnSubmit(newSub);
11 model.SubmitChanges();
12
13 return RedirectToAction("Index");
14 }
The "Submit" action responds to a POST verb and creates a new submission. Due to the nice MVC UpdateModel method, it is very easy to map a POST to my model class.
Finally, I need to add voting. To do this I define a "VoteUp" action and a "VoteDown" action. Both of these call a private method that adjusts the vote count for the submission:
1 public ActionResult VoteUp(int id)
2 {
3 vote(id, 1);
4 return RedirectToAction("Index");
5 }
6
7 public ActionResult VoteDown(int id)
8 {
9 vote(id, -1);
10 return RedirectToAction("Index");
11 }
12
13 private void vote(int id, int direction)
14 {
15 RedditModel model = new RedditModel();
16
17 Submission voteSub = model.Submissions.Where(s => s.ID == id).FirstOrDefault();
18 voteSub.Score += direction;
19
20 model.SubmitChanges();
21 }
With this done, I'm at about 100 lines of C#, and I have a more functional RedditClone than the other examples.
I still need to add the CommentsController, which is very similar to the SubmissionController:
1 public class CommentsController : Controller
2 {
3 [ValidateInput(false)]
4 [AcceptVerbs(HttpVerbs.Post)]
5 public ActionResult Create(FormCollection collection)
6 {
7 RedditModel model = new RedditModel();
8 Comment newComment = new Comment();
9
10 UpdateModel(newComment, new[] { "SubmissionID", "ParentCommentID", "Text" });
11
12 model.Comments.InsertOnSubmit(newComment);
13 model.SubmitChanges();
14
15 return RedirectToRoute(new { controller = "Submissions", action = "View", id = newComment.SubmissionID });
16 }
17
18 public ActionResult VoteUp(int id, int submissionID)
19 {
20 vote(id, 1);
21 return RedirectToRoute(new { controller = "Submissions", action = "View", id = submissionID });
22 }
23
24 public ActionResult VoteDown(int id, int submissionID)
25 {
26 vote(id, -1);
27 return RedirectToRoute(new { controller = "Submissions", action = "View", id = submissionID });
28 }
29
30 private void vote(int id, int direction)
31 {
32 RedditModel model = new RedditModel();
33
34 Comment voteComment = model.Comments.Where(c => c.ID == id).FirstOrDefault();
35 voteComment.Score += direction;
36
37 model.SubmitChanges();
38 }
39 }
This wraps up all of the code needed. All that is left is to create views that actually display the data.
I'm a fan of the opensource project Spark, and will use it instead of the conventional ASP.NET view engine. I find the syntax from Spark a lot easier to read and less "tag-soup" than the ASPX views.
Because I have two different views (All submissions, and comments for a submission), I'll implement a master layout that they both share. Spark makes this easy:
1 "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
2 "http://www.w3.org/1999/xhtml">
3
4
REDDIT CLONE!
5
6
7
8
9
10
Next I'll define my "Index" view:
1 "List" />
2
3
4
5
6
7
- 'var sub in ViewData.Model'>
8 'sub' />
9
10
11
You'll notice the two tags "SubmitLink" and "Submission". These are spark partial views that I used so that I can reuse those templates in other places.
SubmitLink:
1
Submit a link:
2 <% using (Html.BeginForm("Submit","Submissions")) { %>
3 Title:
4 !{Html.TextBox("Title")}
5 Link:
6 !{Html.TextBox("Link")}
7
8 <% } %>
Submission:
1 (${submission.Score}) -
2 "${submission.Link}">
3 ${submission.Title}
4
5
6
7 !{Html.ActionLink(submission.Comments.Count + " Comments", "View",
8 new { submission.ID} )} -
9 !{Html.ActionLink("Vote Up", "VoteUp",
10 new { submission.ID} )} -
11 !{Html.ActionLink("Vote Down", "VoteDown",
12 new { submission.ID} )}
I now have a fully functional reddit page, however I don't have a way to view / post comments. I need to define the comments view:
1 "RedditClone.Models.Submission" />
2
3
4 'ViewData.Model' />
5
6
7
Submit Comment:
8 <% using (Html.BeginForm("Create", "Comments")) {%>
9 !{Html.Hidden("SubmissionID", ViewData.Model.ID)}
10 !{Html.TextArea("Text")}
11
12 <% } %>
13
14 <%
15 var rootComments =
16 ViewData.Model
17 .Comments
18 .Where(c => !c.ParentCommentID.HasValue)
19 .OrderByDescending(c => c.Score)
20 .ToList();
21 Html.RenderPartial("_renderComment", new { comments = rootComments });
22 %>
This view is straight forward, at the top it uses the Submission partial view to show the submission information and links. Below that it has a form to submit a new comment. Finally, it grabs the root comments for the link and passes them to the _renderComment partial.
You may wonder why I am using Html.RenderPartial instead of the spark tag format. The reasoning will be easier to explain when you see the partial:
1 "List" />
2
3
4
- 'var comment in comments.OrderByDescending(c =]] c.Score)'>
5 ( ${comment.Score} ) - ${comment.Text}
6
7 !{Html.ActionLink("Vote Up", "VoteUp", "Comments", new {comment.ID, comment.SubmissionID}, null )} -
8 !{Html.ActionLink("Vote Down", "VoteDown", "Comments", new {comment.ID, comment.SubmissionID}, null )}
9
10 <% using (Html.BeginForm("Create", "Comments")) {%>
11 !{Html.Hidden("ParentCommentID", comment.ID)}
12 !{Html.Hidden("SubmissionID", comment.SubmissionID)}
13 !{Html.TextArea("Text")}
14
15 <% } %>
16
17 if='comment.ChildComments.Any()'>
18 <% Html.RenderPartial("_renderComment", new { comments = comment.ChildComments.ToList() }); %>
19
20
21
The partial renders an entire comment tree by calling itself recursively. Unfortunantly Spark cannot handle recursive partials, so I get around that limitation by invoking it using the default ASP.NET MVC call Html.RenderPartial. Hopefully Spark gains this ability int the future as it will significantly improve readability.
I've now finished my entire reddit clone. It is able to do all of the features I wanted, and weighs in at around 120 lines of C#, and 100 lines of Spark templates. While this is heavier than some of the others, it does have commenting and a real database. Had I not done this, the entire app (templates and all) would have easily fit into 100 lines.
I think this helps dismiss the notion that ASP.NET apps are heavy and require enormous amounts of boilerplate code. It also shows that you have full control of the HTML outputted, which was a common complaint on those who use webforms.
The RedditClone is running here: .
The complete source code is on my Codeplex Repository: .
Remember, this was just for fun, and is missing a lot of features that would make it robust (Input validation for one!). However, it is a nice proof of concept on how fast you can make an application using ASP.NET MVC.
Posted by Jonathan Holland on 2/7/2010.