Saturday 23 May 2009

Encrypting Connections strings in .Net with NAnt

I have been working a lot with databases over the last couple of weeks, one of the things that is a novelty to the people I am working with now is the ability to store multiple connection strings in our .config files. Its a really simple and flexible thing to do, allowing us to transport any of our apps from environment to environment with very little hassle. The only downer is that the connection strings are stored in plain-text, which naturally is a massive security hazard.



That is, of course, unless you encrypt them.



There are loads of examples on the Internet explaining how this can be done, and they are very simple to follow and implement - but I wonder how many people make use of this fairly simple feature? One thing that became apparent to me was, even though I can encrypt the file - just when and how do I want to do this? The when is simple, I want my connection strings encrypted when the application moves into a testing environment an I want them to stay encrypted from then on. The how is slightly different, there is no point in having the encryption take place as I am developing the app, as I want to have the flexibility of changing the connection strings as and when I need them. So, the ideal time for me to encrypt them would be during the build itself. Its a very straight forward thing to do, simply write a small program that encrypts the connection strings section of a web or app .config and have Visual Studio process this as a post build event. Its possibly the easiest way to do this. But, this isnt so flexible, it would only really encrypt one file relevant to a project, and whilst the code is reusable, it isnt very agile.

So, why not move this from being a post build event in the soloution file out to being a post build event in your larger build process? Using NAnt (or MSBUILD), we can create a custom task that will encrypt the connectionStrings section in any .config file. This could be used over and over again as and when requirements dictate a section in a config file should be encrypted.

There are a couple of things we need to capture:
  • The path of the .config file
  • What sort of encryption we want to use.

It would appear that .Net offers two methods to encrypt a .config file, DPAPI and RSA. DPAPI is provided by Microsoft, there is a nice article on Wikipedia discussing this encryption method. For a lot of people though, RSA may be a more suitable encryption method (its the one I am using if that is worth anything).

Back to the task. To start with, I want to get the location of the .config file and decide which encryption method I want to use. This is easy enough:


[TaskName("config-encrypt")]
class Encryption : Task
{
private string strProvider;
private string _filePath;
[TaskAttribute("configfile", Required = true)]
public string FilePath
{
get { return _filePath; }
set { _filePath = value; }
}
private string _rsa;
[TaskAttribute("RSAencryption", Required = false)]
public string RSA
{
get { return _rsa; }
set { _rsa = value; }
}
private string _dpapi;
[TaskAttribute("DPAPIencryption", Required = false)]
public string DPAPI
{
get { return _dpapi; }
set { _dpapi = value; }
}
protected override void ExecuteTask()
{
if (checkFileExists())
{
if (FilePath.Contains("web.config"))
{
encryptWebConfig();
rename();
}
else
{
encryptAppConfig();
rename();
}
}
else
{
throw new BuildException("The .config nominated does not seem to exist");
}
}

I have left the encryption methods as optional - this way, you dont need to worry about which type you need, however by doing this I need to set up a default encryption provider. A simple method like the one below will take care of that for me, I have established that I want to use RSA as the default encryption method if no other is specified:
private void chooseEncryptionProvider()
{
if (RSA == "true")
{
strProvider = "RSAProtectedConfigurationProvider";
}
else if (RSA == "" & DPAPI == "")
{
strProvider = "RSAProtectedConfigurationProvider";
}
else if(DPAPI == "true")
{
strProvider = "DataProtectionConfigurationProvider";
}
}

I also need to make sure that the file I want to apply encryption to exists in the first place. Again, this is very simple to do:


private bool checkFileExists()
{
if (File.Exists(FilePath))
{
return true;
}
else
{
return false;
}
}

Now I am all set to get encrypting. I want my task to be able to handle both web and app.config files, so I have two methods to handle each. They are pretty much identical in their operation, the only real difference is down to the way .Net handles web.config files. To encrypt a web.config I have done the following:
private void encryptWebConfig()
{
chooseEncryptionProvider();
try
{
Configuration _configFile = WebConfigurationManager.OpenWebConfiguration(FilePath);
if (_configFile != null)
{
try
{
ConnectionStringsSection _section = (ConnectionStringsSection)_configFile.GetSection("connectionStrings");
_section.SectionInformation.ProtectSection(strProvider);
_section.SectionInformation.ForceSave = true;
_configFile.Save();
}
catch (Exception e)
{
throw new BuildException("Failed to encrypt the connection strings sectiion",
e.InnerException);
}
}
}
catch (Exception e)
{
throw new BuildException(e.Message.ToString(), e.InnerException);
}
}

You can see where I am choosing the encryption provider at the start of the method. One thing I havent done here is implement any logging. For the sake of an audit trail, it would be a very wise thing to log what sort of encryption is being used. This way, you can look back across the builds you have completed for a specific environment and see which encryption provider was used. The following method encrypts an app.config:


private void encryptAppConfig()
{
string path = FilePath.Replace(".config", "");
chooseEncryptionProvider();
Configuration _configFile = ConfigurationManager.OpenExeConfiguration(FilePath);
if (_configFile != null)
{
try
{
ConnectionStringsSection _section = (ConnectionStringsSection)_configFile.GetSection("connectionStrings");
_section.SectionInformation.ProtectSection(strProvider);
_section.SectionInformation.ForceSave = true;
_configFile.Save();
}
catch (Exception e)
{
throw new BuildException("Failed to encrypt the connection strings section"
, e.InnerException);
}
}
}

As you can see, it is pretty much the same idea for this operation compared to encrypting a web.config. With all this in place, we now have all we need to automatically encrypt a .config file as part of an automated build. This can be added on to the end of a build target in a NAnt script, or better yet added to its own target to be used as a post build event. One thing I found when using this is that after the .config had been encrypted a .config.config file was created leaving the un-encrypted .config in the same directory. Naturally, this defeats the point so I put the following method together to clean up for me:
private void rename()
{
if (File.Exists(FilePath + ".config"))
{
string oldname = FilePath;
string newname = FilePath + ".config";
File.Delete(FilePath);
File.Copy(newname, oldname);
File.Delete(newname);
}
}

And there we go, all done. The syntax for the build script is insanely simple - if you dont mind using RSA encryption all you need to do is point it at the .config you want to encrypt:
</target>
<target name="encrypt">
<config-encrypt configfile="C:\test\web.config" />
</target>

I have been using this task for the past week or so, and it has been working like a dream :). The complete code for the task is below:
using System;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Linq;
using System.Text;
using System.Web;
using System.Web.Configuration;
using NAnt.Core;
using NAnt.Core.Tasks;
using NAnt.Core.Attributes;

namespace Custom.NantTasks.Encryption
{
[TaskName("config-encrypt")]
class Encryption : Task
{
private string strProvider;
private string _filePath;
[TaskAttribute("configfile", Required = true)]
public string FilePath
{
get { return _filePath; }
set { _filePath = value; }
}
private string _rsa;
[TaskAttribute("RSAencryption", Required = false)]
public string RSA
{
get { return _rsa; }
set { _rsa = value; }
}
private string _dpapi;
[TaskAttribute("DPAPIencryption", Required = false)]
public string DPAPI
{
get { return _dpapi; }
set { _dpapi = value; }
}
protected override void ExecuteTask()
{
if (checkFileExists())
{
if (FilePath.Contains("web.config"))
{
encryptWebConfig();
rename();
}
else
{
encryptAppConfig();
rename();
}
}
else
{
throw new BuildException("The .config nominated does not seem to exist");
}
}
private void encryptWebConfig()
{
chooseEncryptionProvider();
try
{
Configuration _configFile = WebConfigurationManager.OpenWebConfiguration(FilePath);
if (_configFile != null)
{
try
{
ConnectionStringsSection _section = (ConnectionStringsSection)_configFile.GetSection("connectionStrings");
_section.SectionInformation.ProtectSection(strProvider);
_section.SectionInformation.ForceSave = true;
_configFile.Save();
}
catch (Exception e)
{
throw new BuildException("Failed to encrypt the connection strings sectiion",
e.InnerException);
}
}
}
catch (Exception e)
{
throw new BuildException(e.Message.ToString(), e.InnerException);
}
}

private void encryptAppConfig()
{
string path = FilePath.Replace(".config", "");
chooseEncryptionProvider();
Configuration _configFile = ConfigurationManager.OpenExeConfiguration(FilePath);
if (_configFile != null)
{
try
{
ConnectionStringsSection _section = (ConnectionStringsSection)_configFile.GetSection("connectionStrings");
_section.SectionInformation.ProtectSection(strProvider);
_section.SectionInformation.ForceSave = true;
_configFile.Save();
}
catch (Exception e)
{
throw new BuildException("Failed to encrypt the connection strings section"
, e.InnerException);
}
}
}

private void chooseEncryptionProvider()
{
if (RSA == "true")
{
strProvider = "RSAProtectedConfigurationProvider";
}
else if (RSA == "" & DPAPI == "")
{
strProvider = "RSAProtectedConfigurationProvider";
}
else if(DPAPI == "true")
{
strProvider = "DataProtectionConfigurationProvider";
}
}

private bool checkFileExists()
{
if (File.Exists(FilePath))
{
return true;
}
else
{
return false;
}
}

private void rename()
{
if (File.Exists(FilePath + ".config"))
{
string oldname = FilePath;
string newname = FilePath + ".config";
File.Delete(FilePath);
File.Copy(newname, oldname);
File.Delete(newname);
}
}
}
}

