Alex Meyer-Gleaves

on httpclient, rest, tools, web services

Using REST in NAnt with a custom HTTP Task

I needed to make HTTP requests to a REST web service from a NAnt script today so I knocked up a custom task. The HttpClient class from the WCF REST Starter Kit that I blogged about previously came in handy to offload most of the heavy lifting, leaving me to worry about the task related implementation details. The task supports all the HTTP methods and allows you to specify the content type and the content itself. You can also retrieve the response content and status code through properties set by the task. This was all achieved with surprisingly little code.

using System;
using System.Collections.Generic;
using System.Net;

using Microsoft.Http;

using NAnt.Core;
using NAnt.Core.Attributes;

namespace AlexMG.NAntTasks
{
    [TaskName("http")]
    public class HttpTask : Task
    {
        private static readonly List<HttpStatusCode> successCodes = new List<HttpStatusCode>
        {
            HttpStatusCode.OK,
            HttpStatusCode.Created,
            HttpStatusCode.Accepted,
            HttpStatusCode.NonAuthoritativeInformation,
            HttpStatusCode.NoContent,
            HttpStatusCode.ResetContent,
            HttpStatusCode.PartialContent
        };

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

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

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

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

        [TaskAttribute("connectiontimeout", Required = false)]
        public int ConnectionTimeout { get; set; }

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

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

        protected override void ExecuteTask()
        {
            HttpClient client = new HttpClient();
            HttpRequestMessage request = new HttpRequestMessage();

            if (!string.IsNullOrEmpty(Method))
            {
                request.Method = Method;
            }

            request.Uri = new Uri(Url);

            if (!string.IsNullOrEmpty(ContentType))
            {
                request.Headers.ContentType = ContentType;
            }

            if (!request.Method.Equals("GET", StringComparison.OrdinalIgnoreCase))
            {
                request.Content = (string.IsNullOrEmpty(Content)) ? HttpContent.CreateEmpty() : HttpContent.Create(Content);
                request.Headers.ContentLength = request.Content.GetLength();
            }

            if (ConnectionTimeout != 0)
            {
                client.TransportSettings.ConnectionTimeout = TimeSpan.FromSeconds(ConnectionTimeout);
            }

            Project.Log(Level.Info, "Executing HTTP request.");
            Project.Log(Level.Info, "Url: {0}", request.Uri);
            Project.Log(Level.Info, "Method: {0}", request.Method);
            Project.Log(Level.Info, "Content Type: {0}", request.Headers.ContentType);
            Project.Log(Level.Info, "Connection Timeout: {0}", client.TransportSettings.ConnectionTimeout);

            try
            {
                HttpResponseMessage response = client.Send(request);

                if (FailOnError)
                {
                    response.EnsureStatusIsSuccessful();    
                }

                if (!string.IsNullOrEmpty(StatusCodeProperty))
                {
                    Project.Properties[StatusCodeProperty] = response.StatusCode.ToString();
                }

                if (successCodes.Contains(response.StatusCode) && !string.IsNullOrEmpty(ResponseProperty))
                {
                    Project.Properties[ResponseProperty] = response.Content.ReadAsString();
                }

                Project.Log(Level.Info, "Received HTTP response.");
                Project.Log(Level.Info, "Status Code: {0}", response.StatusCode);
                Project.Log(Level.Info, "Content Type: {0}", response.Headers.ContentType);
            }
            catch (ArgumentOutOfRangeException ex)
            {
                string message = string.Format("The HTTP '{0}' request to '{1}' failed:{2}{3}", Method, Url, Environment.NewLine, ex.Message);
                throw new BuildException(message, ex);
            }
        }
    }
}

Using the task is simple. The only mandatory attribute is url and the default HTTP method is GET. Here is a sample NAnt project showing how to use the <http/> task.

<?xml version="1.0"?>
<project name="Http">
  <http url="http://www.howtocreate.co.uk/operaStuff/userjs/samplexml.xml"
        method="GET"
        contenttype="text/xml"
        connectiontimeout="30"
        responseproperty="response"
        statuscodeproperty="status"
        failonerror="true" />

  <echo message="Response: ${response}" />
  <echo message="Status Code: ${status}" />
</project>

The source code is available on GitHub. After pulling down the source build the solution with Visual Studio or by running msbuild Source\HttpNAnt.sln from the command line. The Dist folder will contain the AlexMG.HttpNAnt.dll assembly. The compiled assembly has the Microsoft.Http.dll assembly from the WCF REST Starter Kit merged into it using the ILMerge tool. This makes deployment easier by removing the chance of accidentally forgetting to deploy the dependency.

profile
AUTHOR

Alex Meyer-Gleaves

I'm a Technical Architect living in Australia (that island like continent in the southern hemisphere). I love Microsoft .NET and C#. I hate early mornings, slow drivers and Lotus Notes.