Building Applications Using Castle RC2Part 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:
Choose Castle ActiveRecord Project and name the project "Castle.Forums.Model", name the solution (last text box) "Castle.Forums".
Here is the solution that was created:
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.
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:
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.
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.
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:
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.
Comments
Comment preview