Mark Gilbert's Blog

Science and technology, served light and fluffy.

One web.config to rule them all – eh, not so fast

Well, I thought I had the hard parts worked out.

In my previous post I threw out this little bit of hand-waving:

The hard part of this was not figuring out how to put 5 environments’ worth of values into a single web.config – that’s already a solved problem in my shop (and I’m sure you could come up with your own approach).  The hard part here was figuring out to programmatically override the values that I would normally put into a web.config.

Well, as it turns out, changing out the values at runtime was the hard part here.  In fact, when it came to configuring ELMAH to generate email notifications, it was nearly insurmountable.

The mechanisms that we’ve developed to handle a "dynamic" web.config are all centered on the assumption that we can do the following (another gem I wrote in the last post):

At runtime, figure out the URL that the site is being executed under, and look up the values for that URL in the web.config.

And here’s where things fell apart.  You see,

  • In order to figure out the current URL, I needed a Request object to inspect.
  • The soonest I could get at the Request was the Application’s Begin_Request event.
  • The ELMAH module, however, was getting initialized before Application.Start, let alone Begin_Request.
  • Once initialized, ELMAH cached the now-incorrect settings, and provides no mechanism to force an update after the fact.

If I can’t override the settings when the application first starts up, how about forcing an override when the first error occurs?  There was an ELMAH event for that.  Unfortunately, being a module, the ELMAH email module ends up processing the error message AFTER the Request has already been processed, so it’s no longer available.

Rather than trying to come up with an even more elaborate (aka "convoluted") mechanism for saving off the current environment and then overriding ELMAH’s cache when the time came, I considered modifying the ELMAH source to initialize later, have a way to update the cached settings, etc.  Unfortunately, I had already sunk too much time trying to get this to work, so I opted for a much simpler solution – a simple email method of my own device, invoked in the Application.Error handler.  It functions, and occurs while the Request is still available, so I had a much easier time wiring it into the one web.config structure I had in place.  It’s not as robust as ELMAH, and certainly isn’t the way I wanted to go with it.  After using ELMAH for dozens of sites over the last few years, I had come to rely on it so completely.  It felt quite odd to have to go without it.

The good news is that the ELMAH mail module was the only one to give me trouble.  The logging module CAN be updated on Application.Begin_Request.  Although, even there I went a different route.  Instead of logging the errors to the file system, I opted to use the In-Memory provider.  Here is my revised web.config (only the ELMAH-specific bits are shown):

<?xml version="1.0"?>
<configuration>
  <configSections>

    <!-- 
            Error Logging Modules and Handlers (ELMAH) 
            Copyright (c) 2004-7, Atif Aziz.
            All rights reserved.
        -->
      <sectionGroup name="elmah">
        <section name="errorLog" requirePermission="false" type="Elmah.ErrorLogSectionHandler, Elmah"/>
        <section name="security" requirePermission="false"  type="Elmah.SecuritySectionHandler, Elmah"/>
        <section name="errorFilter" requirePermission="false" type="Elmah.ErrorFilterSectionHandler, Elmah"/>
      </sectionGroup>
  </configSections>

  
  <system.web>
    <httpHandlers>
      <add verb="POST,GET,HEAD" path="errors/report.axd" type="Elmah.ErrorLogPageFactory, Elmah"/>
    </httpHandlers>

    <httpModules>
      <add name="ErrorLog" type="Elmah.ErrorLogModule, Elmah"/>
      <add name="ErrorFilter" type="Elmah.ErrorFilterModule, Elmah"/>
    </httpModules>

  </system.web>


  <elmah>
    <errorLog type="Elmah.MemoryErrorLog, Elmah" />
    <security allowRemoteAccess="yes"/>
    <errorFilter>
      <test>
        <equal binding="HttpStatusCode" value="404" type="Int32"/>
      </test>
    </errorFilter>
  </elmah>

  
  <system.webServer>

    <modules runAllManagedModulesForAllRequests="true">
      <add name="ErrorLog" type="Elmah.ErrorLogModule, Elmah"/>
      <add name="ErrorFilter" type="Elmah.ErrorFilterModule, Elmah"/>
    </modules>

    <handlers>
      <add name="ElmahErrorReportingPage" verb="POST,GET,HEAD" path="errors/report.axd" type="Elmah.ErrorLogPageFactory, Elmah"/>
    </handlers>
  </system.webServer>

