A pleasant walk through computing

Comment for me? Send an email. I might even update the post!

TFS Continuous Integration and Private NuGet Package Sources

The Summary

What if you

  1. Don't store your NuGet packages in source control.
  2. Have a NuGet package that's hosted in a private source.
  3. Need to locally test continuous integration.
  4. Or need offsite (i.e. cloud) continuous integration.

There are a few general solutions.

  1. Store the private packages in source control. I'd do this one.
  2. Set up public access to the private source. Not likely.
  3. For testing, set up a local private NuGet source (just point to the folder).

The Problems

NuGet packages are generally great. But there can be problems when it comes to source control and continuous integration.

Most sites' advice on storing NuGet packages in source control (regardless of git, Mercurial, TFS, SVN, etc) is: don't.

The reason is pretty simple. If package restore is enabled, they'll get downloaded and rebuilt anyway, so why store them and take up repository space? With the later NuGet installations, a cache is maintained in the user's profile, so a trip to the NuGet servers might not even be necessary.

The counter argument is also pretty simple. What if the NuGet source isn't available? Suddenly, you can't restore your packages, can't build, can't work. When would this happen?

All of these assume you've downloaded from source control (such as GitHub or TFS), but haven't built the code yet.

  1. You get on a plane and then try to build. If you have no Internet, you can't get the packages.
  2. For some other reason you don't have an Internet connection.
  3. You're connected, but the NuGet site is down.
  4. One of the packages uses a locally (i.e. corporate internal) hosted NuGet source. It's not on the internet, so you can't download the package.
  5. It's been a long time since anyone's built the source, and a package has been removed from NuGet.
  6. You use the cloud for continuous integration, which won't have access to your private NuGet source.

Most of the above can be solved by building the solution immediately after getting it. But here's a real-world example of number 4. I was working for a client, and had access to their Team Foundation Server. I got the source via a VPN connection, copied it to my laptop, and tried to build. This client has several NuGet packages they host locally. They're proprietary, so hosting them on a public site like NuGet would be wrong.

And I didn't have access to that package source, so I couldn't build.

Again, these problems could be solved, and maybe they point to some environment changes needing to be made. But wouldn't it be just as easy to include the package in source control?

Including the NuGet Package Folder in TFS

If you're using a version of TFS prior to 2012, I can't help you (and you should upgrade). Starting in TFS 2012, the tfignore file became available. The purpose of tfignore is just like gitignore and hgignore: tell source control which files to not display for adding/tracking.

But it can also be used to explicitly allow files. Why is this needed? Because, unlike git or Mercurial, TFS + Visual Studio ignores certain files by default, and I haven't found a way to change that or even find out what files those are. Dlls are ignored by default, for example.

  1. Create the .tfignore file in the root of the team project.
  2. Edit the file.
  3. Add the package fies to TFS.
  4. Add your .tfignore file to source control.

The manual way to create a .tfignore file is, in the project folder root

  1. Right-click > New > Text File
  2. Enter .tfignore.

See the trailing period? That's the magic sauce. Otherwise, Windows doesn't let you create a file/folder with a leading period.

Visual Studio shows package folders in Excluded Changes by default. But dlls, and sometimes lib folders, are not included, and we absolutely need those.

Here's how to include the entire packages folder. The leading ! means "don't ignore".

!packages\*

These don't work. Note, especially, that leading backslash doesn't work, even thought that's what VS itself will create via the GUI.

  • !\packages*.*
  • !packages
  • !packages\

To include a specific package, such as one that's hosted in a private NuGet source, ignore the packages folder, then specify the package path without the version number.

packages
!packages\My.Private.Utility.*

In Visual Studio Team Explorer, choose Pending Changes, find Excluded Changes, click Detected: x adds(s).

2017-03-03_101910

