Friday, October 8, 2010

Simple NAnt dependency manager for the TeamCity repository

I did not found any suitable tool that can be used to load dependencies when building .NET project in a TeamCity environment (using NAnt scripts).
Until now all dependencies where stored in VCS together with the source, not so sophisticated.

All our TeamCity projects contains one SDK-configuration that is a zipped file with all output assemblies and executables.

The TeamCity offers URL patterns to access build artifacts. For example:

http://teamcity.mycompany.com/guestAuth/repository/download/AGroup::AProduct/1.1.124/SDK/AProduct_SDK-1.1.124.zip

These two facts trigged me to build a custom NAnt task.
It simply downloads SDK-artifacts from the TeamCity repository, and unpacks the assemblies in the current build environment before the build is started.

I added a “pom”-file to all TeamCity configuration projects that defines the dependencies:

<project>
 <dependencyManagement>
   <repositories>
     <repository>http://teamcity.mycompany.com/guestAuth/repository/download/</repository>
   </repositories>
   <dependencies>
     <dependency>
       <groupId>AGroup</groupId>
       <artifactId>AProduct</artifactId>
       <version>1.1.124.0</version>
     </dependency>
     <dependency>
       <groupId>AGroup</groupId>
       <artifactId>BProduct</artifactId>
       <version>5.4.42.0</version>
     </dependency>
     <dependency>
       <groupId>BGroup</groupId>
       <artifactId>CProduct</artifactId>
       <version>2.5.24.0</version>
     </dependency>
   </dependencies>
 </dependencyManagement>
</project>

The NAnt task is executed in the build file:

<target name="loadDependencies">
  <loaddependencies filename="${Build.Base}\dependencies.xml" target="${Build.Base}\Dependencies"/>
</target>
The two parameters defines the name of the dependency file and where I want the assemblies to be copied.

And here is the source if someone is interested:

using System;
using System.Collections.Generic;

namespace MyNAnt.Build.Tasks
{
    using System.IO;
    using System.Net;
    using System.Xml;

    using global::NAnt.Core;
    using global::NAnt.Core.Attributes;
    using ICSharpCode.SharpZipLib.Zip;

    [TaskName("loaddependencies")]
    public class LoadDependenciesTask : Task
    {
        // NAnt parameters
        [TaskAttribute("filename", Required = true)]
        [StringValidator(AllowEmpty = false)]
        public string DependencyFile { get; set; }

        [TaskAttribute("target", Required = true)]
        [StringValidator(AllowEmpty = false)]
        public string Target { get; set; }

        // Lokal parameters
        private List<Dependency> Dependencies { get; set; }
        private List<string> Repositories { get; set; }

        /// <summary>
        /// Executes the NAnt task
        /// </summary>
        protected override void ExecuteTask()
        {
            // load all dependency information
            LoadDependencies();

            // download dependencies from TeamCity
            foreach (var dependency in Dependencies)
            {
                DownloadArtifact(Repositories, dependency.Group, dependency.Name, dependency.Version, Target);
            }
        }

        /// <summary>
        /// Reads the dependency list from configuration file
        /// </summary>
        private void LoadDependencies()
        {
            var doc = new XmlDocument();

            Log(Level.Info, "Loading dependencies from '{0}'.", DependencyFile);
            doc.Load(DependencyFile);

            // load all repositories to search for assemblies
            var repList = new List<string>();
            var repElemList = doc.GetElementsByTagName("repository");
            foreach (XmlNode repository in repElemList)
            {
                repList.Add(repository.InnerText);
            }
            Repositories = repList;

            // load all dependency assemblies
            var depList = new List<Dependency>();
            var depElemList = doc.GetElementsByTagName("dependency");
            foreach (XmlNode dependency in depElemList)
            {
                var item = new Dependency();
                if (dependency != null)
                {
                    item.Group = dependency["groupId"].InnerText;
                    item.Name = dependency["artifactId"].InnerText;
                    item.Version = dependency["version"].InnerText;
                }
                depList.Add(item);
            }
            Dependencies = depList;
        }

        /// <summary>
        /// Downloads and unzip artifact from TeamCity repository
        /// </summary>
        /// <param name="repositoryList"></param>
        /// <param name="group"></param>
        /// <param name="name"></param>
        /// <param name="version"></param>
        /// <param name="destination"></param>
        private void DownloadArtifact(List<string> repositoryList, string group, string name, string version, string destination)
        {
            Log(Level.Info, "Destination folder: '{0}'.", destination);

            // create destination folder if it not exist
            Directory.CreateDirectory(destination);

            foreach (var repository in repositoryList)
            {
                // URL-example
                // http://teamcity.mycompany.com/guestAuth/repository/download/AGroup::AProduct/1.1.124/SDK/AProduct_SDK-1.1.124.zip
                var address = repository + group + "::" + name + "/" + version + "/SDK/" + name + "_SDK-" + version + ".zip";

                using (var wc = new WebClient())
                {
                    try
                    {
                        using (var streamRemote = wc.OpenRead(new Uri(address)))
                        {
                            Log(Level.Info, "Found artifact '{0}'.", address);

                            var zis = new ZipInputStream(streamRemote);
                            ZipEntry ze;
                            while ((ze = zis.GetNextEntry()) != null)
                            {
                                if (ze.IsDirectory)
                                {
                                    Directory.CreateDirectory(ze.Name);
                                }
                                else
                                {
                                    var buffer = new byte[2048];
                                    var fileName = Path.GetFileName(ze.Name);

                                    Log(Level.Info, "Unzipping '{0}'.", fileName);

                                    using (
                                        Stream outstream = new FileStream(
                                            destination + "\\" + fileName, FileMode.Create))
                                    {
                                        while (true)
                                        {
                                            var bytes = zis.Read(buffer, 0, 2048);
                                            if (bytes > 0) outstream.Write(buffer, 0, bytes);
                                            else break;
                                        }
                                    }
                                }
                            }
                            return;
                        }
                    }
                    catch (Exception)
                    {
                        // ignore exceptions
                    }
                }
            }

            Log(Level.Error, "ERROR: Artifact '{0}::{1}' with version '{2}' not found!.", group, name, version);
        }
    }
}

