Building Applications Using Castle RC2: Part I

Well, now that Castle RC2 is out, let us see what this release can do for us, shall we? After my recent talk, I got forums heavy on my mind, so let us build one.

You can get the release here. I suggest that you would get the MSI installer. After installing the MSI, open Visual Studio and create a new project, you should see this screen:

(Image from clipboard).png

Choose Castle ActiveRecord Project and name the project "Castle.Forums.Model", name the solution (last text box) "Castle.Forums".

Note: Despite all my recent posts about Active Record + NHibernate 1.2 == Love (and Linq :-) ), the RC2 does not have these features, since they mostly rely on enhancements in NHibernate 1.2. Currently, we are focused on getting a stable release out of the door, and we don't want to add NHibernate 1.2 yet. Mostly because it is a moving target and that we want to have a stable release of Active Record before chasing the rainbow over the next hill.
Personally, I am using the trunk version for a lot of stuff, and I find it quite stable, but I like to live dangerously. Also, I can see the need for a feature and add it throughout the stack, which is what I am mostly doing now, seeing what new & exciting places I can take Active Record + NHibernate 1.2. Overall, I think that both frameworks benefits from this.

Here is the solution that was created:

(Image from clipboard).png

It created a test project for us! Yay! Let us start by looking at the AbstractModelTestCase class. It basically setting up everything that we need in order to start testing.  The only thing that we need to do here is just to modify InitFramework() method so it would include our model assembly, and only initialize once. Like this:

protected virtual void InitFramework()

{

       if(isInitialized )

              return;

 

       IConfigurationSource source = ActiveRecordSectionHandler.Instance;

 

       ActiveRecordStarter.Initialize(System.Reflection.Assembly.Load("Castle.Forums.Model"), source);

      

       isInitialized = true;

}

 

protected static bool isInitialized = false;

I also had to change the connection string in the app.config, to point to "test" instead of "mytestdatabase", which is the default. Note that you probably want to run the tests on a scratch database, since the default test behavior is to rebuild the schema at every test.

Now, let us build a list of tests that we want to have. I think that those are interesting;

  • Can create user (name, email, hashed password)
  • Can load user by name and password
  • Can create a forum (name, manager)
  • Can load list of forums
  • Can create a message (title, content, author, forum, parent, root)
  • Can load list of messages for forum.
  • Can load hierarchical list of the last ten root (start) messages for a forum.
Again, a reminder, this is using NHibernate 1.0.2, so no generic collections.

Create a new file called UserTestCase and add the following:

[TestFixture]

public class UserTestCase : AbstractModelTestCase

{

      

}

Now, let us start writing the real test:

[Test]

public void Can_Create_And_Load_User_By_ID()

{

       User user = new User("Ayende", "Ayende@example.com");

       user.SetPassword("Scott/Tiger");

       user.Create();

      

       Flush();

       CreateScope();//replace scope

 

       User userFromDb = User.Find(user.Id);

       Assert.IsFalse( ReferenceEquals(user, userFromDb), "got the same instnace even though we changed scopes!" );

 

 

       Assert.AreEqual( "Ayende", userFromDb.Name , "User name did not match");

       Assert.AreEqual("Ayende@example.com", userFromDb.Email, "Email did not match" );

       Assert.IsTrue( user.PasswordMatch("Scott/Tiger"), "Password did not match" );

}

So, I had written the test, and now I know how I want my code to look. At the moment, it can't compile, because it has no User class. Add a User class to the Castle.Forums.Model assembly. I only want to get this test to work, so here is the implementation of the User class:

[ActiveRecord("Users")]

public class User : ActiveRecordBase<User>

{

       int id;

       string name;

       string email;

       [Field]

       string hashedPassword;

 

       public User()

       {

 

       }

      

       public User(string name, string email)

       {

              this.name = name;

              this.email = email;

       }

 

       [PrimaryKey]

       public int Id

       {

              get { return id; }

              set { id = value; }

       }

 

       [Property]

       public string Name

       {

              get { return name; }

              set { name = value; }

       }

 

       [Property]

       public string Email

       {

              get { return email; }

              set { email = value; }

       }

 

       public void SetPassword(string password)

       {

              hashedPassword = Hash(password);

       }

 

       public bool PasswordMatch(string maybePassword)

       {

              return hashedPassword == Hash(maybePassword);

       }

 

       public static string Hash(string value)

       {

              return value;

       }

}

