Click here to Skip to main content
15,867,453 members
Articles / Programming Languages / C#
Article

Introducing Castle - Part II

Rate me:
Please Sign up or sign in to vote.
4.85/5 (25 votes)
26 Jan 200511 min read 180.4K   344   91   25
Demonstrates the usage of inversion of control and the Castle on Rails framework to build web solutions.

Introduction

This articles is a continuation of: Introducing Castle Part I. I'll go on explaining how inversion of control can simplify the development of applications, but now paying special attention to web applications.

I'll also explain more about Castle's container named Windsor and how its facilities can simplify the developer's life. You can see a list of implemented facilities here.

The MindDump Application

If we just talk about inversion of control without a real life example, things will soon get boring. Thus, let's create a simple yet functional web application. Why not another blog application? A few requirements that we need to accomplish:

  • The user must create an account and a blog to access the publishing area of the application.
  • The user must be logged on to post texts on his blog.
  • The blogs must be accessible from a friendly URL.
  • The system must "remember" the user for a few days, and yet provide the minimum of security.
  • Each blog can have its own theme.

There are a few options to persist data nowadays, but let's stick with the old and well-known databases. I'm going to use MySQL for the development.

This is the screenshot of the first page:

Image 1

This is the maintenance area where the user can publish new entries or edit old entries:

Image 2

The following are two different themes applied to the same blog:

Image 3

Image 4

NHibernate

One of the famous anti-patterns is the NIH, which stands for Not Invented Here. Sometimes, reinventing the wheel is important, even as an exercise, so you can have a deeper knowledge about something you don't know much about. For example, try to create your own compression algorithm, try to implement your own docking code for your windows, and so on.

However, reinventing the wheel for everything is a recipe for disaster, so we don't want to do that. One of Castle's goal is to provide integration with good projects out there for dealing with specific concerns. For example, for database access, we have facilities for NHibernate project and another one for iBatis.Net project. For this application, we're going to use the NHibernate facility.

Web Framework

Every time some one approaches me with a new super framework that is supposed to be the new way of doing whatever, I turn on my ultra-skeptical mode. That's reasonable for everyone, and I had them turned on when I started to develop some application with Ruby and then was introduced to Ruby on Rails web framework. Few hours later, I was astonished. How can someone come up with such a great idea and didn't win the Nobel of Science?

Ruby on Rails is a great framework, but in my humble opinion, not particularly because it solves a lot of common daily things, but because of its simplicity. It's so simple that it becomes predictable, which is extremely good in a framework. You don't have to read thousands of PDF pages to use it. Once you get the basic idea, you get used to it naturally.

"And how it works?" you may ask. Basically, it's a MVC framework, but without configuration files to connect the controller with actions and with views and... You got it. With Rails, you'll have just three entities to pay attention: controllers, models and views. From the URL being accessed, Rails will infer the controller name and the action being called. Actions are nothing but public methods on the controller. So suppose you access the following URL:

http://yoursite/home/index

This URL will instantiate the controller named HomeController and will invoke the method named index. There's a folder for views which uses exactly the same hierarchical definition: controller name plus the action name. If your method doesn't specify anything different, the view chosen will match the controller/action. Your action can use a different view, though.

Well, Ruby on Rails supports much more than this, but you got the idea. You might be thinking about how valuable this concept is, and I'll say, from my standpoint, that this is extremely valuable for applications. It enforces simple design and distinct responsibilities: controllers will orchestrate the invocation of the business objects and obtain data from the model; views will only present the data.

If you compare this approach with ASP.NET, you will see how those values were put off. Each ASP.NET WebForm is like a Visual Basic Form that handles everything: business object invocations - if you're lucky - data ostentation, data binding, input validation, generation of scripts, and at last, the presentation. Don't get me wrong, I like ASP.NET, the whole idea behind controls lifecycle is amazing, and the view state that simulates state in an inherent stateless environment is even greater.

But ASP.NET WebForms do not promote good design. You have to be very picky to not put logic that doesn't belong to the presentation on the WebForm so you don't end up with an unmanageable application.

