# Monday, July 25, 2005

In this post, I’ll show how to build an MsBuild (msdn2) task will automatically generate XSD schemas for your custom tasks. The post is separated in two sections: if you are interested to see how the task works, continue on reading. If you don’t care and want to get straight down to the beef, scroll down until you see TaskSchema reference.

What is MsBuild? Why do we want to generate schemas?

MsBuild is the new .Net build system. It is based on XML files containing the projects, targets and tasks (see also Ant, NAnt). The framework comes with a set of tasks that allows easy compilation of solutions or projects and doing a couple more actions. Of course, everybody has different needs and you will probably end up writing your own specialized task to solve your problems.

When editing a MsBuild script, it is *very* convenient to take advantage of the “built-in intellisense” of XML by attaching it to the MsBuild schema file. You can do this in VS2005 in two mouse clicks (see below How to setup intellisense for msbuild). Unfortunately, this is not true for your custom tasks where you need to create yourself a XSD schema.

I’ll assume you have a basic knowledge of custom msbuild tasks in the following.

Note that NAnt has had this feature (nantschema) for a while now.

What are we looking for?

Before diving into the coding details, let’s see what our “final products” looks like. Let’s build a simple task to use it as our example during the article: the sleep tasks makes msbuild sleep x seconds (note that this implementation is very very poor):

using System;
using System.Threading;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
namespace Foo
{
public sealed Sleep : Task
{
private int seconds;

[Required]
public int Seconds
{
get { return this.seconds;}
set { this.seconds = value;}
}
public override bool Execute()
{
this.Log.LogMessage(“Sleeping {0} seconds”, this.Seconds);
Thread.Sleep(this.Seconds * 1000);
}
}
}

The sleep task has a single (required) parameter “seconds”. We expect the schema for this task to look as follows:

<xs:schema
    xmlns:msb=http://schemas.microsoft.com/developer/msbuild/2003
    elementFormDefault="qualified" 
    targetNamespace="http://schemas.microsoft.com/developer/msbuild/2003"
    xmlns:xs="http://www.w3.org/2001/XMLSchema">
    <
xs:include schemaLocation=" Microsoft.Build.Commontypes.xsd" />
    <
xs:element name="Sleep" substitutionGroup="msb:Task">
    <
xs:complexType>
        <
xs:complexContent mixed="false">
            <
xs:extension base="msb:TaskType">
                <
xs:attribute name="Seconds" type="msb:non_empty_string" />
            </
xs:extension>
        </
xs:complexContent>
   
</xs:complexType>
</xs:element>
</xs:schema>

There’s a couple things to say about this schema:

  1. we include the MsBuild schema so we get all the type that are already defined for msbuild,
  2. Sleep is in the substitutionGroup msb:Task,
  3. the extension has “msb:TaskType” as base class to inherit the Task properties,
  4. Since Seconds is an int, we restrict the attribute to msb:non_empty_string. We cannot restrict it to xs:integer to handle cases where the users pases a property, e.g. $(Property).
  5. Latter on, we could add information from the xml documentation into the schema as well.

Now that we know kind of output we are looking for, let’s see what “ingredients” we need.

Recipe ingredients

Here’s a little summary of tools and techniques that we will use to generate the schemas:

  • System.Reflection: we’ll use reflection to enumerate types, find custom attributes, etc…
  • System.Xml.Schema: this namespace contains the object model to create schemas (what a surprise!)
  • System.Xml: to integrate document parts (if available) in the output
  • Microsoft.Build.Utilities: this namespace contains the Task base class,

Building TaskSchema

The TaskSchema has the following properties (it actually has more properties but I’ll skip them for simplicity):

[Required]
ITaskItem[] Assemblies {get;set}
[Output]
ITaskItem[] Schemas {get;}

Assemblies is a list of assemblies containing the tasks to “schematize”. Schemas is an output parameter that will contain the path of each schema, as we will generate one schema per assembly.

This is the high level “factored” Execute method (almost pseudo-code):

CreateAndInitializeSchema(); // (add namespace, includes, etc...)