I don't have a test for the Hash() method yet, so I just had it return its input, now let us write a very simple test for it:

[Test]

public void Calling_User_Hash_Get_Diffrent_Return_Value()

{

       string hashed = User.Hash("Scott/Tigger");

       Assert.AreNotEqual("Scott/Tiger", hashed, "Password is the same even after hashing");

}

This one fails, obvious, so I add the following implementation:

public static string Hash(string value)

{

       SHA512 hasher = SHA512.Create();

       byte[] valueBytes = Encoding.UTF8.GetBytes(value);

       byte[] hashedBytes = hasher.ComputeHash(valueBytes);

       return Convert.ToBase64String(hashedBytes);

}

Now we need to load a user by name and password, let us write the test first:

[Test]

public void Can_Load_User_By_Name_And_Password()

{

       User user = new User("Ayende", "Ayende@example.com");

       user.SetPassword("Scott/Tiger");

       user.Create();

      

       Flush();

       CreateScope();

 

       User userFromDb = User.FindByNameAndPassword("Ayende", "Scott/Tiger");

       Assert.IsNotNull(userFromDb, "Could not find user by username and password");

}

Implementing FindByNameAndPassword...

public static User FindByNameAndPassword(string name, string password)

{

       string hashedPassword = Hash(password);

       return FindOne(Expression.Eq("Name", name),

                      Expression.Eq("hashedPassword", hashedPassword));

}

Looks like we are through with test for users, in a similar fashion, we are going to build the forums.

In ForumTestCase:

public override void Init()

{

       base.Init();

       this.user = new User("Ayende", "Ayende@example.org");

       this.user.SetPassword("Scott/Tiger");

       this.user.Create();

}

 

[Test]

public void Can_Create_And_Load_Forum()

{

       Forum forum = new Forum("My Forum", user);

       forum.Create();

      

       Flush();

       CreateScope();

 

       Forum forumFromDb = Forums.Find(forum.Id);

       Assert.AreEqual("My Forum", forumFromDb.Name, "Forum name not the same");

       //note: can't do a simple comparision because the arrive from different scopes.

       Assert.AreEqual(user.Id, forum.Manager.Id, "Forum manager is not the same" );

}

And the Forum class implementation:

               

[ActiveRecord]

public class Forum : ActiveRecordBase<Forum>

{

       int id;

       string name;

       User manager;

 

       public Forum()

       {

 

       }

 

       public Forum(string name, User manager)

       {

              this.name = name;

              this.manager = manager;

       }

 

       [PrimaryKey]

       public int Id

       {

              get { return id; }

              set { id = value; }

       }

 

       [Property]

       public string Name

       {

              get { return name; }

              set { name = value; }

       }

 

       [BelongsTo]

       public User Manager

       {

              get { return manager; }

              set { manager = value; }

       }

}

Along the way, we also go the second test for free:

[Test]

public void Can_Load_List_Of_Forums()

{

       Forum forum = new Forum("My Forum", user);

       forum.Create();

 

       Forum forum2 = new Forum("My Forum 2", user);

       forum2.Create();

 

       Forum[] allForums = Forum.FindAll();

 

       Assert.AreEqual(2, allForums.Length);

}

That is all for forums, now to messages. First, the test:

[Test]

public void Can_Create_And_Load_Message()

{

       Message msg = new Message("Hi there", "Hello World", user, forum, null);

       msg.Create();

      

       Flush();

       CreateScope();

 

       Message msgFromDb = Message.Find(msg.Id);

       Assert.AreEqual("Hi there", msgFromDb.Title , "Could not get title");

       Assert.AreEqual("Hello World", msgFromDb.Content, "Could not get content");

       Assert.AreEqual(user.Id, msg.Author.Id, "Author do not match" );

       Assert.AreEqual(forum.Id,  msg.Forum.Id, "Forum do not match");

      

       Assert.IsNull(msg.Parent, "Got a non null parent for a root message");

       Assert.IsNull(msg.RootParent, "Got a non null root parent for a root message");

}

Which is implemented by:

[ActiveRecord]

public class Message : ActiveRecordBase<Message>

{

       int id;

       string title;

       string content;

       User author;

       Forum forum;

       Message parent;

      

       /// <summary>

       /// This is the first message in a discussion thread

       /// </summary>

       Message rootParent;

      

       public Message()

       {

 

       }

 

       public Message(string title, string content, User user, Forum forum, Message parent)