Tuesday 19 May 2009

Using log4net with C# and MySql

Logging your application is a basic unwritten requirement. I cannot think of one application I have ever worked with that does not provide any form of logging, be it a logfile or a log in a database.

Recently at work, I came across a situation where someone described a log as a "nice to have". No, it's not nice to have - its essential to have it. As I was working in .Net (c# to be exact, hence the title), I decided to take a look at log4net. This is a logging framework maintained by the Apache project and is a port of the popular log4j to .Net, I am not going to bore people with an explanation of it here, all I will say about it is that it is a very powerful and flexible logging service. You can check out the official site here: http://logging.apache.org/log4net/index.html.

I spent a few weeks working with log4net (as it apparently wasnt essential to have a log, it was a low priority task). Creating a logfile with it is incredibly easiy, as is logging to the console. There are wide range of methods for creating a log, called appenders, however the most useful for me was appending the log to a database. Whilst log4net is popular, powerful and active, there isnt that much in the way of documentation or howto's, which could put some people off using it, this is especially apparent when logging to a database. The examples that already exist on the interweb, while useful, are sometimes out of date using deprecated syntax.

For my logging scenario, I needed to maintain a log in a MySql table, this compounded my explotation of log4net, as it isnt readily obvious how this can be done. My aim is to use log4net for all of my logging needs, so I want to create an assembly that can be referenced etc wherever I need it. Due to the extremely felxible nature of log4net, this is achievable - the majority of the configuration for it lives within the app.config (or web.config if you are that way inclined). It is possible to programatically configure log4net from within your code - but what would be the point? If you do that, then you loose all of the flexibility offered. One way to highlight this feature - right now I need to log to a MySql table, but what about in future if we decide to migrate to another DB? If I configured my logger programtically, I would then need to rewrite code. Whereas if I configured it via a .config file, all I need to do is deploy that file wherever it needs to go...

Anyway, enough waffle - you are all grownups and have myriad needs. Lets take a look at how I got log4net logging to my MySql table.

Setting up log4net to log to the console or to a file is dead simple, there is very little taking place in the code, most of it is done in the config file. The same can go for logging to a db, but with one small difference - if you rely on the config file to control your logging when using a database you have to be comfortable with the fact that your connection string will be visible. I am not sure how much of a security risk that is for everyone else, but for me it wasnt something I was willing to do at all. Even if you use a seperate database for logging, you are still exposing a lot of information about your infrastructure here. So, instead of placing the connection string in the log4net config, I am going to place it in the connectionStrings section and encrypt it. Once this is done, I need to set the connection programatically. This isnt as easy as you would first imagine (well, at least I didnt find it that easy), whilst there is a lot of documentation on what log4net can do, I found it quite tricky to figure out how to make it work with MySql - and even when I had I still had one small issue... Anyway, on to getting this setup.

