Blog Post A Sample Blog Post

Writing a dumb blog controller.

Hey, I'm starting a blog, I figure I could go ahead and blog about it. It seems appropriately meta eh? I've got a folder full of unfinished and half baked ideas and here I go with another one. I'm going to write all this content in markdown as I code and then I'm going to write a thing that turns http get requests into (hopefully cached) disc requests for markdown files that then parses the markdown into html and spits back a blog post. In reality I want a jekyll blog, except one that I made myself because it seemed like a wheel to reinvent.

Ok starting off I slapped together an ASP MVC site using bootstrap and dropped a controller on that bad boy called BlogController.

namespace CardboardForts.Controllers
{
    using System.Web.Mvc;

    public class BlogController : Controller
    {
        public ActionResult Home()
        {
        }
        public ActionResult Post()
        {
        }
    }
}

High tech stuff there I tell ya.
Ok next up toss some get request parameters in there to specify which blog posting I want to read.

public ActionResult Post(string id){            
    return Content(id);
}

Rocket science goin on here. I run out and hit http://localhost:51902/blog/post/banana and verify that I get a page that just says banana. Next up hitting disc. I've got some markdown stuff sitting in a folder so I'll point to that:

private string _postFolder = @"M:\Docs\blog\"
public ActionResult Post(string id){
    var fileName = string.Join("", _postFolder, id, ".md");
    return Content(System.IO.File.Exists(fileName)
                    ? System.IO.File.ReadAllText(fileName)
                    : id)
}