</configuration>

Since there was no longer any difference between the environments, I didn’t have to override anything.

The ActiveRecord initialization also seemed to go in as I expected.  Both ELMAH logging and ActiveRecord have been in place and working in our environments for a solid week now.

So lesson learned – get the code actually working before I blog on it.

Advertisements

October 17, 2011 Posted by | Castle ActiveRecord, Visual Studio/.NET | Comments Off on One web.config to rule them all – eh, not so fast

One web.config to rule them all

Most of the web-centric work I’ve done in my career and especially in the last four years has involved developing sites that are designed to be deployed to multiple environments: my local workstation, our internal Dev and Staging servers, and up to three environments at the client.  One of my strategies for managing the differences in file paths, email recipients, database connection strings, etc. among all of those environments is to push anything that changes from one environment to another into the web.config.  Then, I have a separate web.config for each environment, and I rename (or have the client’s technical staff rename) the appropriate file for a given environment.

TheSetup
That system has worked well for years.  That is, until a few weeks ago when I was assigned to a new client who insists that there only be a single web.config that covers all environments.  Doing this allows them to simply copy the files from one environment to the next wholesale, and eliminates the need to rename anything.  The other developers who have worked with this client for a while have created a couple of different frameworks for implementing this requirement, but they boil down to the same basic approach:

1) Put all values for all environments into the web.config, but tie them to the corresponding URL for each environment.

2) At runtime, figure out the URL that the site is being executed under, and look up the values for that URL in the web.config.

My colleagues affectionately refer to this scheme as “one web.config to rule them all”.

TheChallenge
A couple of the key components that I incorporate into the sites I work on are ELMAH and Castle ActiveRecord.  Naturally, since my current task involves building three brand new sites, I wanted to drop these in from the beginning.  The challenge was how to use them given this client’s requirement.  The hard part of this was not figuring out how to put 5 environments’ worth of values into a single web.config – that’s already a solved problem in my shop (and I’m sure you could come up with your own approach).  The hard part here was figuring out to programmatically override the values that I would normally put into a web.config.

TheSolution – ELMAH
Let’s start with ELMAH.  Normally, I’d have these sections in my web.config (only the ELMAH-specific portions are shown here):

<configuration>

  <configSections>
    <!-- 
            Error Logging Modules and Handlers (ELMAH) 
            Copyright (c) 2004-7, Atif Aziz.
            All rights reserved.
        -->
    <sectionGroup name="elmah">
      <section name="errorLog" requirePermission="false" type="Elmah.ErrorLogSectionHandler, Elmah"/>
      <section name="errorMail" requirePermission="false" type="Elmah.ErrorMailSectionHandler, Elmah"/>
      <section name="security" type="Elmah.SecuritySectionHandler, Elmah"/>
      <section name="errorFilter" type="Elmah.ErrorFilterSectionHandler, Elmah"/>
    </sectionGroup>
  </configSections>

  
  <system.web>
    <httpModules>
      <add name="ErrorLog" type="Elmah.ErrorLogModule, Elmah"/>
      <add name="ErrorMail" type="Elmah.ElmahMailModule"/>
      <add name="ErrorFilter" type="Elmah.ErrorFilterModule, Elmah"/>
     </httpModules>
  </system.web>

  <elmah>
    <errorLog type="Elmah.XmlFileErrorLog, Elmah" logPath="~/bin/Logs"/>

 


 <errorMail from="me@blah.com"
 to="blah@blah.com"
 subject="Test Error"
 smtpServer="blah.com"/>
 <security allowRemoteAccess="yes"/>
 <errorFilter>
 <test>
 <equal binding="HttpStatusCode" value="404" type="Int32"/>
 </test>
 </errorFilter>
 </elmah>

 

</configuration>
 

The things that differ per environment are:

*) The “logPath” attribute of the elmah/errorLog tag

*) The “to”, “subject”, and “smtpServer” properties of the “elmah/errorMail” tag.

My colleague, Joel, found that you can write a class that inherits from Elmah.ErrorMailModule, override the settings there, and use that in the httpModules block.  First, the class:

Public Class ElmahMailExtension
    Inherits Elmah.ErrorMailModule

    Protected Overrides Function GetConfig() As Object
        Dim o As Object = MyBase.GetConfig()
        o("smtpServer") = "mail.blah.com"
        o("subject") = String.Format("Blah message at {0}", Now.ToLongTimeString())
        o("to") = "me@blah.com"
        Return o
    End Function
End Class

And the web.config modification:

<configuration>
 
  <system.web>

    <httpModules>
      <add name="ErrorLog" type="Elmah.ErrorLogModule, Elmah"/>
      <add name="ErrorMail" type="MvcApplication1.ElmahMailExtension"/>
      <add name="ErrorFilter" type="Elmah.ErrorFilterModule, Elmah"/>
     </httpModules>

  </system.web>

</configuration>

Simple.

Overriding the logging settings requires a slightly different tack.  Instead of inheriting from Elmah.ErrorLogModule, I create a class that inherits from Elmah.XmlFileErrorLog:

Imports System.Web.Hosting

Public Class ElmahLogExtension
    Inherits Elmah.XmlFileErrorLog

    Public Sub New(ByVal config As IDictionary)
        MyBase.New(HostingEnvironment.MapPath("~/bin/Logs2"))
    End Sub

    Public Sub New(ByVal logPath As String)
        MyBase.New(logPath)
    End Sub

End Class

I couldn’t find a convenient collection to change values in, so I cheated.  Using JustDecompile, I looked at what the two constructors were doing.  They basically just manipulate the log path passed in.  So, I leave the New(string) variant alone, and modify the New(IDictionary) variant to ignore the incoming “config” parameter, and substitute the path that I want to use.  One of the things that I noticed the XmlFileErrorLog constructor doing was replacing paths with leading “~/” with the full path on the file system.  Full log paths won’t require this.
TheSolution – ActiveRecord

Here is a common ActiveRecord configuration for me (just the ActiveRecord-relevant parts are shown here):

<configuration>

  <configSections>
    
    <section name="activerecord" type="Castle.ActiveRecord.Framework.Config.ActiveRecordSectionHandler, Castle.ActiveRecord" />
    <section name="nhibernate" type="System.Configuration.NameValueSectionHandler, System, Version=1.0.5000.0,Culture=neutral, PublicKeyToken=b77a5c561934e089" />
    <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net" />

  </configSections>

  <activerecord isDebug="false" threadinfotype="Castle.ActiveRecord.Framework.Scopes.HybridWebThreadScopeInfo, Castle.ActiveRecord">
    <config database="MsSqlServer2005" connectionStringName="MyTestDB">
    </config>
  </activerecord>

  <log4net>
  ...

  </log4net>

  <nhibernate>
  ... 
  </nhibernate>

  <connectionStrings>
    <add key="MyTestDB" value="Database=MyDBName;Server=MyServer,1433;User ID=MyUser;Password=MyPassword;" />
  </connectionStrings>

</configuration>

My Application_Start method in Global.asax initializes ActiveRecord:

Sub Application_Start()
    AreaRegistration.RegisterAllAreas()

    Dim MyConfig As IConfigurationSource = Castle.ActiveRecord.Framework.Config.ActiveRecordSectionHandler.Instance
    Dim MyAssemblies As System.Reflection.Assembly() = New System.Reflection.Assembly() {System.Reflection.Assembly.Load("MvcApplication1")}
    ActiveRecordStarter.Initialize(MyAssemblies, MyConfig)
    AddHandler Me.EndRequest, AddressOf Application_EndRequest

    RegisterRoutes(RouteTable.Routes)
End Sub

The primary piece that I need to override is the connection string.  My first attempts were in a similar vein as with ELMAH – in this case create a class that inherits from Castle.ActiveRecord.Framework.Config.ActiveRecordSectionHandler, and use that in the web.config.  However, I found an even easier way – simple use a different IConfigurationSource object in the call to ActiveRecordStarter.Initialize – one that is constructed programmatically.  As it turns out, there is even a built-in class to do this – InPlaceConfigurationSource:

Sub Application_Start()
    AreaRegistration.RegisterAllAreas()

