Monday, January 9, 2012

Simple Circles–Fake Repository (Part 3)

This post is part of a series on building a simple to use web-based contact and customer relationship management application. The goal is to support various audiences including businesses, teams, clubs, religious organizations, etc. With a basic structure for the domain objects taking shape it’s time to move on to the notion of persistence.

In the last post I set up the interfaces to support the Repository pattern and left off with the following signature:

namespace Circles.Persistence
{
using System;
using System.Linq;

public interface IPartyRepository : IRepository<Party, Guid>, ISupportSave<Party, Guid>, ISupportDelete<Party, Guid>
{
IQueryable<Household> FindAllHouseholds();
IQueryable<Organization> FindAllOrganizations();
IQueryable<Person> FindAllPersons();
}

Now I’m going to put together a simple fake repository implementation to exercise the interfaces as well as quickly provide sample data to work with. I’ve added a new project called FakeRepository containing a single class called FakePartyRepository partly shown here:

namespace Circles.FakeRepository
{
using System;
using System.Collections.Generic;
using System.Linq;

public class FakePartyRepository : IPartyRepository
{
private readonly List<Household> fakeHouseholdList = new List<Household>
{
new Household { PartyName = "Churchill", FormalGreeting = "Sir Winston and Lady Clementine" },
new Household { PartyName = "Roosevelt", FormalGreeting = "President Franklin and Mrs. Eleanor" },
};

private readonly List<Organization> fakeOrganizationList = new List<Organization>
{
new Organization { PartyName = "Great Lakes Food Market" },
new Organization { PartyName = "Hungry Coyote Import Store" },
new Organization { PartyName = "Lazy K Kountry Store" },
};

private readonly List<Person> fakePersonList = new List<Person>
{
new Person { FirstName = "Nancy", LastName = "Davolio", Gender = GenderEnum.Female, Birthday = new DateTime(1948, 12, 8) },
new Person { FirstName = "Andrew", LastName = "Fuller", Gender = GenderEnum.Male, Birthday = new DateTime(1992, 8, 14) },
};

private readonly IQueryable<Household> fakeHouseholdRepository;
private readonly IQueryable<Organization> fakeOrganizationRepository;
private readonly IQueryable<Person> fakePersonRepository;
private readonly IQueryable<Party> fakePartyRepository;

public FakePartyRepository()
{
this.fakeHouseholdRepository = this.fakeHouseholdList.AsQueryable();
this.fakeOrganizationRepository = this.fakeOrganizationList.AsQueryable();
this.fakePersonRepository = this.fakePersonList.AsQueryable();
this.fakePartyRepository = this.fakeOrganizationList.Union(this.fakePersonList).Union(this.fakeHouseholdList).AsQueryable();
}

public IQueryable<Party> FindAll()
{
return this.fakePartyRepository;
}

public Party FindById(Guid id)
{
return this.fakePartyRepository.FirstOrDefault(x => x.PartyId == id);
}
}

As you can see, the underlying data is contained in simple in-memory generic Lists that are wrapped into LINQ IQueryable objects. Note the last line of the constructor where the fakePartyRepository is a .Union of the three underlying lists.


Simple Circles - xunitTo test the FakeRepository let’s add another class library project called UnitTests. We’ll use NuGet to add the xUnit package with the command ‘install-package xunit’ as shown to the right. xUnit was created by James Newkirk (creator of the venerable NUnit) and Brad Wilson to better reflect the purpose of driving and iterating the design of an implementation at the unit level. Brad discusses this philosophy in his article Its not TDD, It’s Design by Example. After adding the xUnit reference, add a class called PartyRepositoryTest:

  public class PartyRepositoryTest
{
[Fact]
public void CanAddHousehold()
{
// arrange
IPartyRepository repository = new FakePartyRepository();

// act
var id = repository.Add(new Household
{
PartyName = "Eisenhower",
FormalGreeting = "President Dwight D. and Mrs. Mamie",
InformalGreeting = "Ike and Mamie"
});

// assert
Assert.NotNull(id);
Assert.NotNull(repository.FindById(id));
}
}

The first thing you may notice is that xUnit uses the attribute [Fact] rather than [Test] as other TDD frameworks do. As Brad puts it “a [Fact] is an expression of some condition which is invariant”. Essentially we’re saying that the condition “CanAddHousehold” is an invariant condition of the PartyRepository which must always be true. In addition to expressing facts, xUnit lets you express theories which are “an expression of a condition which is only necessarily true for the given set of data.”:

  [Theory,
InlineData("D28AA45E-C650-4121-8070-3D12BE31F91A")]
public void CanFindSinglePerson(string id)
{
// arrange
IPartyRepository repository = new FakePartyRepository();
var partyId = new Guid(id);

// act
var person = repository.FindById(partyId) as Person;

// assert
Assert.NotNull(person);
Assert.True(person.Id == partyId);
}

Simple Circles - Unit TestsSince we built the FakeRepository with our known data, we can construct tests with known values to prove the repository is working as designed without defects. In the above example, we know the PartyId of one of the person instances that we put into the fake repository so we can attempt to retrieve that person to exercise that logic. ReSharper’s test runner shows all the tests we currently have. Before the tests can be compiled and run, you must add a reference to the Extensions subproject of xUnit which holds the [Theory] attribute definition and behavior. Once again, use the NuGet Package Manager Console to execute the command “install-package xunit.extensions”.


Finally, you’ll notice in the above samples as well as the source code accompanying this post that I’ve followed the “3A” or “Arrange-Act-Assert” principle for writing the tests.


The source code for this article can be downloaded from here.


Next we’ll look at starting an ASP.NET MVC 3 website to consume and present the domain model.

Wednesday, January 4, 2012

Simple Circles–Persistence (Part 2)

This post is part of a series on building a simple to use web-based contact and customer relationship management application. The goal is to support various audiences including businesses, teams, clubs, religious organizations, etc. With a basic structure for the domain objects taking shape it’s time to move on to the notion of persistence.
One of the tenets of DDD is known as “aggregate root” – a top-level, course grained collection of responsibilities. In plain object-oriented systems, objects tend to expose granular methods to operate on their data. Unfortunately, this approach allows business knowledge and intimate design details to creep outside of the model. That is, accomplishing a business operation usually entails making several method calls across several objects – this sequencing of calls becomes embedded externally in calling applications making the whole thing tightly coupled and brittle. The aggregate root principle groups related objects closely and provides consistent and coordinated access to these related objects.Vaughn Vernon wrote an in depth three-part article on the topic of Effective Aggregate Design.
In this model the Party is an obvious root object – we don’t access addresses or notes without first going through the Party instance they belong to. This approach leads to methods like Party.AddAddress() or Party.AddNote() rather than creating addresses and notes separately.
Once the aggregate root(s) have been identified it’s time to move on to persisting the data they contain. The Repository pattern is a proven way to implement persistence in a storage-neutral manner. There are many references to consult for more information but in the context of DDD, Jak Charlton’s post, DDD: The Repository Pattern, is particularly concise and a good starting point for why it is useful. Gabriel Schenker provides another good write up with code examples on the NHibernate FAQ. Given that we have a Party aggregate root, we should also have a PartyRepository since each aggregate should be responsible for its own “domain” or “scope” if you will.
Since repositories manage aggregate roots and their associated entities/ values, the basic semantics for the repository follow that of a generic collection. For us this means something like:
namespace Circles.Persistence
{
  using System;
  using System.Collections.Generic;

  public interface IPartyRepository
  {
    IList<Party> FindAllParties();
    Party FindPartyById(Guid Id);
    Guid Add(Party party);
    void Delete(Guid Id);
    void Update(Party party);
  }
}

Some might balk at the Delete and Update methods, arguing that pure collections have “Remove” instead. However, I am of the opinion that we’re working with domain (a.k.a. business) entities that are persisted somewhere so the semantics of persisting data are more natural with delete and update.

Of course the above approach is very "party-specific” so it could be refactored using generics like this:
namespace Circles.Persistence
{
  using System;
  using System.Collections.Generic;

  public interface IRepository<T> where T : Entity
  {
    IList<T> FindAll();
    T FindById(Guid Id);
    Guid Add(T entity);
    void Delete(Guid Id);
    void Update(T entity);
  }
}

Now we’ve got an interface that supports any type of domain entity – note the removal of the term “Party” from the method names.

The above refactored interface implies that every repository created supports all four “CRUD” operations.  A better approach would be to split out the operations and then have a specific repository implement the ones it needs like this:
namespace Circles.Persistence
{
  using System;
  using System.Linq;

