Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / programming / tools

.NET REST service

4.98/5 (16 votes)
25 May 2012CPOL4 min read 45.4K   642  
Self-installing version tracking REST service for builds

Introduction

This project is ambitious for its size;

  • REST web service (socket listener/HTTP parser)
  • Self-installing Windows services
  • 12.5 KB
  • Does something useful (get/set/increment/persist version numbers)

Image 1

Background

I set out to create a service to centralize build version number control, but ended up with an interesting study in light-weight RESTful service implementations in .NET. In build environments, version increments are typically solved with a hand written script, custom action, or workflow activity. It's typically not the prettiest aspect of a build solution. This project builds a more elegant REST service, it utilizes a RESTful service called directly from the build which directly increments and fetches any number of build versions. As an example, opening your web browser and putting the URL:

http://localhost:2313/LodeRunner?v=+

..would return 

3.0.0.1

The version of your favorite LodeRunner knock-off project build.

Self-install  

The self-installing aspects of this service actually came from this article... Thanks you much. The critical aspects are that you need to define a Service and an Installer, then funnel the incoming arguments as appropriate (see Versioner.Install.cs and Versioner.Services.cs).

Installer:

C#
[RunInstaller(true)]
public class Installer : inst.Installer
{
    public Installer()
    {
        Installers.Add(new ServiceProcessInstaller() { Account = ServiceAccount.LocalSystem });
        Installers.Add(new ServiceInstaller()
        {
            DelayedAutoStart = false,
            StartType = ServiceStartMode.Automatic,
            Description = "Tracks version numbers for build resources",
            ServiceName = Service.Name,
            DisplayName = "Versioner Service",
        });
    }
} 

This is required by the service manager to provide the basic information that you would see from the service manager. The service itself is in Versioner.Service. That is effectively the "main" of the service. We'll discuss that later when we talk about how it all works.

SimplyListen

There's a lot to setting up sockets correctly, so I created a helper. The class encapsulates the basic concept of listening and gives an Action<T> callback when a socket connects.

C#
public class SimplyListen : IDisposable
{
    private Action<Socket> _onConnect;
    private SimplyListen Listen(int port, Action<Socket> onConnect)
    {
        _onConnect = onConnect;
 
        // Create the listener socket in this machines IP address
        _socketLast = new Socket(AddressFamily.InterNetwork,
                          SocketType.Stream, ProtocolType.Tcp);
 
        _socketLast.Bind(new IPEndPoint(LocalHost, port));
        //listener.Bind( new IPEndPoint( IPAddress.Loopback, 399 ) );
        // For use with localhost 127.0.0.1
        _socketLast.Listen(10);
 
        // Setup a callback to be notified of connection requests
        _socketLast.BeginAccept(new AsyncCallback(OnConnectRequest), _socketLast);
 
        return this;
    }
...
}

The key call here is OnConnectRequest. That is a callback method that the socket layer calls when it's time to respond to a an incoming socket connection. The key to understanding this code is realizing there are two sockets... the one we've had open that's "listening" and the one the client is connecting to that you can receive incoming bytes.  The _socketLast.EndAcccept() call fetches that inner socket and passes it to our _onConnect Action<T>. From there, the caller can handle just the connection aspects without propping up all the other complexities of a socket. 

C#
private Socket _socketLast = null;
 
private void OnConnectRequest(IAsyncResult ar)
{
    lock (_socketLast)
    {
        _socketLast = ar.AsyncState as Socket;
    }
    _onConnect(_socketLast.EndAccept(ar));
    _socketLast.BeginAccept(new AsyncCallback(OnConnectRequest), _socketLast);
}

What's the Versioning Kenneth?!

So all this goop surrounds the real code, which is a persistent version tracking mechanism. The idea is that we store on disk a comma-separated list of version strings. It looks like this: 

bruce,1.0.3.1
bob,3.0.0.3
LoadRunner,1.0.0.1 

I use a pretty basic LINQ command to translate that into a Directory so it's handy in memory...