All of my config for my logger is going to live in my app.config - inlcuded my encrypted db connection string. I need to get this string and then provide it to log4net, this bit is done in the code. It sounds simple but there are a number of steps I need to take first. To start with I need to get the default repository that my logger is going to use with log4net:
private Hierarchy hierachy()



{



Hierarchy h = (Hierarchy)log4net.LogManager.GetRepository();



return h;



}




It is possible to use different configuration repositories with log4net - but I dont need to so I havent had to worry about this. To be honest, I am not sure that a lot of people will need to either. Once this has been done, we need to create an ADOAppeneder - it should also be pointed out by now that I am using the MySql .Net connector to work with my db.
private AdoNetAppender adoAppender()



{



Hierarchy myHierarchy = hierachy();



if (myHierarchy != null)



{



AdoNetAppender adoApp = (AdoNetAppender)myHierarchy.Root.GetAppender("ADONetAppender");



return adoApp;



}



return null;



}




With this code, I am able to get the section of my .config file that holds all of the config for logging to a db through log4net, log4net relies on one or more 'appenders' being configured for use - my db appender is simply called 'ADONetAppender', so I have specified this in the above method. With this done, I am now ready to configure my appender programtically:
private void createAdoSettings(string dbConn)



{



AdoNetAppender myAppender = adoAppender();



if (myAppender != null)



{



myAppender.ConnectionString = dbConn;



myAppender.UseTransactions = true;



myAppender.ActivateOptions();



}



}




After all of this, we are done. All I need to do is tell my logging class what my connection string is. This isnt so difficult (thankfully, is very easy):
string connstring = ConfigurationManager.ConnectionStrings["SQLLogDB"].ConnectionString.ToString();



SetUpLog _setup = new SetUpLog(connstring);




Cant get any simpler really :). All I need to do is drop this into any class I want to log from and I am set. There is only one other addition I need to make, and that is to implement the ILog interface. This is the most important bit, regardless of any db logging, without it you cant log anything at all! I got a nice little gem from here: http://www.ondotnet.com/pub/a/dotnet/2003/06/16/log4net.html that give a good tip on implementing the interface. Basically, popping this line in at the start of your class ensures that log4net reports the name of the class duing the logging process:
protected static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);




This is so useful and provides a very concise log. With all this in place, we are now able to use log4net to log our application. The following code is the test app I wrote when to prove that it works:
using Logger.Setup;



using System;



using System.Configuration;



using System.Collections.Generic;



using System.Linq;



using System.Text;







namespace TestClient



{



class Program



{



protected static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);







static void Main(string[] args)



{



Program test = new Program();



test.logtest();



}



private void logtest()



{



string connstring = ConfigurationManager.ConnectionStrings["LogDB"].ConnectionString.ToString();



SetUpLog _setup = new SetUpLog(connstring);



log.Info("Hello there");



}



}



}








On execution, it provides the following on the command line:
2009-05-23 23:21:37,536 [12260] INFO  Hello there 23 TestClient.Program





You can also see how I am specifying the connection string, which is also held in the .config file. This is driven by my .config file, it is just a console appender and lives inside the log4net section:

<appender name="ConsoleAppender" type="log4net.Appender.ConsoleAppender">



<layout type="log4net.Layout.PatternLayout">



<conversionPattern value="%date [%thread] %-5level %message%newline %L %C" />



</layout>



</appender>




Now I know it works, I can get down to logging the same information to my db, first I create my schema:
CREATE TABLE logdb.mylog(



`DateTime` DATETIME DEFAULT NULL,



Thread VARCHAR (20) DEFAULT NULL,



Level VARCHAR (20) DEFAULT NULL,



Message VARCHAR (20) DEFAULT NULL,



Exception VARCHAR (20) DEFAULT NULL,



line VARCHAR (20) DEFAULT NULL,



object_ref VARCHAR (20) DEFAULT NULL,



taskID VARCHAR (20) DEFAULT NULL



)



ENGINE = INNODB



CHARACTER SET latin1



COLLATE latin1_swedish_ci;