Deselect all files (they're selected by default, a bad choice), then check the ones you want and Promote them (which is the same as "add" in git or Mercurial).

2017-03-03_103218

Using a Private Repo for Testing

If you're testing CI on a separate machine, you can use or modify a nuget.config file to include that source. I tried this at the project level, but it didn't work, so I created the nuget.config file at the solution level. (But that ability is supposedly removed in later NuGet versions. So confusing!)

Here's an example file:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageRestore>
    <add key="enabled" value="True" />
    <add key="automatic" value="True" />
  </packageRestore>
  <packageSources>
    <add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
  <add key="local" value="C:\Users\charles\Documents\Testing\NuGet\" />
  </packageSources>
</configuration>

Again, this must not end up in the production source control repository!

The Wrap Up

I'm not weighing in on storing NuGet packages in source control, except to say that there are some situations where it's clear to me it's a good ideal. Having a private NuGet source involved is one.

TFS Continuous Integration Walk Through Part 5b - Multiple Solutions: Simple Project References

ci-logoThis is part of a series of walk throughs exploring CI in TFS, starting from the ground up. The entire series and source code are maintained at this BitBucket repository.

https://bitbucket.org/bladewolf55/tfs-ci-samples

Previous Part: TFS Continuous Integration Walk Through Part 5a - Multiple Solutions - Overview

A New Beginning

One of my goals for the Multiple Solutions walkthroughs is to put the TFS repository in a, let's say, less than stellar organizational state, and then clean up. To do that, I'll start with a new, clean TFS collection.

Open TFS Administration Console, select Team Project Collections, and click Create Collection.

2017-02-20_145443

Choose a silly name like "CICollection2". Next.

2017-02-20_145927

Verify the SQL server instance and create a new database. Next, Verify, Create.

2017-02-20_150035

It'll take a few minute to create the collection. Click Complete, then Close. You can close the Admin Console, too.

2017-02-20_150517

Connect Visual Studio to the New Collection

Complaint
This was stupidly difficult to figure out. Google searches yielded nothing.

Open Visual Studio and the Team Explorer, then open Manage Connections.

2017-02-20_161007

Drop down "Manage Connections" and choose Connect to Team Project.

2017-02-20_161132

Select CICollection2, then click Connect.

2017-02-20_161318

This is the first time I'm using this collection, so I need to map my TFS Workspaces.

Complaint
I hate TFS workspaces.

2017-02-20_161528

Pick one of the "map workspaces" links, take the defaults, click Map & Get.

2017-02-20_161942

Now I'm connected to the new collection.

2017-02-20_162046

Create a new Team Project

In Visual Studio Team Manager, create a new Team Project by choosing Home > Projects & My Teams > Create Team Project.

2017-02-20_162400

Remember, one of my intentions is a messy collection. So, I'll name my team project Main. (I'm tempted to name it "Turing" and make life even worse, but this will do.) Click Next.

2017-02-20_162607

The default Agile process is fine. Next.

2017-02-20_162652

We're sticking with Team Foundation Version Control. Click Finish.

2017-02-20_162805

After a few seconds our team project is created.

2017-02-20_162930

Add the TuringDesktop Solution

In Visual Studio, create a new Console project. In the dialog, check the Add to Source Control box.

2017-02-20_163059

In the source control dialog, accept the defaults. We're adding our solution folder to the root.

2017-02-20_163216

So, right now our TFS collection structure is:

CICollection2
|_$/Main
  |_TuringDesktop

If you remember from the previous part, our desktop app is going to have two dependencies: the Magic8Engine, and TextColor. Right now, I don't know Magic8 is going to be a shared NuGet package, so I just add it as a new Class Library project. The same with TextColor.

I'm also going to need a unit test project, so I'll add an MSTest project now as well. I'm going to name it TuringDesktop.Tests, even though it's going to contain tests for all my projects.

2017-02-20_164651

In the end, this is my solution's folder structure.

2017-02-20_164939

Our First Suite

References

Assign TextWriter to MemoryWriter
Mocking System Console Behaviour
Magic 8-Ball

Below is all the code and tests for the solution. This is my super-pre-release versions, and has some (mostly intentional) problems.

The Solution Projects

Right now I've got everything in one solution. I'll explain each project in turn.

TuringDesktop

This is a Console application. It references the Magic8Engine and TextColor projects.

2017-02-22_102302

I'm simulating a database with a class that returns a list of answers, and is initialized with a connection string. For this simple sample, the connection string is just a name like "production". If the connection string is named something other than "production", then the name is prepended to each answer. For example, if the connection string were "stage", an answer might be "STAGE: It is certain."

using System;
using System.Collections.Generic;
using System.Linq;