Dim MyConfig As InPlaceConfigurationSource = InPlaceConfigurationSource.Build(DatabaseType.MsSqlServer2005, “Database=MyTest;Server=blahsqlsrvr,1433;User ID=blah;Password=blah;”) MyConfig.ThreadScopeInfoImplementation = GetType(Framework.Scopes.HybridWebThreadScopeInfo)

    Dim MyAssemblies As System.Reflection.Assembly() = New System.Reflection.Assembly() {System.Reflection.Assembly.Load("MvcApplication1")}
    ActiveRecordStarter.Initialize(MyAssemblies, MyConfig)
    AddHandler Me.EndRequest, AddressOf Application_EndRequest

    RegisterRoutes(RouteTable.Routes)
End Sub

Setting the ThreadScopeInfoImplemention property allows me to reproduce the “threadinfotype” property of the <activerecord> block in the web.config.

Using this allows me to completely dump the <activerecord> block and the configSection/section that references it:

<configuration>

  <configSections>
    
    <!--<section name="activerecord" type="Castle.ActiveRecord.Framework.Config.ActiveRecordSectionHandler, Castle.ActiveRecord" />-->
    <section name="nhibernate" type="System.Configuration.NameValueSectionHandler, System, Version=1.0.5000.0,Culture=neutral, PublicKeyToken=b77a5c561934e089" />
    <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net" />

  </configSections>

  <!--<activerecord isWeb="true" isDebug="false" threadinfotype="Castle.ActiveRecord.Framework.Scopes.HybridWebThreadScopeInfo, Castle.ActiveRecord">
    <config database="MsSqlServer2005" connectionStringName="MyTestDB">
    </config>
  </activerecord>—>

  ...  
</configuration> 

TheConclusion

I’m not sold on the “one web.config to rule them all” approach to maintaining environment settings, but at least I don’t have to give up my favorite frameworks as a result.

September 28, 2011 Posted by | Castle ActiveRecord, Visual Studio/.NET | 5 Comments

When I said now, I meant NOW! Castle ActiveRecord Transactions

I’m using Castle ActiveRecord (AR) for the major project that I’m working on right now (I have several lieutenants, but just the one major), and this past week I hit a snag.  I have one part of the web application that I felt was too complex to try to implement purely with AR objects, so I built a traditional query that would pull everything I needed together and my business layer returns it as a collection of custom objects.  The snag surfaced when I tried to write an object to the database using AR, and then immediately read it back using this query.

What I was trying to do was allow the user to click on a button that would cause the record to be written.  Then, I would run my custom query to refresh an in-session structure showing the user’s current progress.  Finally, the new page was rendered, showing the user’s updated progress.  What I was seeing was that the record was not being committed back to the database until after the page was rendered.  The new data WAS still now accessible via the AR framework (using any number of methods of finding it), but Doug (one of my colleagues) and I reasoned that AR was simply caching the data, waiting for the best opportunity to persist it back to the database.

With Doug’s help and after several passes using the debugger, we finally narrowed down where the record was actually being written – the Application_EndRequest event handler in my Global.asax.  You see, my Application_BeginRequest handler would create an AR SessionScope object, and stuff it into session.  Then the Application_EndRequest handler would pull it back out of session and dispose of it. 

Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)

    If BlahAppSettings.Core.EnableActiveRecordLogging Then
        log4net.Config.XmlConfigurator.Configure()
    End If

    ' Initialize ActiveRecord
    Dim activeRecordConfig As IConfigurationSource = Config.ActiveRecordSectionHandler.Instance
    Dim objectAssemblies As System.Reflection.Assembly() = New Reflection.Assembly() {System.Reflection.Assembly.Load("BlahBusinessServices")}
    ActiveRecordStarter.Initialize(objectAssemblies, activeRecordConfig)

    AddHandler Me.EndRequest, AddressOf Application_EndRequest
End Sub


Sub Application_BeginRequest(ByVal sender As Object, ByVal e As EventArgs)
    ' Enable ActiveRecord Session Scope for the request
    HttpContext.Current.Items.Add("ar.sessionscope", New SessionScope())
End Sub


Sub Application_EndRequest(ByVal sender As Object, ByVal e As EventArgs)
    ' Clean up the ActiveRecord Session Scope
    Try
        Dim scope As SessionScope = CType(HttpContext.Current.Items("ar.sessionscope"), SessionScope)
        If Not scope Is Nothing Then
            scope.Dispose()
            HttpContext.Current.Items.Remove("ar.sessionscope")
        End If
    Catch ex As Exception
        Throw
    End Try
