Using REST in NAnt with a custom HTTP Task

by Alex Meyer-Gleaves 18 June 2009 - 12:42 AM

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>

I have attached the source code and task assembly below. 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. If you are compiling from source code you will need to update the xcopy command in the post-build event to point to the location of your NAnt bin folder.

HttpNAnt Binary.zip (139.62 kb)

HttpNAnt Source.zip (1.06 mb)

Tags: , , ,

Categories: Garage Sale Code | Development Tools | Web Development | Web Services

Velocity DataCache Extension Methods

by Alex Meyer-Gleaves 11 June 2009 - 12:53 AM

If you are using the local cache configuration in Velocity you need to be careful how you use objects returned from the cache. The instance returned is a reference to the object stored inside the local cache, and is not a new instance that has been deserialized from the cache host. This means that if you make a change to the object, the change will be seen by any other caller retrieving the same object from the cache.

The current API does not provide a means to determine if the cache you are using has been configured with local caching. When the local caching option is used the type of the DataCache instance returned from the DataCacheFactory is actually an internal type called Microsoft.Data.Caching.LocalCache. The first extension method called IsLocalCache uses this knowledge to determine if you are working with a local cache.

/// <summary>
///        Determines whether the cache is configured as a local cache.
/// </summary>
/// <param name="dataCache">
///        The data cache being extended.
/// </param>
/// <returns>
///     <c>true</c> if configured as a local cache; otherwise, <c>false</c>.
/// </returns>
public static bool IsLocalCache(this DataCache dataCache)
{
    return (dataCache.GetType().FullName == "Microsoft.Data.Caching.LocalCache");
}

The next extension method is another Get method implementation that uses generics to avoid casting in the calling code. This method also accepts a Func<T> delegate that is used to create the object and add it to the cache if the entry for the specified key is not found. To make sure that the object returned from the local cache is not accidently modified the object is cloned before being returned. You can see in the code below that the cloning is performed in the clone method which I will explain in more detail next.

/// <summary>
///        Gets the cached object for the specified key.
/// </summary>
/// <typeparam name="T">
///        The type of the cached object.
/// </typeparam>
/// <param name="dataCache">
///        The data cache being extended.
/// </param>
/// <param name="key">
///        The key for the cached object.
/// </param>
/// <param name="creator">
///        A <see cref="Func{TResult}"/> delegate used to create the object if not found in the cache.
/// </param>
/// <returns></returns>
public static T Get<T>(this DataCache dataCache, string key, Func<T> creator)
{
    object value = dataCache.Get(key);
    if (value == null)
    {
        value = creator();
        dataCache.Put(key, value);
    }
    return dataCache.IsLocalCache() ? clone((T)value) : (T)value;
}

In the private clone method, I have used the DataContractSerializer class to perform the cloning as it works on types that are not explicitly marked as being Serializable or DataContract. The support for serializing POCO objects was added in .NET Framework 3.5 SP1. You can find out more about it here on Aaron Skonnard’s blog. You can see in the implementation below that the serialization cost can be avoided if the type being cloned is a value type or a string. The string class is not cloned because it is immutable and cannot be modified, and value types are of course always passed by value anyway.

private static T clone<T>(T instance)
{
    Type instanceType = typeof(T);
    if (instanceType.IsValueType || instanceType == typeof(string))
    {
        return instance;
    }

    DataContractSerializer serializer = new DataContractSerializer(instanceType);
    using (MemoryStream memoryStream = new MemoryStream())
    {
        serializer.WriteObject(memoryStream, instance);
        memoryStream.Position = 0;
        return (T)serializer.ReadObject(memoryStream);
    }
}

The final extension method is called CreateKey, and is a simple helper method that can be used to create keys for cache entries. It accepts a params array of object values that are concatenated with a pipe character as a separator between them. The ToString method is called on each object to obtain the string value used in the key.

