When experimenting with some PAAS-hosted application development on AppHarbor, it occured to me, what if somebody wanted to blog, but not necessarily deal with and/or pay additional monthly cost for database addons, etc. with their provider? What if this same person wanted the freedom to write their blog posts in Markdown syntax text files from say, their iPad, mobile device or workstation, send the file to their host and have it automatically show up on their site without the admin interface, database, etc.? Today's topic will cover just that scenario. A simple file-based blog solution using Markdown syntax, text files and ASP.NET MVC 3. Let's get started.

Creating the Initial Blog Application

For this example, we'll make use of Phil Haack's Really Empty MVC Project Template to which we'll add an App_Data/BlogPosts folder, Content/BlogPostImages folder, HomeController, corresponding Views/Home/Index.cshtml Home View, some additional Shared Views and initial CSS styling. The resulting solution explorer view and running Blog application is shown below (source code provided at end of post):

Initial solution setup and running application

Installing Markdown

Markdown is a text-to-HTML conversion tool for web writers, bloggers, etc. Markdown allows you to write in an easy-to-read/write plain text format then convert it to structurally valid XHTML (or HTML). Markdown-formatted text is designed to allow content to be published as-is, as plain text, without looking like it's been marked up with tags, etc. Markdown is FREE software, available under a BSD-style open source license.

As we're using MVC 3 written in C#, we'll make use of MarkdownSharp, also an open source C# implementation of the Markdown processor, as featured on Stack Overflow. To add it, we execute the following in Visual Studio's Package Manager console (adds a single MarkdownSharp.dll libary to the application Bin directory):

PM> Install-Package MarkdownSharp

Blog Post & Summary Data Conventions

Being a file-based solution, blog post .txt files are stored in the App_Data/BlogPosts folder created earlier. For this example, we'll use the following naming conventions for both blog post and blog summary info files using the _ character to separate Date and Name information:

YYYY-MM-DD_[blog-post-name-separated-by-hyphens].txt // blog post markdown syntax content
YYYY-MM-DD_[blog-post-name-separated-by-hyphens]_summary.txt // JSON formatted blog summary info

Two files are used in this example to separate Markdown content from Blog Post Summary information. The whole goal of Markdown is to be able to use content "as-is", void of any additional markup tags or instruction. The YYYY-MM-DD value will be used to indicate the Post Date of the blog post whereas the [*] hyphen-separated content indicates the blog title. The summary file will make use of JSON syntax for displaying blog summary information when generating blog listings.

Building the File System Data Model

Firstly, we create a Models/BlogListing.cs model class for housing blog data that will be used to create our list of blogs.

namespace TxtBasedBlog.Sample.Models
{
    public class BlogListing
    {
        public string Url { get; set; }
        public string Title { get; set; }
        public string ShortDescription { get; set; }
        public string Content { get; set; }
        public string Author { get; set; }
        public DateTime PostDate { get; set; }
        public string Image { get; set; }
    }
}

Next, we'll create a Models/BlogPost.cs model class that will be responsible for handling the actual blog post Markdown content.

namespace TxtBasedBlog.Sample.Models
{
    public class BlogPost : BlogListing
    {
        public string Body { get; set; }
    }
}

Writing A Sample Blog Post

Using the data convention described earlier, we'll create two sample files for reading in our blog application. Summary file using JSON syntax first and complete blog post using Markdown syntax second.

{
    Title: "ASP.NET MVC Overview",
    Url: "asp_net_mvc_overview",
    PostDate: "2012-02-09",
    Author: "Microsoft ASP.NET Team",
    ShortDescription: "ASP.NET MVC gives you a powerful, patterns-based way to build dynamic websites that enables a clean separation of concerns and that gives you full control over markup for enjoyable, agile development.",
    Image: "content/blogpostimages/image001.jpg"
}
**ASP.NET MVC** gives you a powerful, patterns-based way to build dynamic websites that enables a clean separation of concerns and that gives you full control over markup for enjoyable, agile development. 

MVC includes many features that enable:

* fast, TDD-friendly development for creating sophisticated applications
* use the latest web standards.

[Learn More About MVC](http://www.asp.net/mvc "Learn more about MVC today!")

Displaying The List of Blogs

An appSetting key in the web.config file to allow for changing data directories without having to re-compile the application that will be used by the BlogFileSystemManager class in our next step.

<appSettings>
    ...
    <add key="BlogPostsDirectory" value="~/App_Data/BlogPosts"/>
    ...
</appSettings>

The Models/BlogFileSystemManager.cs class that is responsible for checking our pre-determined App_Data/BlogPosts data directory (configured previously) for file-based blog data and subsequently building, then returning a list of blog posts.

namespace TxtBasedBlog.Sample.Models
{
    public class BlogFileSystemManager
    {
        private string filePathToBlogPosts;

        public BlogFileSystemManager(string dirPath)
        {
            filePathToBlogPosts = dirPath;
        }

        public List<BlogListing> GetBlogListings(int limit)
        {
            var allFileNames = getBlogPostsFiles();
            var blogListings = new List<BlogListing>();
            foreach (var fileName in allFileNames.OrderByDescending(i => i).Take(limit))
            {
                var fileData = File.ReadAllText(fileName);
                var blogListing = new JavaScriptSerializer().Deserialize<BlogListing>(fileData);
                blogListings.Add(blogListing);
            }
            return blogListings;
        }

        private IEnumerable<string> getBlogPostsFiles()
        {
            return Directory.GetFiles(filePathToBlogPosts, "*summary.txt").ToList();
        }
    }
}

The HomeController loads BlogListing Model data (limited to 5 results currently) for passing down to the corresponding View.

namespace TxtBasedBlog.Sample.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            var manager = new BlogFileSystemManager(Server.MapPath(ConfigurationManager.AppSettings["BlogPostsDirectory"]));
            var model = manager.GetBlogListings(5);
            return View(model);
        }

        public ActionResult Error()
        {
            return View();
        }
    }
}