foreach(string assemblyName in Assemblies)
{
// load error handling comes here
Assembly assembly = LoadAssembly(assemblyName);
// iterating exported types
foreach(Type type in assembly.GetExportedTypes())
{
//if (type is not a task) continue;
if(!typeof(ITask).IsAssignableFrom(type)) continue;
// create a new schema element and name it after the type
// create a simpleType + extension
XmlSchemaElement element = CreateElement(type);
foreach(PropertyInfo property in type.GetProperties())
{
// if (property is defined in some base class) continue;
if (property.DeclaringType != type) continue;
// create schema attribute and name it after property name
// create a simpleType that
XmlSchemaAttribute attribute = CreateAttribute(property);
// add attribute to current element
AddAttribute(element, attribute);
}
}
// saving to disk
SaveSchema(schema);

There are a lot of interesting details to go through to implement this pseudo-code.
I won’t go through all of them. I will rather focus on some parts:

Translating enums into XSD

An enumeration can be represented in XSD by simple type restriction. For example,

[C#]

public enum HelloWorld
{
Hello,
World
}

[XSD]

<xs:simpleType name="HelloWorldType" base="msb:non_empty_integer">
    <
xs:restriction base="xs:string">
        <
xs:enumeration value="Hello" />
        <
xs:enumeration value="World" />
    </
xs:restriction>
</
xs:simpleType>

Getting custom attributes, the generic way

This is a well know (and very handy trick) that add a generic type parameter to a method to “strongly type” it.

T GetAttribute<T>(ICustomAttributeProvider t) where T : Attribute
{
object[] attributes = t.GetCustomAttributes(typeof(T), true);
if (attributes != null && attributes.Length > 0)
return (T)attributes[0];
else
return null;
}

 

Finding and adding documentation to the schema

Adding documention to the schema is straightforward, it is a matter of adding a annotation containing a document element:

<xs:attribute name="Assemblies" use="required">
    <
xs:annotation>
        <
xs:documentation>
            Gets or sets the list of path to analyse.
        </xs:documentation>
    </
xs:annotation>
</
xs:attribute>

The real trouble is to find what kind of documentation we are going to put there. We cannot rely solely on Reflection for this task because MsBuild custom attributes does not store any “documentation” data. Therefore, the best source of documentation is the XML documentation that is generated by the C# compiler (don’t forget to turn it on in Projects -> Properties -> Build). By default, the compiler dumps that file along side of the assembly so it is easy to find. Here’s a sample of how that file looks like:

<?xml version="1.0"?>
<
doc>
    <
assembly>
        <
name>TaskFoo</name>
    </
assembly>
    <
members>
        <
member name="T:TaskFoo.TaskSchema">
            <
summary>
                A Task that generates a XSD schema of the tasks in an assembly.
            </summary>
        </
member>
...

That is sweet. We can access the summary of each member very easily using XPath. For example:

//member[@name=”T:taskType.FullName”]/summary

This expression will return me the summary of the taskType type. Similar constructs can be mode for the property summary.

TaskSchema task reference

A Task that generates the XSD schemas for assemblies containing tasks.

Download: See Task on project distributor. Compiled against .Net 2.0 Beta 2.

Features:
  • Generates ready-to-use XSD schemas. No edition needed.
  • Generates task lists, i.e. the UsingTask sequence, to integrate easily your tasks into MsBuild,
  • If documentation is available, automatically adds documentation to the schema,
  • Task attribute are strongly typed,
  • Generates specific types for enumerations
  • It’s a MsBuild task! It will fit in your automation!
Attributes:
  • Assemblies: Required ITaskItem[] expression. The list of assemblies to analyse.
  • OutputPath: Optional string expression. The desired schemas output path.
  • Schemas: Output ITaskItem[] expression. For each assembly, the corresponding schema path.
  • CreateTaskList: Optional Boolean expression. A value that indicates if TaskList (see below) should be generated as well.
  • TaskLists: Output ITaskItem[] expression. For each assembly, the corresponding task list path. This output is null if CreateTaskList is false.

There are a couple more options. Run TaskSchema to have the full schema!

Examples:

This little msbuild project applies TaskSchema to itself (it is part of Churn.Tasks.dll):

<?xml version="1.0" encoding="utf-8" ?>
<
Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" DefaultTargets="CreateSchema">

    <
UsingTask AssemblyFile="Churn.Tasks\bin\Debug\Churn.Tasks.dll" TaskName="Churn.Tasks.TaskSchema" />

    <
ItemGroup>
        <
Assemblies Include="bin\Debug\TaskFoo.dll" />
    </
ItemGroup>

    <
Target Name="CreateSchema">
        <
TaskSchema
            Assemblies="@(Assemblies)" 
            OutputPath="Churn.Tasks\bin\Schemas" 
            />
    </
Target>
</
Project>

[Output]

Microsoft (R) Build Engine Version 2.0.50215.44
[Microsoft .NET Framework, Version 2.0.50215.44]
Copyright (C) Microsoft Corporation 2005. All rights reserved.
Build started 5/26/2005 8:19:51 PM.
__________________________________________________
Project "D:\Projects\Churn.NET\Churn.Tasks\taskschema.self.xml" (default targets):
Target CreateSchema:
Analysing bin\Debug\Churn.Tasks.dll
Found documentation file at Debug\Churn.Tasks.xml
Analyzing Churn.Tasks.TaskSchema
Creating Schema bin\Schemas\Churn.Tasks.Tasks.xsd
Create Task list bin\Schemas\Churn.Tasks.Tasks
Build succeeded.
0 Warning(s)
0 Error(s)
Time Elapsed 00:00:00.93

[Schema: TaskFoo.xsd]

Execute the sample and see for yourself!

posted on Monday, July 25, 2005 10:27:33 PM (Pacific Daylight Time, UTC-07:00)  #    Comments [5]
# Wednesday, July 20, 2005

After logging in, be sure to visit all the options under Configuration in the Admin Menu Bar above. There are 26 themes to choose from, and you can also create your own.

 

posted on Wednesday, July 20, 2005 12:00:00 AM (Pacific Daylight Time, UTC-07:00)  #    Comments [0]
# Tuesday, July 19, 2005

Andrew and Jay have been working hard lately and published the following article:

Unit Testing with NUnit, MbUnit and VSTS 

posted on Tuesday, July 19, 2005 9:45:30 AM (Pacific Daylight Time, UTC-07:00)  #    Comments [0]
# Friday, July 01, 2005

MbUnit gets a new wiki and issue tracking!

   http://www.mertner.com/confluence/display/MbUnit/Home

 

posted on Friday, July 01, 2005 4:55:29 PM (Pacific Daylight Time, UTC-07:00)  #    Comments [0]