Register | Login

Stacking Code

public interface IBlog { string Dump(Stream consciousness); }

Pages

Friday, 17 December, 2010 @ 6:17 PM < Adam Boddington
Tags: Architecture, ASP.NET MVC, Building Neno, NHibernate

This is post #24 in the Building Neno series. Please click here for a description of the Building Neno project and instructions on how to access the source code for this post.

For someone stumbling on to this series about "Building Neno", things might seem a little confusing. What is missing is a project page with a description of what is going on, some instructions on how to access the source code, and perhaps a list of each post in the series.

I could do the project page as a post, but no. The content will change constantly, comments don't apply, and it deserves a shorter URL than what a post would get.

I need a Page domain class where I can specify the URL I want and the content I want displayed. This will be the first addition to the domain model in a while (or ever), so I'll take the opportunity to step through the entire process of what's involved.

Domain Model

The best place to start is always the domain model. The new Page class is pretty light for now.

namespace StackingCode.Neno.DomainModel
{
    public class Page : Entity<int>
    {
        [Required]
        [StringLength(50)]
        public virtual string Path { get; set; }

        [DisplayName("Published")]
        public virtual bool IsPublished { get; set; }

        [Required]
        public virtual string Text { get; set; }
    }
}

While I'm in the domain model, I'll define what the page repository should look like.

namespace StackingCode.Neno.DomainModel.Repositories
{
    public interface IPageRepository : IRepository<Page, int>
    {
        Page GetByPath(string path);
        IQueryable<Page> GetQueryable();
    }
}

The CRUD methods are defined by IRepository. Next, I'll define what the page service should look like.

namespace StackingCode.Neno.DomainModel.Services
{
    public interface IPageService
    {
        Page GetPage(int id);
        Page GetPageByPath(string path);
        IQueryable<Page> GetPages();
        void CreatePage(Page page);
        void UpdatePage(Page page);
    }
}

Database

Being a brand new domain class, I can make the database table match the class as much as possible.

CREATE TABLE [dbo].[Neno_Page] (
    [Id]          INT            IDENTITY (1, 1) NOT NULL,
    [Version]     INT            NOT NULL,
    [Path]        NVARCHAR (50)  NOT NULL,
    [IsPublished] BIT            NOT NULL,
    [Text]        NVARCHAR (MAX) NOT NULL,
    PRIMARY KEY CLUSTERED ([Id] ASC))

I'll also throw a unique index up on the Path column to guarantee uniqueness for now (this should eventually be a validation rule in the domain model).

NHibernate

The NHibernate mapping file is simple as a result. I want to filter pages by user like I do for posts. Only administrators should see unpublished pages.

<class xmlns="urn:nhibernate-mapping-2.2" mutable="true" name="StackingCode.Neno.DomainModel.Page, StackingCode.Neno" table="Neno_Page">
    <id name="Id">
        <generator class="identity" />
    </id>
    <version name="Version" />
    <property name="Path" />
    <property name="IsPublished" />
    <property name="Text" type="StringClob" />
    <filter name="pagesByCurrentUser" condition="(IsPublished = 1 OR :currentUserIsAnAdministrator = 1)" />
</class>
<filter-def name="pagesByCurrentUser">
    <filter-param name="currentUserIsAnAdministrator" type="System.Boolean" />
</filter-def>

Repository

A quick ISession extension method for my page filter...

namespace StackingCode.Neno.Repositories.NHibernate
{
    public static class SessionExtensions
    {
        public static ISession FilterPagesByCurrentUser(this ISession session)
        {
            User currentUser = User.Current;

            session.EnableFilter("pagesByCurrentUser")
                .SetParameter("currentUserIsAnAdministrator", currentUser != null && currentUser.IsAnAdministrator);

            return session;
        }

        // ...
    }
}

The actual page repository looks like this.

namespace StackingCode.Neno.Repositories.NHibernate
{
    public class PageRepository : Repository<Page, int>, IPageRepository
    {
        #region IPageRepository Members

        public Page GetByPath(string path)
        {
            Session.FilterPagesByCurrentUser();

            return Session.Query<Page>()
                .Where(page => page.Path == path)
                .SingleOrDefault();
        }

        public IQueryable<Page> GetQueryable()
        {
            Session.FilterPagesByCurrentUser();

            return Session.Query<Page>();
        }

        #endregion
    }
}

Again the CRUD methods are implemented in the Repository class.

Service

Finally the page service, which is just a straight pass through to the repository for now.

namespace StackingCode.Neno.Services
{
    public class PageService : Service, IPageService
    {
        protected IPageRepository PageRepository { get; set; }

        #region IPageService Members

        public Page GetPage(int id)
        {
            return PageRepository.Get(id);
        }

        // ... and so on

        #endregion
    }
}

The application implementation of the new Page class is done. Next up, the UI.

Configuration

A bit of cut, paste and replace and the page service and repository are defined in Spring.

<object name="PageService" type="StackingCode.Neno.Services.PageService, StackingCode.Neno" scope="request">
    <property name="Context" ref="Context" />
    <property name="PageRepository" ref="PageRepository" />