  public interface IRepository<T, in TId> where T : Entity
  {
    T this[TId id] { get; set; }
    IQueryable<T> FindAll();
    T FindById(TId Id);
  }

  public interface ISupportSave<in T, out TId> where T : Entity
  {
    TId Add(T entity);
    void Update(T entity);
  }

  public interface ISupportDelete<T, in TId> where T : Entity
  {
    void Delete(TId Id);
  }

  public interface IPartyRepository : IRepository<Party, Guid>, ISupportSave<Party, Guid>, ISupportDelete<Party, Guid>
  {
    IQueryable<Household> FindAllHouseholds();
    IQueryable<Organization> FindAllOrganizations();
    IQueryable<Person> FindAllPersons();
  }

Now we’re explicit about what the PartyRepository does. By implementing IRepository, ISupportSave and ISupportDelete we’re clear that the IPartyRepository supports full CRUD behavior using Guids for the Id types. Additionally, we provide FindAllxxx methods for retrieving specific types from the repository. Notice that having the repository return IQueryable gives flexibility on both sides – concrete implementations can translate the LINQ query passing it to the underlying data store for execution and callers can query the repository in various ways rather than being forced to go through a rigid, narrowly defined API we provide.

One more thing to cover – I slipped in a new Type called Entity that is a base class for all domain entities. Rather than rehashing all the details here I’d suggest checking out Jason Dentler’s blog or his book NHibernate 3.0 Cookbook. Equally important is Billy McCafferty’s work on Sharp Architecture. Standing on the shoulder’s of giants…

The source code for this article can be downloaded from here.

Next we’ll look at testing what we have xUnit.

Tuesday, January 3, 2012

Simple Circles–Domain Model (Part 1)

This post is part of a series on building a simple to use web-based contact and customer relationship management application. The goal is to support various audiences including businesses, teams, clubs, religious organizations, etc. Thus, it is important that the domain model be flexible enough to allow various combinations of entities to be used to support these use cases. For example, in a B2B scenario the notion of a person or a household will not likely be center stage whereas businesses will be. However, a club or sports team will likely interact with individuals and possibly households but not likely with businesses.

Models

Simple Circles - Party ConceptModeling entities such as Person, Household, and Organization come to mind immediately. However, the “Party” model has existed for many years as a way to generically model the relationships between various parties. The figure to the right illustrates graphically this concept. Person, Household, and Organization are all types of parties. This approach also allows reuse of relationships such as notes, postal addresses, and communication channels across all types of parties. Rather that duplicating logic, storage mechanisms and business rules for each entity this approach follows the Don’t Repeat Yourself (DRY) principle.

Simple Circles - Party Class DiagramTransferring the conceptual model to a physical one is accomplished using inheritance as shown in the class diagram to the right. The Party class is an abstract base class containing a PartyId unique identifier and a PartyName. Person, Household, and Organization all inherit these base properties supplementing them with specific properties they contain. Notice that the base class contains relationships to Party Note, Postal Address and Channel Address as well as to Party Type. Through inheritance all child subtypes also inherit these relationships without having to explicitly code them three different times (DRY!). Using a separate Party Type allows classifying individual party instances which supports filtering the parties in lists, views, etc.

Classes

Here is the code for the Party class which realizes the class diagram shown above:
namespace Circles.DomainModel
{
  using System;
  using System.Collections.Generic;

  /// <summary>
  /// An abstract class representing any 'Party' that can be interacted with.
  /// </summary>
  public abstract class Party
  {
    private ISet<ChannelAddress> channelAddresses = new HashSet<ChannelAddress>();
    private ISet<PartyNote> notes = new HashSet<PartyNote>();
    private ISet<PostalAddress> postalAddresses = new HashSet<PostalAddress>();

    /// <summary>
    /// Gets or sets the unique identifier of the Party instance.
    /// </summary>
    public Guid PartyId { get; set; }

    /// <summary>
    /// Gets or sets the name of the Party instance.
    /// </summary>
    public virtual string PartyName { get; set; }

    /// <summary>
    /// Gets or sets the PartyType of the Party instance.
    /// </summary>
    public virtual PartyType PartyType { get; set; }