namespace TuringDesktop
{
    public static class AnswerDatabase
    {
        /// <summary>
        /// Any dbName other than "prod" prepends answers with dbName.
        /// Ex. dbName = "stage", then answer is "STAGE: It could be so."
        /// </summary>
        /// <param name="connectionString"></param>
        public static IEnumerable<string> GetAnswers(string connectionString)
        {
            if (String.IsNullOrWhiteSpace(connectionString))
            {
                throw new ArgumentException("dbName cannot be null or empty");
            }
            if (connectionString == "production")
            {
                return DefaultAnswers;
            }
            else
            {
                return DefaultAnswers.Select(a => connectionString.ToUpper() + ": " + a);
            }
        }

        private static IEnumerable<string> DefaultAnswers
        {
            get
            {
                yield return "It is certain";
                yield return "It is decidedly so";
                yield return "Without a doubt";
                yield return "Yes, definitely";
                yield return "You may rely on it";
                yield return "As I see it, yes";
                yield return "Most likely";
                yield return "Outlook good";
                yield return "Yes";
                yield return "Signs point to yes";
                yield return "Reply hazy try again";
                yield return "Ask again later";
                yield return "Better not tell you now";
                yield return "Cannot predict now";
                yield return "Concentrate and ask again";
                yield return "Don't count on it";
                yield return "My reply is no";
                yield return "My sources say no";
                yield return "Outlook not so good";
                yield return "Very doubtful";
            }
        }
    }
}

Here's the Program class with my Main code. There are a few things to note:

  • I removed the args parameter from the Main method. I don't need it.
  • I made the Main method public, so it can be call from unit tests.
  • I'm using Dependency Injection for the answer engine (Oracle) and the console text colorizer (ConsoleColorizer).
  • There are three settings using a string literal: _connectionString, questionColor and answerColor.
using System;
using TextColor;
using Magic8Engine;

namespace TuringDesktop
{
    public class Program
    {
        static string _connectionString = "production";
        static IConsoleColorizer _colorizer = new ConsoleColorizer();
        static IOracle _oracle = new Oracle(AnswerDatabase.GetAnswers(_connectionString));

        public Program() { }
        public Program(IConsoleColorizer colorizer, IOracle oracle)
        {
            _colorizer = colorizer;
            _oracle = oracle;
        }

        public static void Main()
        {
            string name = "";
            string answer = "";
            string question = "";
            string questionColor = "Red";
            string answerColor = "green";

            Console.Write("Welcome! I'm AT. Please tell me your name. >> ");
            name = Console.ReadLine();
            Console.WriteLine("Good to meet you, " + name + ". "
                + "Ask me a yes-or-no question and I'll give you an answer. "
                + "When you're finished, say 'bye'.");
            do
            {
                try
                {
                    _colorizer.ColorizeWriteLine("Question?", questionColor);
                    question = Console.ReadLine();
                    if (question.ToLower() == "bye") { break; }
                    answer = _oracle.GetAnswer();
                    _colorizer.ColorizeWriteLine(answer, answerColor);
                    Console.WriteLine();
                }
                catch (Exception ex)
                {
                    Console.WriteLine("Error: " + ex.GetBaseException().Message);
                    Console.Write("Press any key to quit.");
                    Console.Read();
                    return;
                }
            }
            while (true);
        }
    }
}

The program's output is simple enough. It greets the user, asks for a name, then starts answering questions. Text coloring is used to make things clear.

2017-02-22_105715

Magic8Engine

I have an IOracle inteface, so that it's easier to use in unit tests.

namespace Magic8Engine
{
    public interface IOracle
    {
        string GetAnswer();
    }
}

The concrete class is initialized with a list of answers (the "database") from which it randomly selects.

using System;
using System.Collections.Generic;
using System.Linq;

namespace Magic8Engine
{
    public class Oracle : IOracle
    {
        Random _random = new Random();
        private IEnumerable<string> _answers = new List<string>();

        public Oracle(IEnumerable<string> answers)
        {
            _answers = answers;
        }

        public string GetAnswer()
        {
            if (_answers.Count() == 0)
            {
                return "";
            }
            int index = _random.Next(_answers.Count());
            return _answers.ToArray()[index];
        }
    }
}

TextColor

The ConsoleColorizer class also implements an interface to improve unit testing.

using System;

