I am coming back to an earlier discussion regarding instance and set comparison in SpecFlow. Those who participated in the thread remember that it was about extending SpecFlow.Assist CompareToInstance/CompareToSet methods with possibility to specify different comparison criteria (more about it in
this blog post). The original proposition suggested introducing an enumerator with comparison modes. This caused some objections and recommendation to use LINQ (Gaspar even sketched possible implementation).
I spent some time trying to get syntax easy to use and came up with the following (I have working code, passing tests but am not completely finished). Here are the highlights.
1. In order to compare Table objects with any IEnumerable collection or a single instance we need an adapter class (TableRowOrInstance in the mail from Gaspar).
2. The adapter class should take into account the actual number of columns specified in the Table object. This is needed to prevent comparison from failure because of the fields not included in the Table object.
3. The implementation should have fluent style, so the client code can be written in a single line.
4. It should support both collection and instance comparison.
Before looking at proposed implementation let's see what it should cover. Imagine we have a table with two columns and a single row:
var table = new Table("StringProperty", "IntProperty");
table.AddRow("b", "2");
Now we are fetching result from querying Entity Framework database (from a table called "ComparisonTable"). We can only compare them if we project query results to a new (anonymous) class containing StringProperty and IntProperty:
var queryResult = from q in _entities.ComparisonTable select new { q.StringProperty, q.IntProperty };
var set = table.CreateSet<ComparisonTable>().Select(x => new { x.StringProperty, x.IntProperty });
Assert.IsTrue(set.Except(queryResult).Count() == 0);
You got the idea. Now this is how it may look using new extension classes:
var table = new Table("StringProperty", "IntProperty");
table.AddRow("b", "2");
var query = from q in _entities.ComparisonTable select q;
Assert.IsTrue(
table.AsProjection<ComparisonTable>()
.Except(query.AsProjection())
.Count() == 0);
The essential part here is AsProjection extension method. Actually there are two:
static class Extensions
{
public static IEnumerable<Projection<T>> AsProjection<T>(this IEnumerable<T> source, Table table = null)
{
return new EnumerableProjection<T>(table, source);
}
public static IEnumerable<Projection<T>> AsProjection<T>(this Table table)
{
return new EnumerableProjection<T>(table);
}
}
EnumerableProjection is a wrapper class that encapsulates Table and collection objects and keep track of properties selected for comparison. Here are more examples, now with generic Lists instead of EF objects:
[Test]
public void SetTest()
{
Person[] persons = new Person[] { new Person() { FirstName = "Bill", LastName = "Gates" } };
People[] people = new People[] { new People() { FirstName = "Paul", LastName = "Allen" }, new People() { FirstName = "Bill", LastName = "Gates" } };
Table table = new Table("FirstName", "LastName");
table.AddRow("Bill", "Gates");
Assert.IsTrue(table.AsProjection<Person>().Intersect(persons.AsProjection(table)).Count() == 1);
table.AddRow("Paul", "Allen");
Assert.IsTrue(table.AsProjection<People>().Intersect(people.AsProjection()).Count() == 2);
Assert.IsTrue(table.AsProjection<People>().Except(people.AsProjection()).Count() == 0);
Assert.IsFalse(table.AsProjection<People>().SequenceEqual(people.AsProjection()));
}
[Test]
public void InstanceTest()
{
Person person = new Person() { FirstName = "Bill", LastName = "Gates" };
People people = new People() { FirstName = "Paul", LastName = "Allen" };
Table table = new Table("FirstName", "LastName");
table.AddRow("Bill", "Gates");
Assert.AreEqual(table.AsProjection<Person>(), person);
Assert.AreNotEqual(table.AsProjection<People>(), people);
}
Internally the implementation uses FillSet from SpecFlow.Assist, but it does not use any of set/instance comparison methods (and in fact replaces them using with standard LINQ operations).
When it comes to naming, I feel use of word projection is justified: "table as projection of people" really means what it is: take a collection o People objects and project it (as using Where clause) onto a subset of columns specified by the table.
What do you think? If you think it's worth it, I can clean up the code and prepare a pull request. The implementation does not change anything in Table of SpecFlow.Assist classes, so it can be submitted independently (although it duplicates CompareToSet functionality). I also think that this is more generic approach that what I earlier suggested with use of enumeration type.
Vagif