Mind blown right? I test it with localhost:51902/blog/post/banana and localhost:51902/blog/post/metablog and suddenly I'm reading the very text that I'm writing right now except for all the formatting is stripped out. So now on to making html like it's 1996 all over again. Rendering markdown to html has to be a solved problem (I'm only out to reinvent one wheel at a time here) Lets hit nuget and see what goodies are hanging around: Tanaka.Markdown.Html -> Looks nice, but no image support. :( Kiwi.Markdown -> seems promising... Github flavored markdown, repackages MarkdownSharp that does everything I want, adds syntax highlighting for code. Winner winner chicken dinner.

Ok now how to use the new toy... Well there's no documentation.
Has unit tests, which are even better! Aaaaand it's an empty folder. Code diggin time. And I'm loosing steam here. It's been over 10 minutes and I haven't got any feedback. MarkdownSharp has documentation and seems good enough.

public ActionResult Post(string id)
{
    var fileName = string.Join("", _postFolder, id, ".md");
    var content = System.IO.File.Exists(fileName) ? System.IO.File.ReadAllText(fileName) : id;
    var md = new MarkdownSharp.Markdown();
    var html = md.Transform(content);
    return Content(html);
}

Crazy talk! It's alive!!! I immediately notice all the markdown errors :) Spellcheck who? Time to do make with the pretty and drop this into a bootstrap template. Cyborg theme seems pretty good. Delete the lorem ipsum and drop this little guy in...

@Html.Raw(ViewBag.Content)

Then modify the controller to look like this:

public ActionResult Post(string id)
{
    var fileName = string.Join("", _postFolder, id, ".md");
    var content = System.IO.File.Exists(fileName) ? System.IO.File.ReadAllText(fileName) : id;
    var md = new MarkdownSharp.Markdown();
    ViewBag.Content = md.Transform(content);
    return View();
}

Wow it's almost like I've got a real blog here. At this point I'd like to say how liberating it is to code like an idiot. ViewBags? File system access based on unauthenticated get parameters? Hard coded magic strings? Oh man... Pig in mud right now. I would never dare to do this while being paid.

Really though, I'm not putting this on a server until I can work out some form of locking it down. So lets take care of the easy targets and move the posts folder from a magic string to something a bit more safe.

public ActionResult Post(string id)
{
    string postsFolder = HttpContext.Server.MapPath("~/App_Data/Posts/");
    var fileName = string.Join("", postsFolder, id, ".md");
    var content = System.IO.File.Exists(fileName) ? System.IO.File.ReadAllText(fileName) : id;
    var md = new MarkdownSharp.Markdown();
    ViewBag.Content = md.Transform(content);
    return View();
}

Now it will pull from the App_Data folder where all good data for my app should belong. It's still disc access even if it's disc access inside a proper folder, so lets get all domain-ish and make some models. I'm going to go nuts and actually make a new project for this domain object, and in that project I'm going to make a models folder, and inside that folder i'm going to go ahead and create a class called blog post. BlogPost is going to have one field named Content.

namespace CardboardForts.Blog.Models
{
    public class BlogPost
    {
        public string Content { get; set; }
    }
}

This may appear to be nuts to many people but I've got a plan here. See I'm seeing this thing come together and I'm imagining, hey maybe I can make the first h1 in the file the blog title, and maybe the first h2 could be the subtitle. Then maybe I want to do some work on image urls so I don't have to hand key relative paths to my images directory. Then maybe I want some bacon on my pizza. And maybe... Just maybe I should spend 3 minutes setting up a proper domain object for these features i've got in my head. So I'll add the reference from the domain project to the mvc app and head into the controller and get rid of that viewbag

public ActionResult Post(string id)
{
    string postsFolder = HttpContext.Server.MapPath("~/App_Data/Posts/");
    var fileName = string.Join("", postsFolder, id, ".md");
    var content = System.IO.File.Exists(fileName) ? System.IO.File.ReadAllText(fileName) : id;
    var md = new MarkdownSharp.Markdown();
    var blogPost = new BlogPost { Content = md.Transform(content) };
    return View(blogPost);
}

Then I'll head to the view and tell it that I'm strongly typed by dumping this line at the top:

@Model CardboardForts.Blog.Models.BlogPost

Then the content dump would look like this

@Html.Raw(Model.Content)

Things are currently getting tedious and I'm starting to think I need to automate the testing on this. I mean I've got a proof of concept, a domain project and a grand total of... 6 lines of code. So lets start pulling the useful stuff out of the controller and into a place where it can be useful. I'm going to make another folder called repositories. In it I'm going to put a thing that distributes blog posts. So I'm going to move everything out of the controller and into a repository. Not pretty yet, but it'll get there.

public class BlogPostRepository
{
    public string PostsDirectory { get; set; }

    public BlogPost Get(string postName)
    {
        var fileName = string.Join("", PostsDirectory, postName, ".md");
        var content = System.IO.File.Exists(fileName) ? System.IO.File.ReadAllText(fileName) : postName;
        var md = new MarkdownSharp.Markdown(); 
        return new BlogPost { Content =  md.Transform(content)};
    }
}

At this point I decided to remove the Kiwi markdown implementation from the mvc project and just install MarkdownSharp in the domain. I was only using the MarkdownSharp bits anyways. That leaves my controller on a bit of the skinny side like this:

public ActionResult Post(string id)
{
    var blogRepo = new BlogPostRepository
    {
        PostsDirectory = HttpContext.Server.MapPath("~/App_Data/Posts/")
    };
    return View(blogRepo.Get(id));
}

Crazy talk! Things are getting a bit too skinny but meh! Onward. The thing that's really kinda top priority is to get this file system out of the way.
I don't know how someone would hack this, but it bothers me that I can type any ol thing into the url and determine something about my file system.
It just feels dirty and wrong. So I'm going to now break this out. I'll write a something that scans a directory on my terms and builds a listing of blog posts that I've uploaded. New Project -> CardboardForts.Blog.UnitTests New class BlogScanner: New Test BlogScannerScansForFiles:

And I've run into the first folly. Not that I started testing the file system. I stopped because I know testing the file system is wrong. Then I wondered how to abstract the file system out of the file system scanner. Which is silly. When I get this way I've got a little thing I do. I draw a tic tac toe board on the wall (I have 2 giant whiteboard walls). If I was to sort my things based on these 9 categories where does this thing fit.

Well currently I'd say that asking the operating system about the contents of a directory would classify itself as an api call. So I should have a thing to wrap that little nugget up. The decision to classify one of those things as a blog post would be a domain concern though so I should have a little thing there as well. It's come to the point where I'd say that I need a persistence tier filling out the full 3 tier system here. I'm not going to add another project here for the persistence layer, because meh, why add the complexity. In terms of namespacing no one will be able to tell the difference anyways. Also no one ever would want to switch this implementation to a database, or webservice backed thing (or would they?!?). I justify splitting the domain into a seperate project because I can see wanting to eventually want to make an android app and having a nice mature api over a domain would be nice to have. Bah, i've spent a good hour navel gazing about where to put my one line of code. CODE MORE!

namespace CardboardForts.Blog.Data.Apis
{
    using System.IO;

    public class DirectoryListing
    {
        public virtual IEnumerable<string> GetFiles(string path)
        {
            return Directory.EnumerateFiles(path);
        }
    }
}

Now lets go spelunking:

[TestClass]
public class BlogScannerTests
{
    [TestMethod]
    public void BlogScannerScansForFiles()
    {
        var fakePath = @"M:\Docs\blog";
        var dir = new DirectoryListing();
        Assert.AreEqual(null, dir.GetFiles(fakePath));
    }
}

One quick Ctrl+R+A later and I've got not only a broken test, but visual studio is kind enough to halt on the line and pop up the locals window for easy copying of the legit values that I can expect to return. [0] "M:\Docs\blog\4. SEO.md" string [1] "M:\Docs\blog\ComponentsOfData.txt" string [2] "M:\Docs\blog\3. Ajax and Webforms.md" string [3] "M:\Docs\blog\What to test.txt" string [4] "M:\Docs\blog\howtoblog.md" string [5] "M:\Docs\blog\FirstPost.md" string [6] "M:\Docs\blog\IsTDDDead2.txt" string [7] "M:\Docs\blog\SideProjectIdea.txt" string [8] "M:\Docs\blog\howtoblog.txt" string [9] "M:\Docs\blog\Blog Ideas.txt" string [10] "M:\Docs\blog\5. IsTDDDead2.txt" string [11] "M:\Docs\blog\StupidVimTricks.txt" string [12] "M:\Docs\blog\metablog.md" string [13] "M:\Docs\blog\code first is pretty awesome.txt" string [14] "M:\Docs\blog\2. Jquery and joy.md" string [15] "M:\Docs\blog\NewProductIdea.txt" string [16] "M:\Docs\blog\LearningNotes.txt" string [17] "M:\Docs\blog\Repository Blog Post.md" string Exactly two find and replace ops later and I've got my stub data. Nuget NSubstitute drop in a stub and rerun it to make sure I didn't do anything stupid.

[TestMethod]
public void BlogScannerScansForFiles()
{
    var fakeResults = new List<string>  
    {
        "M:\\Docs\\blog\\4. SEO.md" ,
        "M:\\Docs\\blog\\ComponentsOfData.txt"  ,
        "M:\\Docs\\blog\\3. Ajax and Webforms.md"   ,
        "M:\\Docs\\blog\\What to test.txt"  ,
        "M:\\Docs\\blog\\howtoblog.md"  ,
        "M:\\Docs\\blog\\FirstPost.md"  ,
        "M:\\Docs\\blog\\IsTDDDead2.txt"    ,
        "M:\\Docs\\blog\\SideProjectIdea.txt"   ,
        "M:\\Docs\\blog\\howtoblog.txt" ,
        "M:\\Docs\\blog\\Blog Ideas.txt"    ,
        "M:\\Docs\\blog\\5. IsTDDDead2.txt" ,
        "M:\\Docs\\blog\\StupidVimTricks.txt"   ,
        "M:\\Docs\\blog\\metablog.md"   ,
        "M:\\Docs\\blog\\code first is pretty awesome.txt"  ,
        "M:\\Docs\\blog\\2. Jquery and joy.md"  ,
        "M:\\Docs\\blog\\NewProductIdea.txt"    ,
        "M:\\Docs\\blog\\LearningNotes.txt" ,
        "M:\\Docs\\blog\\Repository Blog Post.md"   
    };
    var dir = Substitute.For<DirectoryListing>();
    dir.GetFiles("somepath").Returns(fakeResults);
    Assert.AreEqual(fakeResults, dir.GetFiles("somepath"));
}

Stub data works. Now on to building the thing that uses stub data.

var scanner = new CardboardForts.Blog.Services.BlogScanner(dir);
List<Blog.Models.BlogListing> listings = scanner.GetBlogListings();
Assert.AreEqual(fakeResults, dir.GetFiles(somePath));

You'll notice that I didn't actually create the thing I needed this time, I just said what it was that I needed and hit ctrl+. and let visual studio create it for me from the test. I love this utility. I'm also redoing the DirectoryListing to take the path as a constructor argument to simplify the test a bit. Then I'll move the whole NSubstitute chunk out into it's own sub. And I can probably make the stub data and the path members of the class since I'll probably want to compare on them eventually. And I get this.

    [TestMethod]
    public void BlogScannerScansForFiles()
    {
        var scanner = new BlogScanner(GetStub());
        var listings = scanner.GetBlogListing();

        Assert.AreEqual(_fakeResults, listings.Select(l => l.Name););
    }

private DirectoryListing GetStub()
{
    var dir = Substitute.For<DirectoryListing>(somePath);
    dir.GetFiles().Returns(fakeResults);
    return dir;
}

Test fails beause my getbloglisting throws a NotImplementedException so I'll head over there to fix that up.

public List<BlogListing> GetBlogListing()
{
    return dir.GetFiles().Select(f => new BlogListing { Name = f }).ToList();
}

And the test still fails because Assert.AreEqual doesn't check lists... One google later and I've got CollectionAssert.AreEquivalent() Verifies that the specified collections are equivalent. Two collections are equivalent if they have the same elements in the same quantity, but in any order. Elements are equal if their values are equal, not if they refer to the same object. Looks like a winner and the test passes. But it's still churning out wrong data (although i'm confident that it's churning out the expected wrong data). Lets mangle a test to get red.

[TestMethod]
public void BlogScannerScansForFiles()
{
    var scanner = new BlogScanner(GetStub());
    var listings = scanner.GetBlogListing();
    var results = listings.Select(l => l.Name).ToList();

    CollectionAssert.Contains(results, "LearningNotes");
}

This looks more like a thing that I'd really want. And the test fails so I can get to work making it pass. First thing I want to do is strip the path out of the string, then maybe take the file extension off. I'm going to be honest with you, I tried to do string-fu with removing the passed in path then detecting the last period in the file name if it existed. Then I remembered about System.IO.Path... No glory here, just truth.

public List<BlogListing> GetBlogListing()
{
    return dir.GetFiles()
        .Select(Path.GetFileNameWithoutExtension)
        .Select(f => new BlogListing { Name = f })
        .ToList();
}

And my single test passes, and much code was deleted. So landmark occasion time here. I think I actually have a test that I like and want to keep around so time to write up a second test. Next feature in mind is to only get files that have a markdown extension (.md) so I copy paste my test and give it a new name and then run it and am horrified to see that it fails! I mean my learning notes were stored in a text file. FOR SHAME!

[TestMethod]
public void BlogScanner_FindsAFile()
{
    var scanner = new BlogScanner(GetStub());
    var listings = scanner.GetBlogListing();
    var results = listings.Select(l => l.Name).ToList();

    CollectionAssert.Contains(results, "LearningNotes");
}

[TestMethod]
public void BlogScanner_OnlyGetsMarkdownFiles()
{
    var scanner = new BlogScanner(GetStub());
    var listings = scanner.GetBlogListing();
    var results = listings.Select(l => l.Name).ToList();

    CollectionAssert.DoesNotContain(results, "LearningNotes");
}

To make the test pass I toss a where clause in.

public List<BlogListing> GetBlogListing()
{
    return dir.GetFiles()
        .Where(f => Path.GetExtension(f).ToLower() == "md")
        .Select(Path.GetFileNameWithoutExtension)
        .Select(f => new BlogListing { Name = f })
        .ToList();
}

And my first test is now failing because I introduced a contradictory requirement. Fix that up by pointing it to a markdown file. And it still fails... There is something in my code that I don't understand and I'm betting it's in the single line of code I added last time. My locals view tells me my results has a count of 0, by this time I'm used to seeing tests fail so I know it used to say 18. Somehow my where clause is filtering out all the entries. I suspect get extension is returning ".md" instead of "md" so i'll try that, cross my fingers, and there it is. Passing tests! I probably could have set a break point and inspected stuff but sometimes making an educated guess and rerunning the tests is just faster (it's taking a whole 187ms to run tests at this point so what have I got to loose).