namespace TextColor
{
    public interface IConsoleColorizer
    {
        void ColorizeWriteLine(string text, string colorName, bool resetColor = true);
        ConsoleColor GetConsoleColor(string colorName);
        void ResetConsoleColor();
        void SetConsoleColor(string colorName);
    }
}

The concrete class sets properties on the Console object.

using System;

namespace TextColor
{
    public class ConsoleColorizer : IConsoleColorizer
    {
        public void ColorizeWriteLine(string text, string colorName, bool resetColor = true)
        {
            SetConsoleColor(colorName);
            Console.WriteLine(text);
            if (resetColor) { Console.ResetColor(); }
        }

        public ConsoleColor GetConsoleColor(string colorName)
        {
            ConsoleColor color;
            if (Enum.TryParse<ConsoleColor>(colorName, true, out color))
            {
                return color;
            }
            else
            {
                throw new ArgumentException("Invalid ConsoleColor: " + colorName);
            }
        }

        public void SetConsoleColor(string colorName)
        {
            Console.ForegroundColor = GetConsoleColor(colorName);
        }

        public void ResetConsoleColor()
        {
            Console.ResetColor();
        }
    }
}

This should raise a red flag. System.Console is a dependency. Why didn't I abstract that out? I could have. For example, I could have created an IConsole interface with just the features I'm using, then an explicit SystemConsole wrapper class that implented the interface. But it turned out I could test Console successfully without needing to make it entirely replacable.

Good question though. Glad you're thinking.

Tests

Finally, our test project has a class for each project we're testing.

2017-02-22_112309

Here are the unit tests for each project. I'm using nested classes to keep the tests readable.

Magic8 Tests

Note how these test make use of passing in a custom "answer database".

using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Magic8Engine;

namespace TuringDesktop.Tests
{
    [TestClass]
    public class OracleClass
    {
        [TestClass]
        public class GetAnswer_Should: OracleClass
        {
            static List<string> _answers;

            [TestInitialize]
            public void TestInitialize()
            {
                //Start each test with empty list;
                _answers = new List<string>();
            }

            [TestMethod]
            public void ReturnARandomAnswerEachTimeItIsCalled()
            {
                //arrange
                _answers.AddRange(new string[] { "a", "b", "c" });
                var oracle = new Oracle(_answers);
                //act
                List<string> answers = new List<string>();
                for (int i = 0; i < 10; i++)
                {
                    answers.Add(oracle.GetAnswer());
                }
                int uniqueAnswers = answers.Distinct().Count();
                //assert
                Assert.IsTrue(uniqueAnswers > 1);
            }

            [TestMethod]
            public void ReturnAnEmptyAnswerIfAnswerListIsEmpty()
            {
                //arrange
                var oracle = new Oracle(_answers);
                //act
                string actual = oracle.GetAnswer();
                //assert
                Assert.AreEqual("", actual, "Answer list count: " + _answers.Count());
            }
        }
    }
}

TextColor Tests

Neither of these tests confirms the correct color gets set. That's really something best left to a human to verify. But notice in WriteTheUserEnteredString how I'm using the Console.SetOut property to let me verify that user input actually gets written to the console.

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.IO;
using TextColor;

namespace TuringDesktop.Tests
{
    [TestClass]
    public class ConsoleColorizerClass
    {
        [TestClass]
        public class GetConsoleColor_Should
        {
            [TestMethod]          
            public void ReturnTheEnumUsingCaseInsensitive()
            {
                //arrange
                var tc = new ConsoleColorizer();
                string colorName = "gReEn";
                Exception actualEx = null;
                string errorMsg = "";
                //Will change to Green below.
                ConsoleColor color = ConsoleColor.Black;
                //act
                try
                {
                    color = tc.GetConsoleColor(colorName);
                }
                catch (Exception ex) { actualEx = ex; errorMsg = "Threw error " + ex.GetBaseException().Message; }
                Assert.IsNull(actualEx, errorMsg);
                Assert.AreEqual(ConsoleColor.Green, color);
            }

            [TestMethod]
            public void WriteTheUserEnteredString()
            {
                //arrange
                //Store the Console output in a stringwriter.
                StringWriter sw = new StringWriter();
                Console.SetOut(sw);
                var tc = new ConsoleColorizer();
                //act
                tc.ColorizeWriteLine("blamo", "Red");
                //assert
                Assert.AreEqual("blamo\r\n", sw.ToString());
            }
        }
    }
}