Castle on Rails

Castle on Rails is an attempt to provide something similar to Ruby on Rails for the .NET world, but it's not a blind port. As Ruby offers all the beauties of a dynamic language, .NET offers the beauties of static types and attributes. So the goal is trying to combine both worlds.

I've used Castle on Rails on three applications and it has simplified my life and simplified what I code. My controllers end up with little methods and less than a hundred lines of code.

Please note that Castle on Rails does not intend to replace WebForms, in fact, it uses WebForms as one of the available views engine. It also supports NVelocity as a view engine. My personal choice is NVelocity as it stands exactly as it is supposed to be - just presentation processing.

How it works

The working of Rails is pretty straightforward and intentionally similar to the original Ruby on Rails: you have to provide Controllers and Views. Each controller should deal with one specific concern of your web application. The public methods exposed by the controller are accessible actions. The following is a sample controller code:

C#
public class HomeController : Controller
{
  public void Index()
  {
  }
  
  public void ContactUs()
  {
  }
}

This specific controller can be accessed from the URL: http://yourserver/home/index.rails or http://yourserver/home/contactus.rails (it's case insensitive). The action body should perform any logic (gathering data, creating or updating table rows, and so on), and then, after the method process, the framework will execute the view. For the above controller, Castle on Rails will search for an ASPX on the view directory: views\home\index.aspx. If you're using the NVelocity view engine, then it will look for views\home\index.vm. The view must exist, otherwise the framework will throw an exception.

The controller can use a different view, though. The code below will render the index view, ignoring the invoked action:

C#
public class HomeController : Controller
{
  public void Index()
  {
  }
  
  public void ContactUs()
  {
    RenderView("index");
  }
}

Instead of extending the Controller class, you can also use the SmartDispatcherController. This specific Controller will try to match the parameters on the request to your method arguments, for example:

C#
public class AccountController : SmartDispatcherController
{
  public void New()
  {
  }
  
  public void CreateAccount(String name, String login, String password)
  {
    ...
  }
}

For the above controller, you can use the URL: http://yourserver/account/createaccount.rails?name=hammett&login=hammett&password=mypass.

Castle on Rails also supports:

  • Filters allowing you to associate a pre and post processing of the action.
  • Rescues allowing you to associate an error page per controller and per method.
  • Layouts allowing you to associate a master page.

We are going to use most of these features to achieve the requirement for our application, so rest ensured that they will be covered somehow. Castle on Rails can be used as a standalone framework, or integrated with WindsorContainer. This allows you to use the juicy abilities of an inversion of control container.

The damn simple "architecture"

No fancy things, please! We just need a few layers:

  1. Presentation: composed of Controllers and Filters.
  2. Services: just a business abstraction layer between the Presentation and Data access. It's also responsible for the transaction boundaries.
  3. Data Access Objects: or simple DAOs. We're going to use NHibernate to actually save, update and request data.

However, we'll use a fancy publisher/subscriber just to know when a new blog was created. This is important so we can register a new controller for it.

The following picture depicts the components and their interaction:

Image 5

By the way, the picture is a screenshot of a desktop application called Castle's Component Viewer. It instantiates your application's container and obtains its Component Graph. The Component Viewer is still on early stages, but will be a good addition to the whole Castle Project.

The project structure

I hope you agree with me, but I think we need the following separated projects:

  • Castle.Applicatins.MindDump: to hold Controllers, DAOs, Services.
  • Castle.Applicatins.MindDump.Tests: to test the DAOs and Services - we could also test the Controllers.
  • Castle.Applicatins.MindDump.Web: ASP.NET application with the views.

Now that we've agreed, let's go on.

Creating the MindDump application

Of course, I won't explain each step taken to create the application, but I intend to illustrate a few of them so you can easily learn the rest from the source code provided.

First of all, I've created two configuration files, one for production and one for the test cases. I've also used two Catalogs on MySQL, so the test cases could delete all the rows before starting the tests.

The configuration files were used to configure the NHibernate facility, see below:

XML
<facilities>
    <facility id="nhibernate">
        <factory id="nhibernate.factory">
            <settings>
                <item key="hibernate.connection.provider">
                  NHibernate.Connection.DriverConnectionProvider
                </item>
                <item key="hibernate.connection.driver_class">
                  NHibernate.Driver.MySqlDataDriver
                </item>
                <item key="hibernate.connection.connection_string">
                  Database=minddump;Data Source=localhost;
                                    User Id=theuser;Password=user
                </item>
                <item key="hibernate.dialect">
                  NHibernate.Dialect.MySQLDialect
                </item>
            </settings>
            <resources>
                <resource name="..\bin\Author.hbm.xml" />
                <resource name="..\bin\Blog.hbm.xml" />
                <resource name="..\bin\Post.hbm.xml" />
            </resources>
        </factory>
    </facility>
</facilities>

A good practice to simplify your development is to extend the WindsorContainer just to provide your initialization semantics and preferences and in a small reusable unit. For example, for this project, I've created the MindDumpContainer class. If you just instantiate it, it will use the production configuration file. Or you can use the other constructor to specify a different file - the test cases do exactly that.

C#
public class MindDumpContainer : WindsorContainer
{
    public MindDumpContainer() : 
           this( new XmlConfigurationStore("../app_config.xml") )
    {
    }

    public MindDumpContainer(IConfigurationStore store) : base(store)
    {
        Init();
    }

    public void Init()
    {
        RegisterFacilities();
        RegisterComponents();
    }

    ...

DAOs and Services

Now, I assume that you have some knowledge about NHibernate. To access the database, you need to create and open a NHibernate's Session, perform your work, and finally close it. Castle's NHibernate Facility can take care of this repetitive task, you just need to use the UsesAutomaticSessionCreation attribute. Follows the AuthorDao's code:

C#
[UsesAutomaticSessionCreation]
public class AuthorDao
{
    public virtual Author Create(Author author)
    {
        ISession session = SessionManager.CurrentSession;

        if (author.Blogs == null)
        {
            author.Blogs = new ArrayList();
        }

        session.Save(author);

        return author;
    }

    public virtual IList Find()
    {
        return SessionManager.CurrentSession.Find("from Author");
    }

    ...
}

The others DAOs don't have anything much different from this one. We are now free to code the service layer. If the classes involve modification in more than one DAO, or more than one modification on the same DAO, it's necessary to use transactions. The NHibernate Facility detects a transaction on the thread and enlists the Session automatically. So here is the service responsible for creating an account and a blog:

C#
[Transactional]
public class AccountService
{
    private AuthorDao _authorDao;
    private BlogDao _blogDao;

    public AccountService(AuthorDao authorDao, BlogDao blogDao)
    {
        _authorDao = authorDao;
        _blogDao = blogDao;
    }

    [Transaction(TransactionMode.Requires)]
    public virtual void CreateAccountAndBlog( Blog blog )
    {
        _authorDao.Create( blog.Author );
        _blogDao.Create( blog );
    }

    ...
}

Easy, huh?

The Controllers

The start point of our application lies on the Intro controller. It does do much, just presents a list of recent blogs and recent posts. Please remember that the constructor's arguments will be resolved by the container.

C#
[Layout("default")]
public class IntroController : Controller
{
    private BlogService _blogService;

    public IntroController(BlogService blogService)
    {
        _blogService = blogService;
    }