.Where(f => Path.GetExtension(f).ToLower() == ".md")

So I've got a tested thing that works about how I expect it to, so now I'll use it to clean up the prototype code in the repository. As I'm doing this I run into a problem. The UI is the only thing that knows where the posts are located (the folder is going to change as soon as this gets deployed) So I need to let the UI tell the domain repository: Here's the file path. And the repository has to use that configuration nugget to build a the directory scanner. UI layer doesn't talk to the Data layer and all that, so we keep it separated. I hack at it a bit and mess around with constructor vs property injection and come out with this mess.

// Controller
public ActionResult Post(string id)
{
    var path =HttpContext.Server.MapPath("~/App_Data/Posts/" );
    var blogRepo = new BlogPostRepository(path);
    return View(blogRepo.Get(id));
}

// Repository
public class BlogPostRepository
{
    private DirectoryListing _listing;
    private string _path;

    public BlogPostRepository(string path)
    {
        _path = path;
        _listing = new DirectoryListing(path);
    }

    public BlogPost Get(string postName)
    {
        var scanner = new BlogScanner(_listing);
        var content = postName;
        if (scanner.GetBlogListing().Any(listing => listing.Name == postName))
        {
            var fileName = string.Join("", _path, postName, ".md");
            content = System.IO.File.Exists(fileName) ? System.IO.File.ReadAllText(fileName) : postName;
        }
        var md = new MarkdownSharp.Markdown();
        return new BlogPost { Content = md.Transform(content) };
    }
}