C#
Versions = !File.Exists (filename) ? new Dictionary<string, string>() :
    File.ReadAllLines(filename).Select(l =>
        l.Split(',')).Where(v=>v.Length==2).ToDictionary(k => k[0], v => v[1])

And this is  similar linq to save it

C#
File.WriteAllLines(filename,
    Versions.Select(v=>string.Format("{0},{1}", v.Key, v.Value)).ToArray());

The dirty work, putting it all together

The commands coming in from the URL parameters can be either the version you want to set or one or more pluses ('+') which means "increment" the version number by one. The count of pluses indicates which position in the version number to increment.  One plus means increment the last digit, two, the second to last, etc.  I've coded this such that there's no limit on the count of separators in the version number, 4 is standard, but I purposefully left the implementation robust so any will work. 

C#
private static string Increment(string name, string version)
{
    string response = version;
    if (!string.IsNullOrEmpty(version))
    {
        int plusCount = version.Count(c => '+' == c);

        if (plusCount > 0)
        {
            if (!vers.Versions.TryGetValue(name, out response))
                response = "1.0.0.0";

            response = Versioner.Increment(response, plusCount);
        }
    }
    return response;
}

This uses a static method in the Versioner to do the actual number increment.  It splits the version string into an array, parses to integer, increments the correct location, then re-joins.  

C#
public static string Increment(string response, int incIndex)
{
    var ints = response.Split('.').Select(i => int.Parse(i)).ToArray();
    int pos = ints.Length - incIndex;

    if (pos < 0)
        return response;

    ints[pos] = ints[pos]+1;
    while (++pos < ints.Length)
        ints[pos] = 0;

    return string.Join(".", ints.Select(i => i.ToString()));
}

If we have determined our version number, now we need to update the in memory dictionary and save it.  This code also handles the case where we are just asking for the version number (no version parameters at all).  That's the "TryGet".

C#
private static string UpdateVersions(string name, string response)
{
    if (!string.IsNullOrEmpty(response))
    {
        if (vers.Versions.ContainsKey(name))
            vers.Versions[name] = response;
        else
            vers.Versions.Add(name, response);
 
        vers.Save(_versionFilename);
    }
    else if (!vers.Versions.TryGetValue(name, out response))
    {
        response = string.Empty;
    }
 
    return response;
}

Finally, here's the code that wraps it all together.  It's in the service handler's lambda that responds to a connection attempt.  

  • ReadHttpRequestBuffer() reads from the socket and returns the name and versions parameter.
  • Increment() as requested by the version parameter
  • UpdateVersions() update the in-memory dictionary and persist the version to disk
  • Send response as a byte array
C#
string name, version;
if (SimplyListen.ReadHttpRequestBuffer(s, out name, out version))
{
    var response = UpdateVersions(name, Increment(name, version));
    // Convert to byte array and send.

    var byteDateLine = System.Text.Encoding.ASCII.GetBytes(response);

    s.Send(byteDateLine, byteDateLine.Length, 0);
}

Using it  

To install:

  • Copy to where you want it
  • Start a command prompt AS ADMIN
  • Change directory to the copy location
  • Run...
Versioner.exe /i
 
Running a transacted installation.
 
Beginning the Install phase of the installation.
.
.

Once installed you can launch any browser and type this:

http://localhost:2313/LodeRunner?v=1.0.0.0 
 1.0.0.0

http://localhost:2313/LodeRunner?v=++
 1.0.1.0 

The end of the URL is the name of the project, the 'v' parameter has two modes:

  • If you specify a period separated list of integers, it will set the version number for that entity.
  • If you specify any number of '+' characters it will set the appropriate index in the version number...

Example: 'v=+' increments (and stores) the "build" number, 'v=+++' increments the minor version number.

Future: Putting to use in an actual build

You'll need to implement how you want to get this into your build, but over the next few weeks, I will likely add a second part to this article to update the build number in the actual TFS Build workflow.    

History

  • 17 May '12 - Created  
  • 19 May '12 - Rewrote intro to be more generic
  • 25 May '12 - Fixed bug in localhost 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)