    public void Index()
    {
        PropertyBag.Add( "blogs", _blogService.ObtainLatestBlogs() );
        PropertyBag.Add( "posts", _blogService.ObtainLatestPosts() );
    }
}

On this application, we're using the NVelocity View Engine, so follows the contents of the view Intro\Index.vm:

HTML
<p>
MindDump is a simple blog application. To create your blog and start to dump 
your thoughts and opinions, please create 
<a href="/account/new.rails">an account</a>.
</p>
<p>
If you have an account, 
please <a href="/account/authentication.rails">log on</a>
</p>
<br>
<font size="+1">Last updates</font>
<br>
<p>
<table width="100%" border="1">
#foreach( $post in $posts )
  <tr>
    <td>
      <a href="/${post.blog.author.login}/view.rails?entryid=${post.id}"
      >$post.title
    </td>
    <td>$post.date</td>
  </tr>
#end
</table>
</p>
<br>
<font size="+1">Blogs registered</font>
<br>
<p>
<table width="100%" border="1">
#foreach( $blog in $blogs )
  <tr>
    <td><a href="/$blog.author.login/view.rails">$blog.name</td>
    <td>$blog.description</td>
  </tr>
#end
</table>

As you can see, this is just the child contents of a bigger page. That's because the Controller is using the Layout feature. As you can see, it's using the "default" layout. In practice, the NVelocity will look for a "default.vm" under the layouts directory:

HTML
<html>
<head>
<title>MindDump</title>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
<link href="/css/main.css" rel="stylesheet" type="text/css">
</head>

<body>
<div>
  <img src="/images/logo.jpg">
</div>
<br> <br>

$childContent
    
<p>
<br> <br>
<hr width="80%" size="1" noshade>
<div align="center">
Copyright (c) 2005 - Castle Project Team</div>
</p>

</body>
</html>

Secured areas

We have the following requirement to keep in mind: "the user must be logged on to post texts on his blog". We can perform this check on the sensitive actions on each controller or we can think about a more clever solution. Well, I'd say that the best way to solve this problem is by creating a filter that checks whether the user trying to access the Controller is authenticated. If he's not, the filter redirects the user to a login page and returns false, thus preventing the action of being processed.

C#
public class AuthenticationCheckFilter : IFilter
{
    private EncryptionService _encryptionService;
    private AccountService _accountService;

    public AuthenticationCheckFilter(AccountService accountService, 
        EncryptionService encryptionService)
    {
        _accountService = accountService;
        _encryptionService = encryptionService;
    }

    public virtual bool Perform(ExecuteEnum exec, 
           IRailsEngineContext context, Controller controller)
    {
        if (!PerformAuthentication(context))
        {
            context.Response.Redirect("account", "authentication");
            return false;
        }

        return true;
    }

    protected bool PerformAuthentication(IRailsEngineContext context)
    {
        String contents = 
               context.Request.ReadCookie("authenticationticket"); 

        if (contents == null)
        {
            return false;
        }

        String login = _encryptionService.Decrypt(contents);

        Author author = _accountService.ObtainAuthor(login);

        if (author == null)
        {
            return false;
        }

        context.CurrentUser = new PrincipalAuthorAdapter(author);
        
        return true;
    }
}

Then we create a base class for every controller that needs an authenticated user and that's it:

C#
[Filter(ExecuteEnum.Before, typeof(AuthenticationCheckFilter) )]
public abstract class AbstractSecureController : SmartDispatcherController
{

}

The most complex Controller is exactly the one responsible for creating, changing an account and performing the authentication. It has 120 lines of code :-)

Also note that some of the methods are decorated with SkipFilter so they can be accessed without the authentication checking.

C#
[Layout("default")]
public class AccountController : AbstractSecureController
{
    private AccountService _accountService;
    private AuthenticationService _authenticationService;
    private EncryptionService _encryptionService;

    public AccountController(AccountService accountService, 
        AuthenticationService authenticationService, 
        EncryptionService encryptionService)
    {
        _accountService = accountService;
        _authenticationService = authenticationService;
        _encryptionService = encryptionService;
    }

    [SkipFilter]
    public void New()
    {
    }

    [SkipFilter]
    [Rescue("errorcreatingaccount")]
    public void CreateAccount(String login, String name, String email,
                          String pwd, String pwd2, String blogname,
                          String blogdesc, String theme)
    {
        // Perform some simple validation
        if (!IsValid(login, name, email, pwd, 
                pwd2, blogname, blogdesc, theme))
        {
            RenderView("new");
            return;
        }

        Author author = new Author(name, login, pwd);
        Blog blog = new Blog(blogname, blogdesc, theme, author);

        _accountService.CreateAccountAndBlog( blog );

        // Done, now lets log on into the system
        PerformLogin(login, pwd);
    }