Friday, September 24, 2010

WSDL-First development with Visual Studio

This is how I implemented the WSDL-First approach when creating a WCF service with Visual Studio.

An example of a simple WSDL:

<?xml version="1.0" encoding="utf-8"?>
<definitions
  xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
  xmlns:tns="http://www.myweb.com/ws/"
  xmlns:s="http://www.w3.org/2001/XMLSchema"
  xmlns:http="http://schemas.xmlsoap.org/wsdl/http/"
  targetNamespace="http://www.myweb.com/ws/"
  xmlns="http://schemas.xmlsoap.org/wsdl/">
  <types>
    <s:schema elementFormDefault="qualified" targetNamespace="http://www.myweb.com/ws/">
      <s:element name="DoSomething">
        <s:complexType>
          <s:sequence>
            <s:element minOccurs="0" maxOccurs="1" name="param1" type="s:string" />
            <s:element minOccurs="0" maxOccurs="1" name="param2" type="s:string" />
          </s:sequence>
        </s:complexType>
      </s:element>
      <s:element name="DoSomethingResponse">
        <s:complexType>
          <s:sequence>
            <s:element minOccurs="0" maxOccurs="1" name="DoSomethingResult" type="tns:TheDTO" />
          </s:sequence>
        </s:complexType>
      </s:element>
      <s:complexType name="TheDTO">
        <s:complexContent mixed="false">
          <s:extension base="tns:BaseDTO">
            <s:sequence>
              <s:element minOccurs="0" maxOccurs="1" name="Att1" type="s:string" />
            </s:sequence>
          </s:extension>
        </s:complexContent>
      </s:complexType>
      <s:complexType name="BaseDTO" abstract="true">
        <s:sequence>
          <s:element minOccurs="0" maxOccurs="1" name="BaseAtt1" type="s:string" />
          <s:element minOccurs="0" maxOccurs="1" name="BaseAtt2" type="s:string" />
        </s:sequence>
      </s:complexType>
    </s:schema>
  </types>
  <message name="DoSomethingSoapIn">
    <part name="parameters" element="tns:DoSomething" />
  </message>
  <message name="DoSomethingSoapOut">
    <part name="parameters" element="tns:DoSomethingResponse" />
  </message>
  <portType name="IService">
    <operation name="DoSomething">
      <documentation xmlns="http://schemas.xmlsoap.org/wsdl/">Do something.</documentation>
      <input message="tns:DoSomethingSoapIn" />
      <output message="tns:DoSomethingSoapOut" />
    </operation>
  </portType>
  <binding name="TheServiceSoap" type="tns:IService">
    <soap:binding transport="http://schemas.xmlsoap.org/soap/http" />
    <operation name="DoSomething">
      <input>
        <soap:body use="literal" />
      </input>
      <output>
        <soap:body use="literal" />
      </output>
    </operation>
  </binding>
  <service name="TheService">
    <port binding="tns:TheServiceSoap" name="TheServicePort"/>
  </service>
</definitions>

Tip: It can be useful to download a WSDL-template to start with, instead of writing it from scratch.
Use the svcutil utility to save the WSDL-file exported from a service:

> svcutil /t:metadata http://localhost:8731/TheService/Service/

I added a Pre-Build command that reads my WSDL-file and creates an interface file in preferred language, in my case C#:

svcutil /language:C# /n:*,TheService /out:$(ProjectDir)IService.cs $(ProjectDir)\TheService.wsdl

The IService.cs will be regenerated each time I initiates a build so I adds the implementation of the interface in a separate file, Service.cs.

using System;
namespace TheService
{
    using System.ServiceModel;

    [ServiceBehavior(Namespace = "http://www.myweb.com/ws/")]
    public class Service : IService
    {
        public TheDTO DoSomething(string param1, string param2)
        {
            throw new NotImplementedException();
        }
    }
}

It is preferable to store the WSDL file separately in the VCS, then you can redesign both server and clients without affecting the functionality, as long as they use the same version of the WSDL file.

 

The WSDL can now be used to create service clients.
For example, to create a .NET client, use the svcutil utility to create the service proxy code:

> svcutil TheService.wsdl

The svcutil utility creates a Service.cs that you include in the client project, and then the service can be invoked as if it were a local object.

var myService = new ServiceClient();

TheDTO result = myService.DoSomething("A", "B");

Wednesday, January 20, 2010

Subversion authentication problem in Hudson

I tried to move a TeamCity .NET project to Hudson.
Build script is written in NAnt so I only needed to change the TeamCity environment properties to the Hudson equivalents.

But I run into the same authentication problem as with TeamCity described in earlier post:

ERROR: Failed to update https://<removed>/svn/MyProject/trunk org.tmatesoft.svn.core.SVNCancelException: svn: authentication cancelled

Hudson uses the SvnKit as well so I was pretty sure what the cause was, the Java NTLM implementation.

After adding the property -Dsvnkit.http.ntlm=jna to the Hudson configuration file, hudson.xml, and restarting the Hudson Windows service, everything worked perfectly!