The object_ref and taskID columns are things I want to capture as part of my wider logging process - they dont really have anything to do with log4net or its configuration. With my schema in place all I need to do now is configure log4net to log my messages to my new table. This can be a bit frustrating when working with MySql - if you get something wrong in the config you will get an error message, but they dont always seem to be that helpful to me. For instance, log4net requires you to define a connection type in order to log to a db. When I first started working with log4net, my connection type was incorrect which resulted in an error. However, the error I got complained about connection strings being null - not that there was an error with the connection type I had configured. Granted, they are closely related, but it would have just been nice if I get an error back telling me that my connection type was wrong... Anyway, I got passed it. When configuring log4net there are a couple sections that you need to work with. I am going to assume that if you are reading this then you have already looked at the log4net site and looked at how it is configured. I am not going to explain how to configure log4net here in general, if you want to see how this works then there are a lot of other articles out there that do this well, one good place to start on the topic of configuration is the log4net site - even though it is very general it will give you an idea of how all the appenders look: http://logging.apache.org/log4net/release/manual/configuration.html

Now that my schema is in place and my logic is complete, all I need to do now is configure a log4net appender. Appenders in log4net control how information is formatted as it is logged, I am going to use an ADO appender and it looks like this:
<appender name="ADONetAppender" type="log4net.Appender.ADONetAppender">
<connectionType value="MySql.Data.MySqlClient.MySqlConnection, MySql.Data, Version=6.0.3.0, Culture=neutral, PublicKeyToken=c5687fc88969c44d" />
<commandText value="insert into mylog (thread,line,object_ref,taskID,message,level,datetime)
values(?thread,?line,?objectref,?taskid,?message,?level,?log_date)" />
<parameter>
<parameterName value="?thread"/>
<dbType value="String"/>
<size value="20"/>
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%thread"/>
</layout>
</parameter>
<parameter>
<parameterName value="?line"/>
<dbType value="String"/>
<size value="20"/>
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%L"/>
</layout>
</parameter>
<parameter>
<parameterName value="?objectref"/>
<dbType value="String"/>
<size value="20"/>
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%C"/>
</layout>
</parameter>
<parameter>
<parameterName value="?taskid"/>
<dbType value="String"/>
<size value="20"/>
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%property{taskid}"/>
</layout>
</parameter>
<parameter>
<parameterName value="?message"/>
<dbType value="String"/>
<size value="20"/>
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%message"/>
</layout>
</parameter>
<parameter>
<parameterName value="?level"/>
<dbType value="String"/>
<size value="20"/>
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%level"/>
</layout>
</parameter>
<parameter>
<parameterName value="?log_date" />
<dbType value="DateTime" />
<layout type="log4net.Layout.RawTimeStampLayout"/>
</parameter>
</appender>

The most notable thing about my config here is the use of a property. This lets me capture custom information for my log and place this in my db. This is really useful, I am sure you can all understand why and goes to show the power of log4net. With all of this in place, I am now able to log to my db - but with one small caveat, I think I have found a problem with log4net when using an ADO appender with MySql. For some reason, log4net generates an error telling me that there is a null system exception, but carries on and logs the data to the db anyway. I spent a long time trying to figure out what the problem was, but couldnt spot it. So, after some advice, I started working with the log4net source to spot where the error was being generated. Stepping through the code, I found that log4net (when used with the MySql .Net connector) seems to loose the connection string, but logs data anyway. I couldnt replicate this with SQL Server and have still been unable to resolve the error. I dont like errors hanging around like this, the data is logged, but I get this nasty message.... I have posted my findings to the log4net support group, but have not had anything back about it really - which is a shame. However, in saying that, log4net is still a truly useful tool to use in any project that requires any form of logging.

Saturday 9 May 2009

NAnt, .Net, Sandcastle and Documentation

Documentation is great, it makes people feel good about themselves. After they have created a particularly cunning peice of work, it's nice to throw together some documentation so that others can see your genius. But wait, that means you need to go through all your work and come up with some documentation of your own! Gasp, shock - horror!!

Lets face it, for a lot of people, creating standardised documentation after they have finished their project etc isnt at the fore front of their minds. This is a shame, because with flippancy aside, people do really like documentation. It enables developers to get on with their work without asking people for info on an API etc and it lets managers see that there is "stuff being done with documentation". And dont forget, standardisation is key here - you dont want a hundred different developers all creating documentation in different ways, it would be a nightmare.