    /// <summary>
    /// Gets or sets the Notes associated with the Party instance.
    /// </summary>
    public ISet<ChannelAddress> ChannelAddresses
    {
      get { return this.channelAddresses; }
      set { this.channelAddresses = value; }
    }

    /// <summary>
    /// Gets or sets the Notes associated with the Party instance.
    /// </summary>
    public ISet<PartyNote> Notes
    {
      get { return this.notes; }
      set { this.notes = value; }
    }

    /// <summary>
    /// Gets or sets the Notes associated with the Party instance.
    /// </summary>
    public ISet<PostalAddress> PostalAddresses
    {
      get { return this.postalAddresses; }
      set { this.postalAddresses = value; }
    }
  }
}

Note that this is not the “finished product” but rather an early version along the way. Future articles will refine this further.

The source code for this article can be downloaded from here.

Next we’ll look at persistence of this model.

Sunday, January 1, 2012

Simple Circles–Introduction

This series will be a step-by-step approach on building a simple to use web-based contact and customer relationship management application. The goals are to employ current best practices including domain-driven design (DDD), object-relational mapping (ORM), unit testing – a component of Test-Driven Development (TDD), and the model-view-controller paradigm of ASP.NET MVC3. Additionally, as the name suggests – the finished product must be simple to use. While patterns, practices and techniques employed to build the application might be advanced the end user mustn't be overwhelmed. Keeping track of a circle of friends, acquaintances and associates should be simple!
There are numerous examples of using these practices and technologies around the Internet. However many are overly simplified – designed to show a particular technique – rather than a complete “real world” example. Technical books tend to stick to the Microsoft mantra of always using their latest offering or their shiny new version of some proven OSS product. For example, Steve Sanderson’s Sports Store in Pro ASP.NET MVC 3 Framework and Tim McCarthy’s SmartCA in .NET Domain-Drive Design with C# both deliver complete examples yet neither tackle NHibernate. That is, true persistence ignorance beyond the theory that its possible. Don’t misunderstand, I’m not saying they’re bad books – I’ve bought both, used them to learn, and do refer back to them. However, if I see another contrived example of using Entity Framework with a few wizards to whip out a solution I’ll scream. Smile
This application will be built with NET 4.0 using the following tools and technologies:
  1. Visual Studio 2010 with NuGet package support
  2. ASP.NET MVC 3 and Razor view engine
  3. Persistence layer will be:
    1. NHibernate 3.1 with SQLite
    2. Entity Framework 4.1 with SQL Server CE 4.0
  4. Ninject 2.2 for dependency injection
  5. xUnit for unit testing
  6. jQuery UI for user experience.
Here are links to the posts for this project:
  1. Domain Model
  2. Persistence
  3. Fake Repository
  4. ASP.NET MVC 3 UI
    1. Scaffold People CRUD
    2. Replace EF hard wiring with Repository pattern
  5. MVC Controller Unit Tests
  6. Enabling jQuery UI
  7. NHibernate Repository
    1. IDbSession and implementation
    2. Fluent NHibernate mapping
    3. NHibernate Unit Testing
As this series is developed, I’ll keep updating this post with new links.

Friday, April 15, 2011

First Foray into Windows Home Server (WHS)

I just inherited an HP EX485 MediaSmart Server in near new condition. First impression is that it is *tiny* — about as tall as a college textbook standing on edge and six inches wide. WHS machines are network-ready, do not have a VGA output nor display adapter and typically have multiple drive bays designed for adding low cost serial ATA (SATA) drives as storage needs grow. system_diskThe EX485 comes with a single 750 GB drive partitioned with a small 20 GB system (“C:\”) and the remainder as a large “D:\” data drive.The specifications state that fully loaded with four drives it will consume a paltry 76 watts of power!

WHS is an interesting, strange brew – a fully-functioning Windows server product with an easy to use setup and interface sold only to OEMs. The intention is to provide something dead-simple to setup that consumers can take home, plug-in and it just works. Scott Hanselman wrote a nice introductory review of WHS and the MediaSmart here. As the name suggests, the EX485 is designed to store and serve media – movies, music and photos.

whs_consoleUnder the covers WHS v1 is Windows Server 2003 so it’s a solid and reliable server platform. It exposes a user-friendly console that OEMs can extend to provide functionality as shown here. Notice the small link indicated in the upper right that opens a settings panel where you can configure the server – again the dialog is extensible by OEMs.

whs_upnpWHS supports universal plug and play (UPnP) so other machines in your home network can discover and access it. From there you can install a client “connector” that gives you access to the console shown above. Connecting a client machine is sort of like an ultra-light version of joining a domain without the intrusive “taking over”.

Client computers will get a “Shared Folders” link on their desktop that shows all the pre-configured shares (e.g. Documents, Music, Photos, Videos) on the server as well as any custom ones you’ve added. A “Windows Home Server Connector Service” will be installed along with a system tray icon and nightly backups will be configured.

ex485_serverWindows Home Server 2011 (a.k.a. v2) was released a couple of weeks ago. While articles are popping up that show how to wipe the existing v1 and install the shiny new toy I’m going to stay put. I’m currently running HP MediaSmart Server v2.5.15.35297 which was released in April 2009. The system shipped with this configuration and it’s got the latest patches and updates. I expect to be running this until the hardware fails or becomes so outdated that it becomes compelling to update to a newer platform.

Friday, December 24, 2010

How To Build an ASP.NET 4.0 Web Service and Consume It With Excel 2007-Part 2

With a freshly built web service we can now have a bit of fun.
  1. Create a new project utilizing the Visual Studio Tools for Office template for Excel 2007 Workbook:
    2010-12-05_091921
  2. Accept the default settings for Create a new document and click OK:
    2010-12-05_091937
    Note: if you encounter errors while creating a new project it is most likely because the VSTO add-ins are not configured until first used.
    2010-12-05_091947
    The quickest way to resolve the issue is to exit out of Visual Studio and use the right-click Run as administrator trick to launch with elevated permissions.
  3. From the Solution Explorer, right-click on the References folder then choose Add Service Reference:
    2010-12-05_092357
  4. Type in the web address of the web service we just created:
    2010-12-05_092441
    Hint: You may want to switch (Alt-Tab) over to the browser window where we tested the web service against IIS after deploying then copy the address from the address bar.
  5. After pressing Go the deployed web service will be queried and the results displayed for confirmation:
    2010-12-05_092507
  6. With the Excel Workbook Designer displayed from within Visual Studio right-click anywhere on the worksheet and choose View Code:
    2010-12-05_092756
  7. Create a new method called DisplayCustomer containing the following code:
    public void DisplayCustomer(int customerKey)
    {
    // Call the Web service.
    using (var service = new
    Assignment3Service.WebServiceSoapClient())
    {
    var result = service.GetCustomerByKey(customerKey);
    if (result != null)
    {
    var items = result.Tables["Customer"]
    .Rows[0].ItemArray;
    var limit = items.GetLength(0);
    for (int i = 0; i < limit; i++)
    {
    var item = items[i];
    this.Cells[1, i + 1] = item;
    }
    }
    }
    }