End Sub

It was the .Dispose call in Application_EndRequest that was forcing the record back to the database.  I didn’t want to try to force the .Dispose in my page, but I did need that record written out sooner than EndRequest.

After doing some digging into AR sessions, I found this post on Stack Overflow: http://stackoverflow.com/questions/38729/do-you-know-how-to-implement-transactions-in-castle-activerecord.  In it, Ben Scheirman shows how to quickly create a new AR SessionScope object which effectively sets up an AR transaction, do my .Save, and then close the transaction, thus committing the write to the database. 

Using New Castle.ActiveRecord.SessionScope
    ...
    MyObject.Save()
End Using

I wasn’t sure if nesting transactions like this would work or would cause other problems, but after some additional testing it appears to do the job and no more.

Ship it!

November 29, 2010 Posted by | Castle ActiveRecord | Comments Off on When I said now, I meant NOW! Castle ActiveRecord Transactions

Join me, and together we’ll rule… – ActiveRecord and Attributes on Join Classes

I mentioned a couple of weeks ago that other than one notable exception, I was loving Castle ActiveRecord.  That one exception is today’s topic.  I say this is an exception to the statement “I love Castle ActiveRecord” only because I fought with this issue for several weeks* before I figured it out.

Originally, I had two tables – Book and Contributor.  These were joined in a many-to-many relationship in a table called BookContributor.  The first version of BookContributor was purely a join table and therefore only contained keys to the other two.  The Book class had a property called Contributors that had the HasAndBelongsToMany() attribute on it, just as it is shown in this example.  That allowed me to add new contributors to a book using something like “MyBook.Contributors.Add(MyNewContributor)”.  And everything was peachy.

The application that uses these classes shows contributors (authors, editors, etc.) in a list box.  Up to this point they were shown in the order that they were added to the list (I was sorting on the primary key of the BookContributor table).  Now, I wanted to allow the user to rearrange the contributors in any arbitrary way.  That meant I needed to add a field to BookContributor called “SortOrder”.  I added the field, and then hit my first roadblock.  Ok, THE roadblock.

ActiveRecord had been doing a fine job managing the contents of the BookContributor table.  Saying MyBook.Contributors.Add(MyNewContributor) would create the record in BookContributor for me, and wire the up the foreign keys just right.  Now, I wanted to be able to manage the SortOrder field in the join table, so saying MyBook.Contributors.Add(MyNewContributor) wouldn’t cut it anymore.  Since SortOrder was a property of the join table and not the Contributor record itself, I also couldn’t set a property on the MyNewContributor object before invoking the Add method.

I did a search for variations on “ActiveRecord join table property” to find a way to set SortOrder.  One of the most promising posts I found was the ActiveRecord online documentation page for the HasAndBelongsToMany attribute, referenced above.  There is a section titled “Attributes on the Association Table”, which sounded like what I wanted to do.  Their approach was to define an ActiveRecord class based on the join table.  Up to this point I hadn’t needed to do that – ActiveRecord was able to manage the contents of the join table purely from the relationship I had defined between the Book and Contributor classes.

What this post wasn’t clear on, however, was how the Post and Category classes (to use their examples) needed to be adjusted to refer to the new PostCategory class.  After rereading this page several times over, and doing some additional digging on the Internet, I concluded that the Post and Category classes really didn’t refer to the PostCategory class at all.  Attaching a Category to a Post (or conversely, adding a Post to a category) doesn’t set the PostCategory.ArbitraryValue database field.  If you wanted to do anything with ArbitraryValue, you’d have to do it using the PostCategory class.

This is basically the solution I ultimately arrived at.  I would create a class for BookContributor, and instead of saying MyBook.Contributors.Add(MyNewContributor), I would so something like the following:

Dim MyBook As Book
Dim MyContributor As Contributor
Dim NewBC As BookContributor

’ Load up the MyBook and MyContributor objects here

NewBC = New BookContributor
NewBC.Book = MyBook
NewBC.Contributor = MyContributor
NewBC.SortOrder = 2
NewBC.Save()

It’s not nearly as clean as MyBook.Contributors.Add(MyContributor), but it gets the job done.  Additionally, the Contributors property on the Book class still gets populated behind the scenes which allows me to sort on the new SortOrder field, so the only real change from what I had in place before is the process of adding new ones.