       {

              this.title = title;

              this.content = content;

              this.author = user;

              this.forum = forum;

              this.parent = parent;

       }

 

       [Property]

       public string Title

       {

              get { return title; }

              set { title = value; }

       }

 

       [Property]

       public string Content

       {

              get { return content; }

              set { content = value; }

       }

 

       [BelongsTo]

       public User Author

       {

              get { return author; }

              set { author = value; }

       }

 

       [BelongsTo]

       public Forum Forum

       {

              get { return forum; }

              set { forum = value; }

       }

 

       [BelongsTo]

       public Message Parent

       {

              get { return parent; }

              set { parent = value; }

       }

 

       [BelongsTo]

       public Message RootParent

       {

              get { return rootParent; }

              set { rootParent = value; }

       }

 

       [PrimaryKey]

       public int Id

       {

              get { return id; }

              set { id = value; }

       }

}

Now, I can think of two scenarios that I didn't cover. First, the message should automatically set the root parent property if is it passed a non null parent, and it should set its root parent to the parent's root parent if that is not empty. Here are the tests:

[Test]

public void Ctor_Sets_Parent_And_Root_Parent_If_Not_Null()

{

       Message parent = new Message("Hi there", "Hello World", user, forum, null);

       Message msg = new Message("Hi there", "Hello World", user, forum, parent);

 

       Assert.AreEqual(parent, msg.Parent, "did not set parent correctly");

       Assert.AreEqual(parent, msg.RootParent, "did not set root parent correctly");

}

 

[Test]

public void Ctor_Sets_RootParent_To_Parent_Root()

{

       Message grandParent = new Message("Hi there", "Hello World", user, forum, null);

       Message parent = new Message("Hi there", "Hello World", user, forum, grandParent);

       Message msg = new Message("Hi there", "Hello World", user, forum, parent);

 

       Assert.AreEqual(parent, msg.Parent, "did not set parent correctly");

       Assert.AreEqual(grandParent, msg.RootParent, "did not set root parent correctly");

}

And their implementation, which is merely this addition to the constructor:

public Message(string title, string content, User user, Forum forum, Message parent)

{

       this.title = title;

       this.content = content;

       this.author = user;

       this.forum = forum;

       this.parent = parent;

       if(parent!=null)

       {

              this.rootParent = parent.RootParent ?? parent;

       }

}

Now, we need to be able to load messages for a forum, let us try:

[Test]

public void Can_Load_Messages_By_Forum()

{

       Message grandParent = new Message("Hi there", "Hello World", user, forum, null);

       grandParent.Create();

       Message parent = new Message("Hi there", "Hello World", user, forum, grandParent);

       parent.Create();

       Message msg = new Message("Hi there", "Hello World", user, forum, parent);

       msg.Create();

      

       Flush();

 

       Message[] messages = Message.FindAllByForum(forum);

       Assert.AreEqual(3, messages.Length );

}

And its implementation:

public static Message[] FindAllByForum(Forum forum)

{

       return FindAll(Expression.Eq("Forum", forum));

}

Now we only have to handle of finding all the last root messages in a forum:

[Test]

public void Can_Load_Last_Ten_Root_Messages()

{

       CreateNestedMessages();

      

       Flush();

       CreateScope();

 

       Message[] message = Message.FindLastRootsByForum(forum, 10);

 

       Assert.AreEqual(10, message.Length);

}

private void CreateNestedMessages()

{

       for (int i = 0; i < 12; i++)

       {

              Message grandParent = new Message("Hi there", "Hello World", user, forum, null);

              grandParent.Create();

              for (int j = 0; j < 2; j++)

              {

                     Message parent = new Message("Hi there", "Hello World", user, forum, grandParent);

                     parent.Create();

                     for (int k = 0; k < 1; k++)

                     {

                           Message msg = new Message("Hi there", "Hello World", user, forum, parent);

                           msg.Create();

                     }

              }

       }

}

The implementation is ridiciously short, actually:

public static Message[] FindLastRootsByForum(Forum forum, int count)

{

       return SlicedFindAll(0, count,

                                    Expression.IsNull("RootParent"),

                                    Expression.Eq("Forum", forum));

}

This is it! We now have ten tests, and a working domain model we can start using. A couple of things before we continue:

  • We dealt 99% of the time with tests / code
  • Except for the connection string, there was no need to handle configuration / references and other stuff that always clutter this examples.