There are a lot of ways documentation can be generated at build time. One popular way is to use NDoc and this is catered for within NAnt. However, this doesnt support .Net 2.0 let alone 3.5 and it hasnt seen a release since 2005... So, there appears to be a massive gap for generation of documentation, that is if you don't count Sandcastle. This is a fairly useful, if slightly complex method of generating documentation. There are lots of examples on how to use Sandcastle via batch files and powershell scripts (the version I downloaded came packed with them). In an effort to understand the generation process, I decided to put together a NAnt task so this could be automated as part of continuous integration.

Sandcastle itself is not really one application. Right now, Sandcastle is a number of programs working together. It consits of:

  • MRefBuilder.exe
  • XslTransform.exe
  • BuildAssembler.exe
  • CHMBuilder.exe
  • DBCSFix.exe

Each of these programs are used in turn to generate first XML documentation followed by CHM (should you need it.). The following examples give an indication of how you could put a NAnt task together to make use of these programs for all your documentation needs.

All we need to do is create a suite of tasks that represent each of these programs. Essentially, all we are doing here is capthuring the arguments we need to run the apps themselves, there isnt much going on in task per se. For example, here is the complete code for MRefBuilder:

using System;
using System.Collections.Generic;
using NAnt.Core;
using NAnt.Core.Tasks;
using NAnt.Core.Attributes;
using Custom.NantTasks.Common;

namespace Custom.NantTasks.Sandcastle
{
/// <summary>
/// NAnt task to make use of Sandcastle from MS
/// </summary>
[TaskName("mrefbuilder")]
class MRefBuilder : ExternalProgramBase
{
private string _assemblyPath;
[TaskAttribute("assembly-path", Required = true)]
public string AssemblyPath
{
get { return _assemblyPath; }
set { _assemblyPath = value; }
}
private string _outputPath;
[TaskAttribute("output-path", Required = true)]
public string OutPutPath
{
get { return _outputPath; }
set { _outputPath = value; }
}
private string _executable;
[TaskAttribute("executable", Required = true)]
public string Executable
{
get { return _executable; }
set { _executable = value; }
}
public override string ExeName
{
get
{
return _executable;
}
set
{
base.ExeName = value;
}
}
public override string ProgramArguments
{
get { return AssemblyPath + " /out:" + OutPutPath; }
}
protected override void ExecuteTask()
{
Log(Level.Info, "Running MRefBuilder");
Task();
}

private void Task()
{
try
{
base.ExecuteTask();
}
catch (Exception e)
{
throw new BuildException(e.Message);
}
}
}
}


As you can see, this is pretty much just an arguments gathering exercise. Things become a little trickier for XslTransform. This app specifies multiple transformations, so we need to be able to specify at least one transformation in the task itself - but reserve the ability to specify a second. I am not going to go into exactly how Sandcastle works here, all I would be doing is copying information. For an in depth explanation of how Sandcastle works, take a look at the Sandcastle blog. So, for the XslTransform task, I simply specify two attributes for the .xsl files:


private string _xslPath;
[TaskAttribute("xsl-path", Required = true)]
public string XslPath
{
get { return _xslPath; }
set { _xslPath = value; }
}
private string _xslPath2;
[TaskAttribute("xsl-path2", Required = false)]
public string XslPath2
{
get { return _xslPath2; }
set { _xslPath2 = value; }
}

You may wish to use more than one .xsl - I don't, the example batch file in the version I downloaded didnt seem to make use of more than two. However, if you need to add more then its a very simple task to add this to the task itself. Once the arguments have been captured, we need to then use them with the app itself. We need to be able to provide at least one .xsl in the argument we use with the app and if a second is specified, then we need to be able to provide this also. To achieve this, I used the following:


private string checkXSLPath()
{
string _arg;
if (XslPath2 == null)
{
_arg = "/xsl:\"" + XslPath + "\" " + InputFile + " /out:" + OutputPath;
Log(Level.Info, _arg);
return _arg;
}
else
{
_arg = "/xsl:\"" + XslPath + "\" " + "/xsl:\""
+ XslPath2 + "\" " + InputFile + " /out:" + OutputPath;
Log(Level.Info, _arg);
return _arg;
}

}