</object>
<!-- ... -->
<object name="PageRepository" type="StackingCode.Neno.Repositories.NHibernate.PageRepository, StackingCode.Neno.Repositories.NHibernate" scope="request">
    <property name="Context" ref="Context" />
    <property name="Session" ref="Session" />
</object>

Routes

Pages are a special case when it comes to routes. I need to retrieve the pages from the database and register their paths as routes.

public class MvcApplication : HttpApplication
{
    public static void RegisterRoutes(RouteCollection routes, IEnumerable<Page> pages)
    {
        // ...

        // Page routes.
        foreach (Page page in pages)
            routes.MapRoute(
                "PagePageByPath-" + page.Id,
                page.Path,
                new { Controller = "page", Action = "pagebypath", page.Path });

        // I may need to specifically define my controllers and place them before the
        // page routes so they don't get accidentally occluded.
        routes.MapRoute(
            "Default",
            "{Controller}/{Action}/{Id}",
            new { Controller = "home", Action = "index", Id = UrlParameter.Optional });
    }

    protected void Application_Start()
    {
        AreaRegistration.RegisterAllAreas();
        RegisterRoutes(RouteTable.Routes, GetPages());
    }

    private IEnumerable<Page> GetPages()
    {
        // Fudge this to get it working. I can't access request scoped objects here.
        using (ISession session = Container.Get<SessionFactoryWrapper>().OpenSession())
            return session.Query<Page>()
                .ToArray();
    }
}

Because I've defined all the objects in my Spring IOC container with a request scope (apart from SessionFactoryWrapper), Spring won't let me play with them outside of a request (something about the Spring module not being initialised yet). The fudge is to create an NHibernate session directly.

Controller

I need five controller actions, one to display the page given its path, and four for creating and updating pages. The controller actions are what you would expect except the postpack actions involve an update of the route table after a save. Here's one example.

[HttpPost]
[UserIsAnAdministrator]
[ValidateInput(false)]
public ActionResult Edit(int id, FormCollection collection)
{
    Page page = Container.Get<IPageService>().GetPage(id);

    if (page == null)
    {
        Messages.Add(MessageType.Warning, "Page " + id + " not found.");

        return RedirectToAction("index", "home");
    }

    try
    {
        TryUpdateModel(page, new[] { "Version", "Path", "IsPublished", "Text" });

        if (!ModelState.IsValid)
            return View(page);

        Container.Get<IPageService>().UpdatePage(page);
        RegisterRoutes();
        Messages.Add("Page updated successfully.");

        return Redirect("/" + page.Path);
    }
    catch (Exception exception)
    {
        Messages.Add(exception);

        return View(page);
    }
}

A hard Redirect is used because a RedirectToAction doesn't work without doubling up the route data -- I'm not sure why.

The RegisterRoutes method looks like this.

private void RegisterRoutes()
{
    // Rebuild the route table.
    using (RouteTable.Routes.GetWriteLock())
    {
        RouteTable.Routes.Clear();
        MvcApplication.RegisterRoutes(RouteTable.Routes, Container.Get<IPageService>().GetPages());
    }
}

I tried to be selective about removing the old route and inserting a new one, but inserting a route with a name proved problematic. In the end, rebuilding the entire route table seemed the easiest way.

Views

Finally the views! If I create a page like this...

New

I get this.

PageByPath

See the short URL in the address bar? Love it.

There are 0 comments.


Comments

Leave a Comment

Please register or login to leave a comment.


Older
NHibernate Filters

Newer
Another View Model Refactor for Attachments

Older
NHibernate Filters

Newer
Another View Model Refactor for Attachments

browse with Pivot


About


Projects

Building Neno


RSS
Recent Posts

Codility Nitrogenium Challenge
OS X Lock
HACT '13
Codility Challenges
Priority Queue


Tags

Architecture (13)
ASP.NET (2)
ASP.NET MVC (13)
Brisbane Flood (1)
Building Neno (38)
C# (4)
Challenges (3)
Collections (1)
Communicator (1)
Concurrency Control (2)
Configuration (1)
CSS (5)
DataAnnotations (2)
Database (1)
DotNetOpenAuth (2)
Entity Framework (1)
FluentNHibernate (2)
Inversion of Control (5)
JavaScript (1)
jQuery (4)
Kata (2)
Linq (7)
Markdown (4)
Mercurial (5)
NHibernate (20)
Ninject (2)
OpenID (3)
OS X (1)
Pivot (6)
PowerShell (8)
Prettify (2)
RSS (1)
Spring (3)
SQL Server (5)
T-SQL (2)
Validation (2)
Vim (1)
Visual Studio (2)
Windows Forms (3)
Windows Service (1)


Archives


Powered by Neno, ASP.NET MVC, NHibernate, and small furry mammals. Copyright 2010 - 2011 Adam Boddington.
Version 1.0 Alpha (d9e7e4b68c07), Build Date Sunday, 30 January, 2011 @ 11:37 AM