Now, we want to create a the UI. For this, please create a new Castle MonoRail Project, and name it Castle.Forums.Web, you should then see the following dialog:

(Image from clipboard).png

Choose Brail (of course :-) ) and enable routing, we will use this a bit later, but let us be prepared. We don't need Windsor intergration, since this is a very small project. Admire the artwork for a while before pressing next.

(Image from clipboard).png

I don't really need to tell you that you are going to create a test project, right? Click finish and let us take a look at the projects that were created.

(Image from clipboard).png

Notice how structured this is? We already have the foundations of creating something really nice. Set the Castle.Forums.Web project to the startup project and run it. The ASP.Net Development Server will open, and you will see the boring default page.

I need to be able to work with users (since they are mandatory for everything else), but I don't want to invest any time in it at the moment. Let us just setup scaffolding. Here we need to do a bit of manual work:

Add the following references (all exist in the .NET tab in the Add References dialog) to the Castle.Forums.Web:

  • Castle.ActiveRecord
  • Castle.Components.Common.TemplateEngine
  • Castle.Components.Common.TemplateEngine.NVelocityTemplateEngine
  • Castle.MonoRail.ActiveRecordScaffold
  • Castle.MonoRail.ActiveRecordSupport
  • NHibernate

Now, add this to the <configSection> part in the web.config file:

<section name="activerecord"

               type="Castle.ActiveRecord.Framework.Config.ActiveRecordSectionHandler, Castle.ActiveRecord" />

And then add this at the bottom of the file:

<!-- For the configuration reference, check -->

<!-- http://www.castleproject.org/index.php/ActiveRecord:Configuration_Reference -->

<activerecord isWeb="true">

       <!-- The configuration below is good enough for MS SQL Server only -->

       <!-- Remember that you should use a test database, never use development or production -->

       <config>

              <add key="hibernate.connection.driver_class"

                      value="NHibernate.Driver.SqlClientDriver" />

              <add key="hibernate.dialect"

                      value="NHibernate.Dialect.MsSql2000Dialect" />

              <add key="hibernate.connection.provider"

                      value="NHibernate.Connection.DriverConnectionProvider" />

              <add key="hibernate.connection.connection_string"

                      value="Data Source=.;Initial Catalog=test;Integrated Security=SSPI" />

       </config>

</activerecord>

Now, go to the GlobalApplication.cs file and add the following to the Application_OnStart method:

IConfigurationSource source = ActiveRecordSectionHandler.Instance;

 

ActiveRecordStarter.Initialize(System.Reflection.Assembly.Load("Castle.Forums.Model"), source);

 

ActiveRecordStarter.CreateSchema();

Please note that the last line here will rebuild the database everytime that the application starts. This is great for testing / development, not so great if you are going to production, be aware of this.

We need to add a controller for the users, we will call it UserController and it looks like this:

[Layout("default"), Rescue("generalerror"), Scaffolding(typeof(User))]

public class UserController : Controller

{

}

Now that we have everything setup the way we want to, run the project, and then change the url from "/home/index.rails" to "/user/list.rails", you should see the following:

(Image from clipboard).png

It is not pretty, but it is functional, and it allows us to immediately start working with the data. Tomorrow, I'll post part II, and I'll try to discover how one can TDD a web application using MonoRail.

Print | posted on Wednesday, November 01, 2006 10:29 PM

Feedback


Gravatar

#  11/1/2006 11:15 PM Omer van Kloeten

Cool! Could you please post this demo project's source for download?


Gravatar

#  11/1/2006 11:23 PM David Hayden

Sweet!

I love the ActiveRecord implementation and I have been wondering about MonoRail. Great tutorial. I look forward to part II.


Gravatar

#  11/1/2006 11:45 PM hammett

Ayende, just a correction: the installer only adds two assemblies to the GAC: Castle.MonoRail.TestSupport and Cassini.dll. All others will be visible to VS.Net because we use the AssemblyFolders on the registry.

About MonoRail TDD, while the testsupport works, I'm not happy with it, and maybe there's a bunch of people who is not happy too. We could think in supporting a different approach.

The problem is: when I developed the test support I wanted that the test environment replacing the web.config (setting like a web-test.config when creating the ASP.Net AppDomain). But the damn Microsoft API does not allow that. So you end up &quot;testing&quot; the same site, and inheriting the configuration. Not good.


Gravatar