Finally, we add code to our Views/Home/Index.cshtml strongly typed View for displaying the BlogListing model passed in by the HomeController. Note the root-level URL, for that we'll need to create some Routes in our next step.

@model IEnumerable<TxtBasedBlog.Sample.Models.BlogListing>

<h2>Recent Blog Posts</h2>

@{
    foreach (var item in Model)
    {
        <div class="post">
            <div class="img-post">
                <a href="/@item.Url" title="@item.Title"><img src="../../@item.Image" alt="" /></a>
            </div>
            <div class="inline">
                <p><a href="@item.Url">@item.PostDate.ToString("MM/dd/yyyy") - @item.Title</a><br />@Html.Raw(item.ShortDescription)</p>
            </div>
        </div>
    }
}

At the moment, our application now looks like below.

Displaying the blog list

Displaying the Blog Post

To display blog post content at application root level, a couple Routes need to be added to the Global.asax.cs file. The first route ensures that a parameterless URL correctly routes to the site home page, the second routes users to the Views/Shared/Error.cshtml View in the event a Blog page can't be found and lastly, the route responsible for routing root-level blog post URLs. These routes are entered ABOVE the default route that comes with the project template.

public static void RegisterRoutes(RouteCollection routes)
{
    ...

    routes.MapRoute(
        "HomePage", 
        "", 
        new { controller = "Home", action = "Index" }
    );

    routes.MapRoute(
        "Error", 
        "Oops", 
        new { controller = "Home", action = "Error" }
    );

    routes.MapRoute(
        "BlogPost", 
        "{postName}", 
        new { controller = "Home", action = "ViewBlogPost", postName = "" }
    );

    ...
}

When a user clicks a Blog post link, the URL requested will routed to the HomeController's ViewBlogPost ActionResult excepting a post name string value prior to displaying the Views/Home/ViewBlogPost.cshtml View to the user. The following has been added to HomeController.

namespace TxtBasedBlog.Sample.Controllers
{
    public class HomeController : Controller
    {
        ...

        public ActionResult ViewBlogPost(string postName)
        {
            var manager = new BlogFileSystemManager(Server.MapPath(ConfigurationManager.AppSettings["BlogPostsDirectory"]));
            if (!manager.BlogPostFileExistsByTitleForUrl(postName))
            {
                return RedirectToRoute("Error");
            }
            var model = manager.GetBlogPostByTitleForUrl(postName);
            return View(model);
        }

        ...
    }
}

With final blog post file system validation logic added to the BlogFileSystemManager class to ensure valid blog post content is indeed present on the file system.

namespace TxtBasedBlog.Sample.Models
{
    public class BlogFileSystemManager
    {
        ...

        public bool BlogPostFileExistsByTitleForUrl(string titleForUrl)
        {
            var matchingFiles = getFilesForBlogPostByTitleForUrl(titleForUrl);
            return (matchingFiles.Count == 2);
        }

        public BlogPost GetBlogPostByTitleForUrl(string titleForUrl)
        {
            var matchingFiles = getFilesForBlogPostByTitleForUrl(titleForUrl);
            var summaryFileData = File.ReadAllText(matchingFiles.Where(i => i.Contains("_summary")).FirstOrDefault());
            var blogPost = new JavaScriptSerializer().Deserialize(summaryFileData);
            blogPost.Body = File.ReadAllText(matchingFiles.Where(i => !i.Contains("_summary")).FirstOrDefault());
            return blogPost;
        }

        private List getFilesForBlogPostByTitleForUrl(string titleForUrl)
        {
            // Updated 2012-03-07: 
            // Richard Fawcett's regex suggestion to prevent titleForUrl subset results. Thanks Richard!
            var files = Directory.GetFiles(filePathToBlogPosts, string.Format("*{0}*.txt", titleForUrl));
            var r = new Regex(@"\d{4}-\d{2}-\d{2}_" + titleForUrl + @"(_summary)?\.txt", RegexOptions.IgnoreCase);
            return files.Where(f => r.IsMatch(f)).ToList();
        }

        ...
    }
}

With validation logic in place, we add the Views/Home/ViewBlogPost.cshtml View to the project with markup responsible for showing the blog Markdown content to the user via a custom @Html.Markdown() helper in association with MarkdownSharp lib installed in beginning of the example.

@using TxtBasedBlog.Sample.Models
@model TxtBasedBlog.Sample.Models.BlogPost

@{
    ViewBag.Title = Model.Title;
}

<h2>@Model.Title</h2>
<p>Posted on @Convert.ToDateTime(Model.PostDate).ToString("dd MMM, yyyy") by @Model.Author - @Html.ActionLink("Back to Blog List", "Index")</p>
@Html.Markdown(Model.Body)

@Html.Markdown() helper class credit goes to Danny Tuppeny and his original post as shown below.

namespace TxtBasedBlog.Sample.Models
{
    public static class MarkdownHelper
    {
        static readonly Markdown MarkdownTransformer = new Markdown();

        public static IHtmlString Markdown(this HtmlHelper helper, string text)
        {
            var html = MarkdownTransformer.Transform(text);
            return MvcHtmlString.Create(html);
        }
    }
}

The final Markdown blog post result looks like this. Notice the clean, root-level URL!

Final blog detail view