All this does is simply check to see if a second xsl file has been specified. If it has, then it returns an argument that makes use of it, if not, then it returns an argument that doesnt. The full source looks like this:


using System;
using System.Collections.Generic;
using NAnt.Core;
using NAnt.Core.Tasks;
using NAnt.Core.Attributes;

namespace Custom.NantTasks.Sandcastle
{
[TaskName("xsltransform")]
class XslTransform : ExternalProgramBase
{
private string _xslPath;
[TaskAttribute("xsl-path", Required = true)]
public string XslPath
{
get { return _xslPath; }
set { _xslPath = value; }
}
private string _xslPath2;
[TaskAttribute("xsl-path2", Required = false)]
public string XslPath2
{
get { return _xslPath2; }
set { _xslPath2 = value; }
}
private string _inputFile;
[TaskAttribute("inputfile", Required = true)]
public string InputFile
{
get { return _inputFile; }
set { _inputFile = value; }
}
private string _outPutPath;
[TaskAttribute("output", Required = true)]
public string OutputPath
{
get { return _outPutPath; }
set { _outPutPath = value; }
}
private string _executable;
[TaskAttribute("executable", Required = true)]
public string Executable
{
get { return _executable; }
set { _executable = value; }
}
public override string ExeName
{
get
{
return _executable;
}
set
{
base.ExeName = value;
}
}
public override string ProgramArguments
{
get { return checkXSLPath(); }
}
protected override void ExecuteTask()
{
Log(Level.Info, "Starting XslTransform");
Task();
}

private void Task()
{
try
{
base.ExecuteTask();
}
catch (Exception e)
{
throw new BuildException(e.Message);
}
}
private string checkXSLPath()
{
string _arg;
if (XslPath2 == null)
{
_arg = "/xsl:\"" + XslPath + "\" " + InputFile + " /out:" + OutputPath;
Log(Level.Info, _arg);
return _arg;
}
else
{
_arg = "/xsl:\"" + XslPath + "\" " + "/xsl:\""
+ XslPath2 + "\" " + InputFile + " /out:" + OutputPath;
Log(Level.Info, _arg);
return _arg;
}

}
}
}


Looking good so far. Now we have two NAnt tasks that will generate documentation as xml and then transform it. Now we get to the meat of the whole process, BuildAssembler. Again, I am not going to go into detail about what this does here - instead, check the Sandcastle blog entry on BuildAssembler here for a complete explanation. Whilst the process behind BuildAssembler is very involved, the NAnt task for it is quite simple, the complete source is below:


using System;
using System.Collections.Generic;
using NAnt.Core;
using NAnt.Core.Tasks;
using NAnt.Core.Attributes;

namespace Custom.NantTasks.Sandcastle
{
[TaskName("buildassembler")]
class BuildAssembler : ExternalProgramBase
{
private string _manifest;
[TaskAttribute("manifest", Required = true)]
public string Manifest
{
get { return _manifest; }
set { _manifest = value; }
}
private string _config;
[TaskAttribute("config", Required = true)]
public string Config
{
get { return _config; }
set { _config = value; }
}
private string _executable;
[TaskAttribute("executable", Required = true)]
public string Executable
{
get { return _executable; }
set { _executable = value; }
}
public override string ExeName
{
get
{
return _executable;
}
set
{
base.ExeName = value;
}
}
public override string ProgramArguments
{
get { return " /config:\"" + Config + "\" " + "\"" + Manifest + "\""; }
}
protected override void ExecuteTask()
{
Log(Level.Info, "Starting BuildAssember");
base.ExecuteTask();
}
}
}


Essentially, all we are doing is giving BuildAssembler a config file to work with as well as a manifest file we generated with the previous task, XslTransform. Now even though there are more steps to go through, we need to start looking at the NAnt script itself. On its own, Sandcastle wont do everything you want, there are things you need to initiate, some of these are the creation of directories as well as copying files etc. Lets take a look at the NAnt script, at this stage it should look something similar to this:


<target name="Sandcastle">
<echo message="Trying out my Sandcastle task" />
<echo message="Checking for output directory.." />
<mrefbuilder assembly-path="Custom.NantTasks.dll"
output-path="C:\somefolder\sandcastletest.xml"
executable="C:\Program Files\Sandcastle\ProductionTools\MrefBuilder.exe" />
<xsltransform xsl-path="C:\Program Files\Sandcastle\ProductionTransforms\ApplyVsDocModel.xsl"
xsl-path2="C:\Program Files\Sandcastle\ProductionTransforms\AddFriendlyFilenames.xsl"
inputfile="C:\somefolder\sandcastletest.xml"
output="C:\somefolder\refelction.xml"
executable="C:\Program Files\Sandcastle\ProductionTools\xsltransform.exe" />
<xsltransform xsl-path="C:\Program Files\Sandcastle\ProductionTransforms\ReflectionToManifest.xsl"
inputfile="C:\somefolder\refelction.xml"
output="C:\somefolder\manifest.xml"
executable="C:\Program Files\Sandcastle\ProductionTools\xsltransform.exe" />
<mkdir dir="C:\somefolder\html" />
<mkdir dir="C:\somefolder\icons" />
<copy todir="C:\somefolder\icons">
<fileset basedir="C:\Program Files\Sandcastle\Presentation\vs2005\icons">
<include name="*" />
</fileset>
</copy>

<mkdir dir="C:\somefolder\scripts" />
<copy todir="C:\somefolder\scripts">
<fileset basedir="C:\Program Files\Sandcastle\Presentation\vs2005\scripts">
<include name="*" />
</fileset>
</copy>
<mkdir dir="C:\somefolder\styles" />
<copy todir="C:\somefolder\styles">
<fileset basedir="C:\Program Files\Sandcastle\Presentation\vs2005\styles">
<include name="*" />
</fileset>
</copy>
<mkdir dir="C:\somefolder\media" />
<mkdir dir="C:\somefolder\intellisense" />
<buildassembler config="C:\Program Files\Sandcastle\Presentation\vs2005\configuration\sandcastle.config"
manifest="C:\somefolder\manifest.xml"
executable="C:\Program Files\Sandcastle\ProductionTools\buildassembler.exe" />
</target>

As you can see, my script is very static. It specifies one model for Sandcastle (VS2005) and specifies one assembly to generate from. Also, as you can see all of the paths are static - but this doesnt stop you from creating a generic task to handle multiple assemblies as part of a CI process. Points to note in this script - simply the creation and population of directories prior to using BuildAssembler. These come directly from the location Sandcastle has been installed to (or from wherever you will choose to store them).

So, we are nearly there. By now, we have xml documentation generated, following the provided scripts in the Sandcastle download, the next step is to create some CHM file. Personally, I dont really need .chm files generated - I want my help files to go straight into a website/wiki.... To do that though, requires a bit more thought and code than I cover here. So stay tuned!!

Sunday 3 May 2009

Cycle paths, great...

I have been getting on my bike lately. This isnt such a novel thing for me, it was a past time for me prior to my move up to Scotland. Cycling up here is a lot different to my previous venue, the weather, for one thing, is completely different. Down south I could be content in the knowledge that it would be relatively sunny from March to September and also had the added benefit of being on the doorstep to a national park. In Edinburgh though, things are different by quite some degree, for starters, I have to travel quite a bit before I get to anywhere interesting. And as I dont have a car, let alone a license, it means a period of cycling through a rather boring Edinburgh.

But, thats all forgotten once you hit one of the cycle routes that cross through Edinburgh. I was out near a place called Auchendinny a few days ago and managed to get myself lost (wasnt on a cycle route), on the way back into Edinburgh I had the misfortune to pass through Bonnyrigg. I have never been there before and if ever I go back I will be sure to spend as little time as possible there. I am sure there are a lot of lovely people there, but unfortunately the only natives I met decided to through a vodka bottle at me and shredded my front tire in the process.

Charming.

It was in such a poor state, I had to get a lift back into Edinburgh. Due to Easter, it meant that the buses were on a crap schedule - by pure chance I was lucky enough to find a cab, not only an available one but one who would admit my rather mucky bike and me in it. I didnt catch his name, but he was a life saver and has my undying thanks.

And no, that wasnt in Bonnyrigg, it was a few miles down the road in Dalkeith!