Console App Tests

If you're thinking the Main method should be refactored, you're right. I'll do that later. For now, it's interesting to see how we can test a console app.

First, I'll create my mock objects. I'm not even checking MockOracle's GetAnswer method, but that's OK. What I'm guaranteeing is my unit tests have no external dependencies.

MockOracle

    public class MockOracle : Magic8Engine.IOracle
    {
        public string GetAnswer()
        {
            return "This is a fake answer";
        }
    }

MockColorizer

In this mock, I store the simulated user's input in a list, and I always return a black console color. I don't set or reset console colors.

Important
Remember, I'm not testing if the colorizer works. There are already unit tests for that. I'm testing if the Main method works. I just need my mock objects to return consistent results quickly.

    public class MockColorizer : TextColor.IConsoleColorizer
    {
        public List<string> ConsoleLines = new List<string>();


        public void ColorizeWriteLine(string text, string colorName, bool resetColor = true)
        {
            ConsoleLines.Add(text);
        }

        public ConsoleColor GetConsoleColor(string colorName)
        {
            return ConsoleColor.Black;
        }

        public void ResetConsoleColor() { }

        public void SetConsoleColor(string colorName) { }
    }

Here are the tests for the Main method. Notice how I'm using StringWriters for both Console.In and Console.Out. This lets me buffer all the responses a user would make, and capture the console's output. I use a couple of helper methods to make this work.

What I'm not trying to do is test if Console works. I'm testing if I'm my code that writes to the console works. But, unfortunately, I do have a dependency on Console.

So, are these unit tests, or integration tests? Short answer: integration. I'll make some improvements later to isolate code that doesn't depend on Console.

There's a real danger here that I'll forget to use SendUserInputs, leading to the application hanging. I know this danger exists because I did it. The tests are brittle.

    [TestClass]
    public class ProgramClass
    {
        [TestClass]
        public class Main_Should
        {
            StringWriter _consoleOut = new StringWriter();
            string UserInputs = "";

            [TestInitialize]
            public void TestInitialize()
            {
                //Store the Console output in a stringwriter.
                Console.SetOut(_consoleOut);
            }

            [TestMethod]
            public void DisplayInitialGreetingMessage()
            {
                //arrange
                AddUserInput("Charles");
                AddUserInput("bye");
                SendUserInputs();
                var mockColorizer = new MockColorizer();
                var mockOracle = new MockOracle();
                var program = new Program(mockColorizer, mockOracle);
                string expected = "Welcome! I'm AT. Please tell me your name.";
                //act
                Program.Main();
                //assert
                string output = _consoleOut.ToString();
                Assert.IsTrue(output.IndexOf(expected) >= 0);
            }

            [TestMethod]
            public void DisplayWelcomeWithName()
            {
                //arrange
                AddUserInput("Charles");
                AddUserInput("bye");
                SendUserInputs();
                var mockColorizer = new MockColorizer();
                var mockOracle = new MockOracle();
                var program = new Program(mockColorizer, mockOracle);
                string expected = "Good to meet you, Charles.";
                //act
                Program.Main();
                //
                //assert
                string output = _consoleOut.ToString();
                Assert.IsTrue(output.IndexOf(expected) >= 0, "Output was: " + output);
            }

            #region "Test Helpers"
            private void AddUserInput(string value)
            {
                UserInputs += value + Environment.NewLine;
            }

            private void SendUserInputs()
            {
                //Send all the inputs needed for the Read and ReadLine statements
                StringReader consoleIn = new StringReader(UserInputs);
                Console.SetIn(consoleIn);
            }
            #endregion
        }
    }

Running my tests, I get nice, readable output.

2017-02-22_115207

Initial Continuous Integration Build Definition

In Visual Studio Team Explorer, click Builds.

2017-02-22_115629

Click New Build Definition.

2017-02-22_115709

This will open the web page and prompt for a definition. This is nice because we're taken directly to our team project builds.

2017-02-22_115934

Choose the Visual Studio template, check the box that says Continuous Integration and accept the defaults. Save the definition, naming it TuringDesktop Suite Build.

If you've been following along, you'll realize that we've never committed any of our source code! Do so now and the build should happen automatically and successfully. Be sure to verify that all the tests ran!

2017-02-22_122709

2017-02-22_122936

Next Up