It's still terrible because I'm doing IO directly in the repository, but hey at least I'm pulling blog posts on my terms (the blog scanners terms) instead of reacting to any old thing someone wants to put in the url. And that distinction is important to me. Now to get rid of some of the ick factor of IO in that repository. Go Red!

[TestMethod]
public void BlogScanner_RetrievesValidFile()
{
    var scanner = new BlogScanner(GetStub());
    string blogText = scanner.GetBlog("howtoblog");

    Assert.IsNotNull(blogText);
}

As expected, I do not have a method named GetBlog() on the blog scanner that returns the contents of a file. Time to let visual studio generate that for me.

public string GetBlog(string fileName)
{
    return File.Exists(fileName) ? System.IO.File.ReadAllText(fileName) : fileName;
}

Pulling directly from the repository and mangling it up a bit I get direct file access. Lets see what it does.... Test passes which is surprising. I guess it could have picked up on the path the test stub had in it. I'm going to set a break point just to visualize what it actually got. Aaaaand I have it returning the path if it didn't find any content. Maybe that's a bad idea now. Fix that test up to say:

public string GetBlog(string fileName)
{
    return File.Exists(fileName) ? System.IO.File.ReadAllText(fileName) : "Nothin Here Sir.";
}

[TestMethod]
public void BlogScanner_RetrievesValidFile()
{
    var scanner = new BlogScanner(GetStub());
    string blogText = scanner.GetBlog("howtoblog");

    Assert.IsTrue(blogText.Equals("Nothin Here Sir."));
}

