Using LINQ to write constraints in OCL style v3

In a comment to my last post, Keith Short challenged me to write a version of this constraint with good error reporting, i.e. to post an error to the task list that gives the names of the duplicate properties, and supports navigation to the element that is in error.  After a bit of head-scratching, I decided I can't do this easily using the built-in functions (it turns out that the Except() function will not subtract a set from a bag leaving the duplicates).  But, using extension methods, I can build my own supporting functions.  This is what I ended up with.

[ValidationMethod(ValidationCategories.Menu | ValidationCategories.Save)]

private void TestExampleElement(ValidationContext context)

{

  var propnames = this.Properties.Select( p => p.Name);

  var duplicateProps = propnames.Duplicates();

  if (duplicateProps.Count() > 0)

  {

string errors = duplicateProps.CommaSeparatedList();

      context.LogError(string.Format("Non-unique property names: {0}", errors), "Error 1", this);

  }

  var subpropnames = this.Properties.SelectMany(p => p.SubProperties).Select( p => p.Name);

  var duplicateSubProps = subpropnames.Duplicates();

  if (duplicateSubProps.Count() > 0)

  {

      string suberrors = duplicateSubProps.CommaSeparatedList();

      context.LogError(String.Format("Non-unique sub property names: {0}", suberrors), "Error 2", this);

  }

}

public static class C

{

  public static IEnumerable<T> Duplicates<T>(this IEnumerable<T> source)

  {

      return source.Aggregate(Enumerable.Empty<T>(),

           (agg, p) => ( source.Count(x => x.Equals(p)) > 1 ?

                                 agg.Union(new T[1] { p } ) : agg ) );

  }

  public static string CommaSeparatedList(this IEnumerable<string> source)

  {

    return source.Aggregate(String.Empty,

                   (agg, s) => String.IsNullOrEmpty(agg) ? s : (agg + ", " + s));

  }

}

The  Duplicates function goes through the source collection, tests whether each item appears in the source more than once, and if so puts it into the result collection.  The errors put into the task list contain a comma-separated list of the duplicated names.  The third parameter to LogError is the model element that carries the error; double-clicking on the error navigates to that element.