    [SkipFilter]
    public void Authentication()
    {
    }

    [SkipFilter]
    public void PerformLogin(String login, String pwd)
    {
        if (!_authenticationService.Authenticate(login, pwd))
        {
            Context.Flash["errormessage"] = 
                  "User not found or incorrect password.";
        
            RenderView("Authentication");
        }
        else
        {
            DateTime twoWeeks = DateTime.Now.Add( new TimeSpan(14,0,0,0) ); 

            Context.Response.CreateCookie("authenticationticket", 
                _encryptionService.Encrypt(login), twoWeeks );
            
            Redirect("Maintenance", "newentry");
        }
    }
    ...
}

I'm afraid now you know as much about Castle on Rails as I do :-)

The test cases

Testing a loosely coupled application is very simple. For example, follows a few test cases for one of the DAOs:

C#
[TestFixture]
public class AuthorTestCase : BaseMindDumpTestCase
{
    [Test]
    public void Create()
    {
        ResetDatabase();

        AuthorDao dao = (AuthorDao) Container[ typeof(AuthorDao) ];
        Assert.AreEqual( 0, dao.Find().Count );

        Author author = new Author("hamilton verissimo", "hammett", "mypass");

        dao.Create( author );

        IList authors = dao.Find();
        Assert.AreEqual( 1, authors.Count );

        Author comparisson = (Author) authors[0];
        Assert.AreEqual( author.Name, comparisson.Name );
        Assert.AreEqual( author.Login, comparisson.Login );
        Assert.AreEqual( author.Password, comparisson.Password );
    }

    ...

I've lived in hell five years ago when we had to cope with a complex and untesteable application in C++. It's incredible how much one can learn from their bad experiences... ;-)

The Web Application

In order to use Castle on Rails with the WindsorContainer, you need to:

  • Add a few entries to web.config.
  • Make the container available through the HttpApplication.

Might sound complex, but it isn't.

First, the web.config. You need to say to Castle on Rails which View engine you wish to use and which Controller and Filter Factories it should use. Don't forget to associate the extension "rails" with the ASP.NET ISAPI if you're using IIS. Follows the web.config contents:

XML
<?xml version="1.0" encoding="utf-8" ?> 
<configuration>

  <configSections>
    <section name="rails"
    type="Castle.CastleOnRails.Engine.Configuration.RailsSectionHandler, 
              Castle.CastleOnRails.Engine" />
  </configSections>

  <rails>
    <viewEngine 
      viewPathRoot="views" 
      customEngine=
       "Castle.CastleOnRails.Framework.Views.NVelocity.NVelocityViewEngine, 
        Castle.CastleOnRails.Framework.Views.NVelocity" />
    <customFilterFactory 
      type="Castle.CastleOnRails.WindsorExtension.WindsorFilterFactory, 
            Castle.CastleOnRails.WindsorExtension" />
    <customControllerFactory 
      type="Castle.CastleOnRails.WindsorExtension.WindsorControllerFactory, 
            Castle.CastleOnRails.WindsorExtension" />
  </rails>

  <system.web>
    <httpHandlers>
      <add verb="*" path="*.rails" 
      type="Castle.CastleOnRails.Engine.RailsHttpHandlerFactory, 
            Castle.CastleOnRails.Engine" />
    </httpHandlers>
  </system.web>

</configuration>

Now, just create a global.asax if you don't have one, and associate the following code with it:

C#
public class MindDumpHttpApplication : HttpApplication, IContainerAccessor
{
    private static WindsorContainer container;

    public void Application_OnStart() 
    {
        container = new MindDumpContainer();
    }

    public void Application_OnEnd() 
    {
        container.Dispose();
    }

    public IWindsorContainer Container
    {
        get { return container; }
    }
}

Conclusion

The sample code will probably lower your skeptical level. In the next article, I'll talk about the ActiveRecord Facility and anything else you might consider important.

