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.
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);
}
}
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).
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>
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.
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.
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>
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.
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.
Finally the views! If I create a page like this...
I get this.
See the short URL in the address bar? Love it.
There are 0 comments.
Older
NHibernate Filters
Older
NHibernate Filters
browse with Pivot
Codility Nitrogenium Challenge
OS X Lock
HACT '13
Codility Challenges
Priority Queue
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)
Comments
Leave a Comment
Please register or login to leave a comment.