/// <summary>
///        Creates a key to use for caching objects.
/// </summary>
/// <param name="dataCache">
///        The data cache being extended.
/// </param>
/// <param name="values">
///        The values to build the key from.
/// </param>
/// <returns>
///        A key to use for caching objects.
/// </returns>
public static string CreateKey(this DataCache dataCache, params object[] values)
{
    StringBuilder combinedValues = new StringBuilder();
    foreach (object value in values)
    {
        if (combinedValues.Length > 0)
        {
            combinedValues.Append("|");
        }
        combinedValues.Append(value);
    }
    return combinedValues.ToString();
}

Here are some code samples of how to call the extension methods based on some unit tests I created while writing the extensions. I have attached the code file for the extensions to the end of the post. The actual unit tests are not attached as those require Velocity to be installed and configured appropriately.

// Get the default cache using the factory.
DataCacheFactory factory = new DataCacheFactory();
DataCache cache = factory.GetDefaultCache();

// Test if the cache is configured as a local cache.
Assert.That(cache.IsLocalCache(), Is.True);

// Test that objects returned from the local cache are cloned.
Customer customer1 = cache.Get("customer", () => new Customer {FirstName = "John", LastName = "Smith"});
customer1.FirstName = "Frank";

Customer customer2 = cache.Get("customer", () => new Customer {FirstName = "John", LastName = "Smith"});

Assert.That(customer1.FirstName, Is.EqualTo("Frank"));
Assert.That(customer2.FirstName, Is.EqualTo("John"));

// Test that value types are supported.
int integer = cache.Get("integer", () => 123);
Assert.That(integer, Is.EqualTo(123));

// Test that cache keys are created correctly.
Assert.That(cache.CreateKey("customer", 123), Is.EqualTo("customer|123"));

DataCacheExtensions.cs (2.68 kb)

Tags:

Categories: Garage Sale Code | Microsoft .NET

Mocking support in Microsoft Velocity

by Alex Meyer-Gleaves 3 June 2009 - 2:06 AM

I have been looking at the Microsoft Distributed Cache and have found the been fairly impressed with the project. The download page offers a brief description of “Velocity”:

"Velocity" is a distributed in-memory application cache platform for developing scalable, high-performance applications. "Velocity" can be used to cache any common language runtime (CLR) object and provides access through simple APIs. The key aspects of "Velocity" are distributed cache performance, scalability, and availability.

Being a fan of TDD (Test-Driven Development) and mocking in my unit tests I was keen to see how well the API supported mocking. It turns out that the support for mocking is no where to be found. None of the classes implement interfaces and the important methods that support general caching operations are not virtual. Looking at the sample code below you can see how not having an IDataCache interface on the DataCache class will make unit testing the CustomerRepository class in isolation difficult. Even though the DataCache instance is passed into the constructor the interaction with it cannot be mocked.

public class CustomerRepository : ICustomerRepository
{
    private readonly DataCache _dataCache;

    public CustomerRepository(DataCache dataCache)
    {
        _dataCache = dataCache;
    }

    public Customer GetCustomer(int customerId)
    {
        Customer customer = (Customer)_dataCache.Get(customerId.ToString());
        if (customer == null)
        {
            customer = // Code that gets customer from the database.
            _dataCache.Put(customerId.ToString(), customer);
        }
        return customer;
    }
}

The idea of having my unit tests dependant on a running cache service is certainly not attractive, and while it would be possible to wrap the classes and provide an interface implementation of my own, this is not an attractive option either. The support for unit testing is now expected in the design of an API and Microsoft should be providing this for something as core as an application cache. I hope that the team at Microsoft sees this as a problem and provides some interfaces, or at very least makes the methods that support the primary caching operations virtual.

Tags:

Categories: Microsoft .NET

About the author

Alex Meyer-Gleaves I'm a software developer 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.

Google Reader Clips

SpringWidgets
RSS Reader
This widget is the staple of our platform. Read all your feeds right here with thisone widget - Supported feeds are OPML, RSS, RDF, ATOM. Watch your favorite Podcastin the embedded Video Player on the Desktop or publish your own video playlist toyour site for others to view!

Recent Comments

Comment RSS

Links

Disclaimer

The opinions expressed herein are my own personal opinions and do not represent my employer's view in  anyway.

© Copyright 2008