#  11/1/2006 11:52 PM knocte

This looks totally neat. I plan to give my first steps with MonoRail but I think I should try to implement an XSLT-based view engine. Do you think it will be useful? And how difficult to implement?


Gravatar

#  11/2/2006 2:14 AM Jason Finch

Great stuff!


Gravatar

#  11/2/2006 2:37 AM hammett

@knocte
Check http://svn.castleproject.org/svn/castlecontrib/viewengines/trunk/src/XsltViewEngine/


Gravatar

#  11/2/2006 6:57 AM Ken Egozi

@Ayende: super cool.
I'd add the configuration for SessionPerScope http module to the default schema of any web application using ActiveRecord, weather it's MonoRail or WebForms:
&lt;httpModules&gt;
&lt;add name=&quot;ar&quot; type=&quot;Castle.ActiveRecord.Framework.SessionScopeWebModule, Castle.ActiveRecord&quot; /&gt;
&lt;/httpModules&gt;

@Hammet and knocte: It's nice to see new types of engines. Nice to know. I've grabbed all the svn but didn't notice this one. Anyway - Brail has took me over completely and I'm totaly into it and using it for my projects. I like the ability to use &quot;real&quot; language for the view, like in RHTML.



Gravatar

#  11/2/2006 9:43 AM Dan

You got a spelling error in Calling_User_Hash_Get_Diffrent_Return_Value which will ensure its always green light.

@Hammet: Perhaps a custom section handler would help? I had a similar problem here at work and made a section handler which has static methods for choosing the configuration it should be using so I can swap, so my config looks something like this:

&lt;section name=&quot;nhibernate&quot; type=&quot;MyNamespace.ConfigurationSelectionSectionHandler, MyAssembly&quot;/&gt;

&lt;nhibernate type=&quot;System.Configuration.NameValueSectionHandler, System, Version=1.0.3300.0,Culture=neutral, PublicKeyToken=b77a5c561934e089&quot;&gt;
&lt;when config=&quot;bronze&quot;&gt;
&lt;add key=&quot;hibernate.connection.connection_string&quot; value=&quot;Server=.; Integrated Security=SSPI&quot;/&gt;
&lt;/when&gt;
&lt;when config=&quot;silver&quot;&gt;
&lt;add key=&quot;hibernate.connection.connection_string&quot; value=&quot;Server=QNA; Integrated Security=SSPI&quot;/&gt;
&lt;/when&gt;
&lt;when config=&quot;gold&quot;&gt;
&lt;add key=&quot;hibernate.connection.connection_string&quot; value=&quot;Server=Live; Integrated Security=SSPI&quot;/&gt;
&lt;/when&gt;
&lt;/nhibernate&gt;

And then when the program starts it either shows a dialog (if its the debug build) or just picks the gold config otherwise:

MyNamespace.ConfigurationSelection.SetSelectedSection(&quot;gold&quot;)


Gravatar

#  11/2/2006 1:01 PM hammett

That might be an idea, Dan. Gonna create a jira issue.


Gravatar

#  11/2/2006 2:13 PM Ayende Rahien

@Ken, this is a good idea.
I'll try to add that to the project template.


Gravatar

#  11/2/2006 8:51 PM Ayende Rahien

@Omer, I'll have the source for download when I finish with part 2.


Gravatar

#  11/6/2006 12:13 AM Aldwis

Hello everybody!

Why I got lot of errors?
I took lot of changes myself, for example I added the forum field to ForumTestCase class and added code to init() method to initialize it,
but at last I still don't get running,

when I first run it show &quot;Unable to locate action [index] on controller [user].&quot;
then I added a empty index method to the UserController class,
and run it shows &quot;Could not find a view with path user\index.boo&quot;.


Gravatar

#  11/6/2006 11:38 AM Aldwis

I've download the sourcecode and run it without any change, and it shows a &quot;Unexpected error&quot;: Could not perform FindAll for Forum...


Gravatar

#  11/6/2006 11:46 AM Ayende Rahien

You need to create the database, run the tests once, and it will create them


Gravatar

#  11/6/2006 11:40 PM Aldwis

Hi Ayende!

I've created the Database &quot;test&quot;, and even I tried to use sa in the connectionString, nothing is created in the Database, actually &quot;Could not perform FindAll for Forum&quot; when running it...

I'm sorry for these simple question, but I really didn't fount the answer in forum...

Comments have been closed on this topic.