  8. Right-click on the Excel Workbook project and choose Add New Item:
    2010-12-05_093805
  9. Select Office under the Visual C# Installed Templates then choose Ribbon (Visual Designer) and click Add:
    2010-12-05_093910
  10. From the Toolbox drag an EditBox control then a Button control onto the designer surface:
    2010-12-05_094125
    Resulting in the following:
    2010-12-05_094212
  11. Clean up the Labels to make the Ribbon more meaningful and useful:
    2010-12-05_094313
    2010-12-05_094332
  12. The completed Ribbon should look similar to this:
    2010-12-05_094451
  13. Double-click on the Search button (text) on the Ribbon to jump to the event-handler section. Put the following code to call the add-in method DisplayCustomer previously created:
    private void btnSearch_Click(object sender, RibbonControlEventArgs e)
    {
    Globals.Sheet1.DisplayCustomer(int.Parse(CustomerKeyBox.Text));
    }

  14. Notice as you start typing that Intellisense will provide filtered statement completion:
    2010-12-05_094538
  15. Here is the completed method:
    2010-12-05_094721
    Pay attention to the name of the control (shown as “CustomerKeyBox” above) – this name must match the name you gave to the EditBox control immediately preceding this step.
  16. Now press F5 to debug the solution. Doing so will launch Excel 2007 (or Excel 2010) and you’ll notice the Add-Ins tab of the Ribbon UI displays your custom fields:
    2010-12-05_094814
  17. Simply type in a customer number then click Search, the results should look something like this:
    2010-12-05_110725
At this point we have used a custom add-in running on a client desktop to invoke a web service published and deployed in IIS which in turn connects to a SQL database to execute a query using the data supplied by the client (customer key value) and the results are returned and displayed in Excel.

Wednesday, December 22, 2010

How To Build an ASP.NET 4.0 Web Service and Consume It With Excel 2007-Part 1

This short series builds upon the previous How to Build a 2-Tier ASP.NET 4.0 Web Site (Parts 1, 2, 3). If you’re jumping in here you’ll need to adjust accordingly.
  1. Right-click on the web project in Solution Explorer, choose Add New Item:
    2010-12-04_221654
  2. Ensure that Visual C# is chosen under Installed Templates on the left, select Web Service:
    2010-12-04_221753
  3. Replace the default boilerplate code show here:
    2010-12-04_221902
    With the following code (begin replacement on line 10 above):
    [WebService(Namespace = "http://pragmatic/")]
    [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
    public class WebService : 
          System.Web.Services.WebService {
    [WebMethod]
    public DataSet GetCustomerByKey(int CustomerKey)
    {
      DataSet resultSet = new DataSet();
      SqlConnection conn = new SqlConnection(
      ConfigurationManager.ConnectionStrings["AdventureWorksDW2008R2ConnectionString"]
         .ConnectionString);
      using (SqlCommand cmd = new SqlCommand(
    @"SELECT [LastName], [FirstName], [MiddleName], [BirthDate], [EmailAddress], [Phone] 
    FROM [DimCustomer] 
    WHERE ([CustomerKey] = @CustomerKey)"))
      {
      cmd.Parameters.AddWithValue(
                "@CustomerKey", CustomerKey);
      conn.Open();
      cmd.Connection = conn;
      SqlDataAdapter dataAdapter = 
            new SqlDataAdapter(cmd);
      dataAdapter.Fill(resultSet, "Customer");
      }
      return resultSet;
    }
  4. Here is the resultant web service code-behind file:
    2010-12-04_223404
  5. Explanation of above lines:
    10: Replace default namespace with meaningful one.
    12: Define a web method called GetCustomerByKey which accepts an integer assigned to the variable “CustomerKey” and returns an ADO.NET DataSet object.
    17: Declare an ADO.NET DataSet that will be populated with the results of the Sql query.
    19-20: Declare an ADO.NET SqlConnection used to connect to the Sql database *AND* initialize the connection string using the same value previously stored in the configuration file from Part 3. Note that your connection string value (shown here as “AdventureWorksDW2008R2ConnectionString” may be different. Open web.config file to obtain the actual value to use from the “name=” attribute:
    2010-12-05_114702
    22-24: Construct a SqlCommand using the same SELECT statement previously used in Part 3. Note the “@CustomerKey” parameter token.
    26: Add an entry for the @CustomerKey parameter token and set its value to be the variable that is passed into the web method.
    27: Open the connection to the database.
    28: Associate the open connection to the SqlCommand object so the command will use it.
    29: Declare an ADO.NET DataAdapter and associate it to the SqlCommand object. This “magic” object will take care of a TON of work executing the Sql statement, taking the query results returned from the database and filling the collection of .NET DataSet objects (tables, rows, columns, values) with the result of the query.
    30: Simple little statement calling the Fill method of the DataAdapter, giving it the command to use and the dataset to be filled. This is the truly “magic” part of high-level object oriented programming.
    32: Return the populated dataset to the caller of this method.
  6. To test the web service, simply press “F5” or click the green arrowhead on the toolbar next to “Debug”. When the web browser comes up, type the name of the web service file in the address bar and hit enter:
    2010-12-04_224423
    Remember that your port number may be different than the :1483 shown above – that’s okay.
  7. You should see the sample web service test page shown here:
    2010-12-04_224441
  8. Click the “GetCustomerByKey” hyperlink and you’ll see the test page for the web method:
    2010-12-04_224514
  9. Input a value, such as 11000 and click invoke to see the results of the web method:
    2010-12-04_225618
  10. You can re-publish the website as shown in the first few steps of Part 3 – no need to go through all the one-time configuration steps however. Verify the web service is published by opening a browser and typing in the correct address:
    2010-12-04_232205
Next time we’ll use Excel to quickly test consuming the published web service with very little effort.