We have a working application that's automatically built in our CI server. But we've also got some problems:

  • Refactoring for the main application's Console dependency.
  • String literals for settings.
  • Our application's data source is the same in development, CI, and production.
  • No separate integration tests.

In short, we're about to go from nice, sunny sample development to real-world, why-does-this-have-to-be-so-hard programming.

Next Part: TFS Continuous Integration Walk Through Part 5c - Multiple Solutions: Build Settings

TFS Continuous Integration Walk Through Part 5a - Multiple Solutions: Overview

ci-logo This is part of a series of walk throughs exploring CI in TFS, starting from the ground up. The entire series and source code are maintained at this BitBucket repository.
https://bitbucket.org/bladewolf55/tfs-ci-samples

Previous Part: TFS Continuous Integration Walk Through Part 4b - Problems With Traits

Where I'm Headed

For the next walkthroughs, I'm starting with new team projects and sample applications, building a suite of solutions with shared dependencies. My goal is to uncover the various problems that come from:

  • DLL dependencies
  • Project references
  • Web.config and app.config settings
  • Unit tests
  • Integration tests
  • BONUS: Migrating to Git

Along the way, I'm going to completely abuse the TFS team project structure and show how to clean up and organize a repository.

Relationship of Team Projects, Solutions and Build Definitions

Team Projects are intended to manage a software project.
But that doesn't mean there's just one Visual Studio solution for a project. There might be several. For example, our CI Sample projects might end up looking like this:

$/CI Sample (team project)
|_CIConsoleSample
|_CIRestSample
|_CIDocumentation

$/CI Shared (team project)
|_CINuGetSample

What does this mean for automated builds?

  • Maybe the Console now has a dependency on the REST solution, and we need Console to build any time REST changes.
  • Maybe we need the NuGet solution to deploy to a staging location on a successful build, then build dependent solutions pulling this NuGet dll from the staging source.
  • And maybe we don't want the Documentation to build at all.

Important If you're using a Continuous Integration trigger, and any trigger fires, all the solutions in the definition will be rebuilt. For example, let's say the definition builds all solutions in the CI Sample team project above (the default behavior). You have a trigger that says "only build when CIConsole changes." It would still build all three solutions.

This feature of a build definition means that much of the time we'll configure a definition to build just one solution. However, as pointed out above, for a solution that builds an assembly used by multiple projects, you might want to rebuild all the solutions that depend on it, to catch bugs.

As this diagram shows, a team project can include multiple solutions. A build definition can build multiple solutions. A build definition belongs to a single team project, but, it can (with effort) build solutions in other team projects.

team-projects-and-builds

The Projects - A Turing Oracle

My sample suite of applications is a pseudo Turing Test, and is made up of desktop and web apps. The user can ask questions, and get answers that seem to be from a human. I'll start simple, imitating a real-world project progression. In the end, this will be my structure in the TFS repository.

_$Shared
|_$/Magic8Engine

$/TuringDesktop
|_TuringDesktop.sln
  |_TuringConsole.csproj (Dependencies: Magic8Engine, TextColor)
  |_TextColor.csproj

$/TuringWeb
|_TuringWeb.sln
  |_TuringWeb.csproj (Dependencies: TuringSoap)
|_TuringSoap.sln
  |_TuringSoap.csproj (Dependencies: Magic8Engine)

Magic8Engine The Magic8Engine produces the answers, using the eerily accurate Magic 8-Ball Algorithm. It's a shared dll, and lives in a separate folder. It's published as a NuGet package.

TuringDesktop TuringDesktop is a console application for asking questions. The team project has one solution, which itself has two projects.

TuringConsole, the UI, has two dependencies. One is the Magic8Engine dll, the other is on TextColor, which is configured as a project reference.

TextColor is for changing the color of the text. It is only used by TuringConsole.

TuringWeb This is our web interface. The team project has two solutions.

TuringWeb Solution This solution has one project, TuringWeb, which is dependent on the TuringSoap web service.

TuringSoap Solution Our old-fashioned SOAP web service is dependent on the Magic8Engine dll. In the future, the web service could become shared by other applications.

Next Up

In the next part, I'll build my desktop application as if I didn't know anything about the future web application.

Next Part: TFS Continuous Integration Walk Through Part 5b - Multiple Solutions: Simple Project References