* Several weeks averaging roughly 30 minutes per day.  I had a lot going on that month.

January 21, 2010 Posted by | Castle ActiveRecord, Visual Studio/.NET | 1 Comment

Sorry, you’re just not my Type

I was trying to write a method for one of my Castle ActiveRecord classes that would return a list of Contributors by type.  The types were defined as an Int32 enumeration in the Contributor class, and my original method was as follows:

Public Shared Function GetAllByType(ByVal SearchContributorType As ContributorType) As IList(Of Contributor)
    Dim MySearchCriteria As DetachedCriteria
    Dim MySearchOrder As NHibernate.Expression.Order
    Dim ContributorArray() As Contributor
    Dim ContributorCollection As ICollection(Of Contributor)

    MySearchCriteria = DetachedCriteria.For(GetType(Contributor)).Add(Expression.Eq("Type", SearchContributorType))
    MySearchOrder = New NHibernate.Expression.Order("Name", True)
    ContributorArray = Contributor.FindAll(MySearchCriteria, MySearchOrder)

    ContributorCollection = CType(ContributorArray, ICollection(Of Contributor))
    Return CType(ContributorCollection, IEnumerable(Of Contributor))
End Function

When I ran the unit test that exercised the FindAll method, I got this error:

Could not perform FindAll for Contributor

Not terribly helpful, but when I looked at the inner exception, I found the real cause:

Type mismatch in NHibernate.Expression.EqExpression: Type expected type System.Int32, actual type BMSBusinessServices.Contributor+ContributorType

I tried explicitly casting the SearchContributorType parameter to an Integer (Int32) value, and it worked – no more exception, and my test passed.  

In the past, .NET has done a fabulous job of implicitly casting things for me.  Perhaps what is happening here (and I have not looked at the NHibernate source to confirm this, so this is pure conjecture) is that NHibernate needed to compare the Contributor.Type property to the SearchContributorType variable.  The former is declared as Int32 while the latter was of the Contributor.ContributorType enumeration.  Even though the latter was also ultimately defined as Int32, NHibernate needed to do something like a .Equals, and the slight difference in types was just enough to throw it off.

At any rate, the CType() was easily inserted into the Expression, and I was on my merry way.

January 7, 2010 Posted by | Castle ActiveRecord, Visual Studio/.NET | Comments Off on Sorry, you’re just not my Type

Nearly Sentient Error Messages

I’ve been working with Castle ActiveRecord for a “home” project for much of the last year.  With one notable exception (which I intend to blog about in the near future), I love the framework and the functionality it provides.

One particular area where ActiveRecord excels is its error messages.  Not only are the messages detailed, but the ones that I’ve come across also tend to give you hints as to what you may have forgotten to do.  Take this one, for example:

Castle.ActiveRecord.Framework.ActiveRecordException : You have accessed an ActiveRecord class that wasn’t properly initialized. The only explanation is that the call to ActiveRecordStarter.Initialize() didn’t include BMSBusinessServices.SKUDate class.

Before you can use an ActiveRecord class, you have to initialize it.  For desktop apps like mine, the call to ActiveRecordStarter.Initialize() is made in the MDI Parent form’s constructor, one of the first things to execute when the app loads (for web apps you’d place this call in the Application_Init event, defined in Global.asax).  The Initialize() method takes a list of the classes that you want to designate as being visible to ActiveRecord.  The error message above points you in that direction.

Now, as it turns out, I did have the SKUDate class listed in the Initialize method, so I had to dig a little deeper for the solution, which I found in this post: http://www.feed-squirrel.com/index.cfm?evt=viewItem&ID=69310.  The post is a little hard to read (any and all whitespace between sentences and paragraphs appeared to be stripped out), but near the very end the post mentions that this error can also be caused when you forget to decorate the class in question with the <ActiveRecord()> attribute.

Which I had.

And once I added it everything was happy again.

Despite the fact that the error didn’t point to the real solution to my problem, what it did give showed me what an exception could really be.  After seeing this particular message, I felt compelled to take a long hard look at the custom exceptions that I write and the messages that they include. Basically, my exceptions needed to grow up a little and become slightly more intelligent.

January 4, 2010 Posted by | Castle ActiveRecord, Visual Studio/.NET | 1 Comment