Castle still is on the beta stage, but the public API is very unlikely to change. The team is growing as we develop a healthy community.

Please contact the team through the mailing list on Castle Project site.

History

  • 25-Jan-2005 - Initial version.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Web Developer
Brazil Brazil
Hamilton Verissimo has been working with software development for 9 years. He's involved with a lot of open source projects to fill his spare time.

Comments and Discussions

 
QuestionAdditional references Pin
Arash I14-Jul-14 3:27
Arash I14-Jul-14 3:27 
Generalasp.net 4 Pin
Forve_Avar12-May-11 0:11
Forve_Avar12-May-11 0:11 
GeneralCastle's Component Viewer Pin
Wualla20-Jun-07 7:56
Wualla20-Jun-07 7:56 
QuestionActiveRecord developer's guid Pin
li_robert14-Apr-07 4:54
li_robert14-Apr-07 4:54 
Question.NET 2.0 Version? Pin
Mateusz www.Kierepka.pl19-Dec-05 20:03
Mateusz www.Kierepka.pl19-Dec-05 20:03 
AnswerRe: .NET 2.0 Version? Pin
Hamilton Verissimo19-Dec-05 21:51
Hamilton Verissimo19-Dec-05 21:51 
JokeRe: .NET 2.0 Version? Pin
Mateusz www.Kierepka.pl19-Dec-05 22:38
Mateusz www.Kierepka.pl19-Dec-05 22:38 
GeneralRe: .NET 2.0 Version? Pin
Hamilton Verissimo20-Dec-05 2:43
Hamilton Verissimo20-Dec-05 2:43 
QuestionRe: .NET 2.0 Version? Pin
li_robert14-Apr-07 9:44
li_robert14-Apr-07 9:44 
QuestionSo good job, when the part III? Pin
Alex Jones5-Sep-05 21:24
Alex Jones5-Sep-05 21:24 
QuestionDatabase Script? Pin
mgaerber4-Jul-05 4:00
mgaerber4-Jul-05 4:00 
AnswerRe: Database Script? Pin
Hamilton Verissimo4-Jul-05 5:35
Hamilton Verissimo4-Jul-05 5:35 
GeneralRe: Database Script? Pin
Diamondo25-Nov-06 0:05
Diamondo25-Nov-06 0:05 
GeneralIt's a problem to use NVelocity Pin
cklein13-Apr-05 6:49
cklein13-Apr-05 6:49 
GeneralRe: It's a problem to use NVelocity Pin
Hamilton Verissimo13-Apr-05 7:11
Hamilton Verissimo13-Apr-05 7:11 
GeneralIssues for Shared Hosting Pin
Anonymous2-Mar-05 5:31
Anonymous2-Mar-05 5:31 
GeneralRe: Issues for Shared Hosting Pin
Hamilton Verissimo2-Mar-05 5:58
Hamilton Verissimo2-Mar-05 5:58 
QuestionsException? Pin
mmkk27-Feb-05 23:38
mmkk27-Feb-05 23:38 
AnswerRe: sException? Pin
Hamilton Verissimo28-Feb-05 1:35
Hamilton Verissimo28-Feb-05 1:35 
GeneralRe: sException? Pin
mmkk28-Feb-05 14:37
mmkk28-Feb-05 14:37 
GeneralRe: sException? Pin
Hamilton Verissimo28-Feb-05 15:12
Hamilton Verissimo28-Feb-05 15:12 
GeneralRe: sException? Pin
mmkk28-Feb-05 16:27
mmkk28-Feb-05 16:27 
GeneralRe: sException? Pin
Hamilton Verissimo28-Feb-05 16:51
Hamilton Verissimo28-Feb-05 16:51 
GeneralRe: sException? Pin
Hamilton Verissimo1-Mar-05 17:16
Hamilton Verissimo1-Mar-05 17:16 
GeneralRe: sException? Pin
mmkk28-Feb-05 16:29
mmkk28-Feb-05 16:29 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.