And I've got a passing test, a expected result, and a failing requirement. What I really want is the text inside the file, not the message that the file wasn't found. Kinda wishing I had functional programming and an algebraic type now because that response text looks ick as well, but one thing at a time!

// In DirectoryListing.cs
public virtual string GetText(string fileName)
{
    return File.Exists(fileName) ? System.IO.File.ReadAllText(fileName) : string.Empty;
}

// In BlogScanner.cs
public string GetBlog(string fileName)
{
    return dir.GetText(fileName);           
}

// In BlogScannerTests.cs
private string _fakeContent = "I am a bunch of words that are fake";        
[TestMethod]
public void BlogScanner_RetrievesValidFile()
{
    var scanner = new BlogScanner(GetStub());
    string content = scanner.GetBlog("howtoblog");
    Assert.AreEqual(content, _fakeContent);
}
private DirectoryListing GetStub()
{
    var dir = Substitute.For<DirectoryListing>(_somePath);
    dir.GetFiles().Returns(_fakeResults);
    dir.GetText(Arg.Any<string>()).Returns(_fakeContent);
    return dir;
}

So I run that and things stay passing as expected. But the requirement is still failing. I'm actually trying to load a file with no extension and no path and getting a valid result. So lets say that.

[TestMethod]
public void BlogScanner_RetrievesValidFile()
{
    var slug = "howtoblog";
    var dl = GetStub();
    var scanner = new BlogScanner(dl);
    string content = scanner.GetBlog(slug);
    dl.Received().GetText(_somePath + slug + ".md");
}

As expected NSubstitute throws an error because it did not receive the correct path. So to get it my first temptation is string-fu again. Resist! I'm trying to be secure here so ask, but never tell and I've already got something here that filters out unwanted slugs.

public string GetBlog(string fileName)
{
    var file = dir.GetFiles()
                  .Where(f => Path.GetExtension(f).ToLower() == ".md")
                  .Where(f => Path.GetFileNameWithoutExtension(f) == fileName)
                  .First();

    return dir.GetText(file);           
}

Test passes. Which is weird because I honestly expected that to be more work than it was. Whatever. I copy pasted stuff so I've got a great refactor target that i'm not letting go of.

public List<BlogListing> GetBlogListing()
{
    return ValidFileNames().Select(
        f => new BlogListing
        {
            Name = Path.GetFileNameWithoutExtension(f)
        }).ToList();
}

public string GetBlog(string fileName)
{
    var file = ValidFileNames().Where(
        f => Path.GetFileNameWithoutExtension(f) == fileName)
        .First();
    return dir.GetText(file);           
}

private IEnumerable<string> ValidFileNames()
{
    return dir.GetFiles().Where(f => Path.GetExtension(f).ToLower() == ".md");
}

So yeah, I'm now still wrapped up in my safty net and all 3 tests are still passing. Now to take my new toy for a test spin to make sure reality is matching the tests. I edit BlogPostRepository.cs to look like the following and fire it up:

public BlogPost Get(string postName)
{
    var scanner = new BlogScanner(_listing);
    return new BlogPost { Content = new Markdown().Transform(scanner.GetBlog(postName)) };
}

It's at this point that I get suspicious about the difference between my BlogPostRepository and my BlogScanner.
It seems like BlogScanner has become more of a Finder of Models, than a Doer of Stuff to Models. It's the code smell of useless abstraction and I want it gone!

// In my controller
public ActionResult Post(string id)
{
    var path = HttpContext.Server.MapPath("~/App_Data/Posts/" );
    var blogScanner = new Blog.Services.BlogScanner(path);
    return View(blogScanner.GetBlog(id));
}

// In the BlogScanner
public BlogScanner(string path) : this(new DirectoryListing(path)) { }
public BlogPost GetBlog(string fileName)
{
    var md = new Markdown();
    var file = ValidFileNames().Where(
        f => Path.GetFileNameWithoutExtension(f) == fileName)
        .First();
    return new BlogPost { Content = md.Transform( _directory.GetText(file)) };
}

I seem to have caught that one early enough that it was pretty painless to refactor it all out. I brought markdown into the GetBlog Method and altered the return type to a BlogPost instead of a string. I then added an alternate constructor to the mix. And done. Fixing the one test that broke was also pretty easy (I had to compare to the .Content property of the BlogPost object that was being returned). Now the only thing left is to rename my BlogScanner to BlogRepository and move it to the other folder. Then rename variables to make sense. Ok so now I've kinda hit a stable point and it's time to do a feature review.

  • GetBlog will take a name and produce html.
  • GetBlogListing will produce a list of all valid files that can be got.
  • The listing will not grab files in another drectory, or files without the .md extension.

So next up on the list is to actually go out and try to view these things. Tests are good. Using it is better.

And it immediately crashes.

public BlogPost GetBlog(string fileName)
{
    var md = new Markdown();
    var file = ValidFileNames().Where(
        f => Path.GetFileNameWithoutExtension(f) == fileName)
        .First();
    return new BlogPost { Content = md.Transform(_directory.GetText(file)) };
}

Apparently when I navigate out to my posts directory the controller passes a null string over to GetBlog and the .First() does not like not returning anything. Which brings up the good case of what to do if you ask for a blog that doesn't exist. So I'll first write a test to duplicate my failure.

[TestMethod]
public void GetBlog_ReturnsBlogNotFoundOnInvalidRequest()
{
    var slug = "howtoblog";
    var dl = GetStub();
    dl.GetFiles().Returns(new List<string>());
    var repo = new BlogRepository(dl);
    string content = repo.GetBlog(slug).Content;
    dl.DidNotReceive().GetText(_somePath + slug + ".md");
}

Which kicks off the exception. Now how to handle it... After a bit of navel gazing I go back to my tic tac toe board and consider that 404 truly is a state that a blog can be in. Besides my domain model is in a pretty sad state with it's only one poco. So lets write another failing test.

[TestMethod]
public void BlogPost_SetContent()
{
    var post = new BlogPost();            
    post.SetContent(_fakeContent);
    Assert.AreEqual(_fakeContent, post.Content);
}

Ugh, i'm testing a setter method. Gross. but it fails to compile so I create the method and I can mangle this test into something useful.

Assert.IsTrue(post.IsValid);

Fails compilation and I quickly Ctrl+. to auto fix it. Well this setter will have side effects so there. Gotta come up with a better name than set (still gross). I'll also make the properties setter private so there's really only one entry point for this thing.

public class BlogPost
{
    public string Content { get; private set; }
    public bool IsValid { get; private set; }

    public void SetContent(string content)
    {
        Content = content;
        IsValid = !string.IsNullOrWhiteSpace(content);
    }
}

Come on brain. Think of better name! It feels a little less gross now that it's properly encapsulated, but meh! Lets look at it from the perspective of the repository.

public BlogPost GetBlog(string fileName)
{
    var blogPost = new BlogPost();
    var markdown = new Markdown();
    var content = ValidFileNames().Where(f => Path.GetFileNameWithoutExtension(f) == fileName)
                    .Select(_directory.GetText)
                    .Select(markdown.Transform)
                    .FirstOrDefault();

    blogPost.SetContent(content);
    return blogPost;
}

Still feels a bit awkward to use, but the naming conventions are weak tonight. Lets see it from the controllers standpoint.... ok I don't technically do anything to the controller it just returns null. Lets use the program and hope that provides insight... Well nuts it works properly. Stuff to fix was my hopeful inspiration. Ok onward to test the other cases in the unit tests. I kinda got ahead of myself with that string.IsNullOrWhitespace business so I'll get that IsValidFail test out of the way.

[TestMethod]
public void BlogPost_SetInvalidContent()
{
    var post = new BlogPost();
    post.SetContent(null);
    Assert.IsFalse(post.IsValid);

    post.SetContent(string.Empty);
    Assert.IsFalse(post.IsValid);

    post.SetContent("  ");
    Assert.IsFalse(post.IsValid);
}

Passes as expected. The page does kinda look bare with no 404 text so I could do some controller work there. The page does look a bit weird without any error text so I suppose I'll toss a 404 message on my domain object. I actually started typing that, then hit myself and put the 404 stuff in the controller.

public class BlogController : Controller
{
    public ActionResult Home()
    {
        return View();
    }
    public ActionResult Post(string id)
    {
        var path = HttpContext.Server.MapPath("~/App_Data/Posts/" );
        var repo = new BlogRepository(path);
        var blog = repo.GetBlog(id);
        if (!blog.IsValid)
            return RedirectToAction("Home"); 

        return View(blog);
    }
}

Speaking of the redirect to home, it's getting obvious that I should have some sort of listing on blog/home of the actual posts, so that seems simple enough to do with the functionality I have

// Controller
public ActionResult Home()
{
    var path = HttpContext.Server.MapPath("~/App_Data/Posts/" );
    var repo = new BlogRepository(path);
    var listings = repo.GetBlogListing();

    return View(listings);
}

// View
@model List<CardboardForts.Blog.Models.BlogListing>
@foreach (var listing in Model)
{
<h1> <a href="#"> @listing.Name </a> </h1>
}

So now I have very sad listings and that works, next up link up. I'm in prototype mode right now so I'm slinging code around like a barbarian trying to figure out what works and what looks good. The UI still scares and confuses me at times. After a bit of digging I come up with

@Url.Action("Post", "Blog", new {id = listing.Name})

And all is good with the world except it still looks bad. New limitations on this method are afoot and starting to make theirselves known. My blog titles must limit theirselves to a single word. And that's limiting. I need some view specific models. And it seems like most blog listings on the internets have a title, a post date, a title image, and maybe the first paragraph of the post that I'm pretty sure has an official name like 'the sell' or 'the hook'. Despite feeling redicilous about calling anything in my code 'the sell' like it deserves the third person, I wants it all.

namespace CardboardForts.ViewModels
{
    public class BlogListing
    {
        public string Title { get; set; }
        public DateTime PostDate { get; set; }
        public string ImageUrl { get; set; }
        public string Description { get; set; }
    }
}

Leave a Comment:


Blog Search

Side Widget Well

Bootstrap's default well's work great for side widgets! What is a widget anyways...?