When I try to annotate class, property and method, and then try to retrieve annotated node, only the class one is returned. Why?
Here is the code that annotates
SyntaxAnnotation propertyAnnotation = null;
SyntaxAnnotation classAnnotation = null;
SyntaxAnnotation setMethodAnnotation = null;
document = document
.AnnotateClass(classDeclaration, out classAnnotation)
.AnnotateProperty(propertyDeclaration, out propertyAnnotation)
.AnnotateSetMethod(setMethodDeclaration, out setMethodAnnotation);
I have these extension methods on IDocument defined
internal static IDocument AnnotateSetMethod(this IDocument document, MethodDeclarationSyntax method,
out SyntaxAnnotation annotation)
{
annotation = new SyntaxAnnotation();
var newRoot = document.GetSyntaxRoot()
.ReplaceNode(method, method.WithAdditionalAnnotations(annotation));
return document.UpdateSyntaxRoot(newRoot);
}
internal static IDocument AnnotateProperty(this IDocument document, PropertyDeclarationSyntax property,
out SyntaxAnnotation annotation)
{
annotation = new SyntaxAnnotation();
var newRoot = document.GetSyntaxRoot()
.ReplaceNode(property, property.WithAdditionalAnnotations(annotation));
return document.UpdateSyntaxRoot(newRoot);
}
internal static IDocument AnnotateClass(this IDocument document, ClassDeclarationSyntax classDeclaration,
out
SyntaxAnnotation annotation)
{
annotation = new SyntaxAnnotation();
var newRoot = document.GetSyntaxRoot()
.ReplaceNode(classDeclaration, classDeclaration.WithAdditionalAnnotations(annotation));
return document.UpdateSyntaxRoot(newRoot);
}
public static TSyntaxNode GetAnnotatedNode<TSyntaxNode>(this IDocument document, SyntaxAnnotation annotation)
where TSyntaxNode : CommonSyntaxNode
{
return document.GetSyntaxRoot().GetAnnotatedNode<TSyntaxNode>(annotation);
}
And if I do
var propertyDeclaration = document.GetAnnotatedNode<PropertyDeclarationSyntax>(propertyAnnotation);
I get an error, but if I try with ClassDeclarationSyntax it works fine.
My crystal ball is telling me that the .Replace node calls in all but your AnnotateClass are failing. See if the new roots you get back are the exact same object as the old roots.
This is because once you've added an annotation to the class, you now have a new tree, and so the property syntax you have is no longer "in" that tree -- it's a new node. This is the way of immutability -- once a new node is created somewhere, all the nodes in the tree are effectively new since you can get to any node from any other node. (It's because of this problem that we added syntax annotations in the first place....otherwise you'd make a few nodes and have no way to get back to them.)
You have a few ways to approach this:
Use a SyntaxRewriter where you do the rewrite all in one stage. You override VisitClass, VisitProperty, etc and produce new nodes with annotations all at once.
Rather than calling ReplaceNode, call ReplaceNodes, where you replace all three nodes at once.
In either case, doing a single rewrite is always preferable to a bunch of rewrites for performance reasons. Like I said, once you replace a node in a tree and get back a new root, all your instances have changed and may have to be re-created. This is expensive and produces memory pressure.
[Technically, that statement is a lie: we do build stuff lazily and reuse lots stuff. But the less rewrites the better.]
Related
I have the following setup :
public class CustomAttribute : Attribute
{
[...]
public CustomAttribute(Type type)
{
[...]
}
}
[Custom(typeof(Class2))]
public class Class1
{
public void M1(Class2) {}
public void M2(Class2) {}
}
public partial class Class2
{
[...]
}
What I am trying to achieve using the new Code Generation mechanism added in .NET 5 is at compile-time, find every class in the project referencing my generator being annotated by the Custom attribute, and then, create a partial class for the type in its constructor containing methods having the same name and parameters (It won't be the same parameters, it's just to simplify a bit).
Before, I was planning to use TTs to generate the partial file but creating one per type was announcing itself to be both tedious and hard to maintain.
Thing is...
I'm a little lost.
What I did manage to do:
Create a generator, making sure it is called at generation and the code it generates is usable (~ a hello world version)
Find my attribute symbol in the compilation context (not sure I'll need it, but I found it)
Found a way to identify the classes being annotated by my attribute by relying on the syntax trees present in the compilation context.
Now, though I don't know how to proceed further, the syntax tree has at the same level the identifier nodes for my attribute and the class being used as a parameter, meaning if I ever use another attribute, I fear they will all get at the same level (might use the order getting the position of the identifier for my attribute and then getting the next one).
But then even if we omit that... How can I list all methods and their parameters for a given class that I have the name of? Reflection is obviously out of the picture since the assembly is not loaded.
I only found Rosly examples, based on using the solution or Analyzers who don't really have the same type of objects available, and thus the proposed solutions are not applicable. And I'm not sure starting another Roslyn analysis on single files is really the way it is supposed to be done.
Please keep in mind that this is my first foray with the Semantic/Syntaxic API.
Preparatory work: Introduction to C# source generators
This will guide you towards settings up a code generator project. As far as I understood, tooling is on the way to automate this part.
TL;DR there will be the full ExecuteMethod at the end of this answer
Filtering out the syntaxic trees containing no classes decorated with an attribute
This is our first step, we only want to work with the classes that are decorated by an attribute, we'll then make sure it's the one that interests us. This also has the secondary benefit of filtering out any source files that do not contain classes (think AssemblyInfo.cs)
In the Execute method of our new Generator, we will be able to use Linq to filter out the trees:
var treesWithlassWithAttributes = context.Compilation.SyntaxTrees.Where(st => st.GetRoot().DescendantNodes().OfType<ClassDeclarationSyntax>()
.Any(p => p.DescendantNodes().OfType<AttributeSyntax>().Any()));
We will then be able to loop on our syntaxic trees (from what I could see, one syntaxic tree corresponds roughly to one file)
Filter-out classes not being annotated with an attribute
The next step is to make sure that in our current syntaxic tree, we only work on classes being decorated by an attribute (for the cases where several classes are declared in one file).
var declaredClass = tree
.GetRoot()
.DescendantNodes()
.OfType<ClassDeclarationSyntax>()
.Where(cd => cd.DescendantNodes().OfType<AttributeSyntax>().Any())
This is all pretty similar to the previous step, tree being one of the items we got in our treesWithClassWithAttributes collection.
Once again, we'll loop on that collection.
Filter-out classes that are not annotated with our specific attribute
Now, that we are working on single classes, we can dive in and check if any of the attributes are the one we are looking for. This will also be the first time we will need the semantic API, since the attribute identifier is not it's class name (PropertyAttribute, will be used as [Property] for example), and the semantic API, allows us to find the original class name without us having to guess.
We will first need to initialize our semantic model (this should placed in our top level loop):
var semanticModel = context.Compilation.GetSemanticModel(tree);
Once initialized, we get to our searching :
var nodes = declaredClass
.DescendantNodes()
.OfType<AttributeSyntax>()
.FirstOrDefault(a => a.DescendantTokens().Any(dt => dt.IsKind(SyntaxKind.IdentifierToken) && semanticModel.GetTypeInfo(dt.Parent).Type.Name == attributeSymbol.Name))
?.DescendantTokens()
?.Where(dt => dt.IsKind(SyntaxKind.IdentifierToken))
?.ToList();
Note: attributeSymbol is a variable holding the Type of the attribute I am searching for
What we are doing here, is for each syntaxic node related to our class declaration, we only look at the ones describing an attribute declaration.
We then take the first one (my attribute can only be placed once on a class) that has an IdentifierToken for which the parent node Is of the type of my attribute (the semantic API does NOT return a Type hence the name Comparison).
For the next steps, I will need the IdentifiersToken, so we'll use the Elvis operator to get them if we found our attribute, we'll get a null result otherwise which will allow us to get to the next iteration of our loop.
Get the class type used as my Attribute parameter
This is where it gets really specific to my use case, but it's part of the question, so I'll cover it anyway.
What we got at the end of the last step was a list of Identifier Tokens, which mean, we will have only two for my attribute : The first one Identifying the attribute itself, and the second one identifying the class I want to get the name of.
We will be using the semantic API again, this allows me to avoid looking in all the syntax trees to find the class we identified :
var relatedClass = semanticModel.GetTypeInfo(nodes.Last().Parent);
This gives us an object similar to the ones we were manipulating until now.
This is a good point to start generating our new class file (so a new stringbuilder, with all the test needed to have a partial class in the same namespace the other one was, it will always be the same in my case, so I went and wrote it directly)
To get the name of the type in relatedClass => relatedClass.Type.Name
List all methods used in a class
So now, to list all the methods in the annotated class. Remember we are looping on classes here, coming from our syntactic tree.
To obtain a list of all the methods declared in this class we will ask to list the member of type method
IEnumerable<MethodDeclarationSyntax> classMethod = declaredClass.Members.Where(m => m.IsKind(SyntaxKind.MethodDeclaration)).OfType<MethodDeclarationSyntax>()
I will strongly recommend either casting to MethodDeclarationSyntax or assigning to a variable with the explicit type, because it is stored as a base type that does not expose all of the properties we'll need.
Once we've got our methods, we will once again loop on them.
Here are the few properties I needed for my use case :
methodDeclaration.Modifiers //public, static, etc...
methodDeclaration.Identifier // Quite obvious => the name
methodDeclaration.ParameterList //The list of the parameters, including type, name, default values
The rest was just a matter of constructing a string representing my target partial class which is now a pretty simple matter.
The final solution
Remember that's what I came up with as my first try, I will most probably submit it on the CodeReview StackExchange to see what could be improved.
And the RelatedModelaAttribute is basically the CustomAttribute class from my question.
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using SpeedifyCliWrapper.SourceGenerators.Annotations;
using System.Linq;
using System.Text;
namespace SpeedifyCliWrapper.SourceGenerators
{
[Generator]
class ModuleModelGenerator : ISourceGenerator
{
public void Execute(GeneratorExecutionContext context)
{
var attributeSymbol = context.Compilation.GetTypeByMetadataName(typeof(RelatedModelAttribute).FullName);
var classWithAttributes = context.Compilation.SyntaxTrees.Where(st => st.GetRoot().DescendantNodes().OfType<ClassDeclarationSyntax>()
.Any(p => p.DescendantNodes().OfType<AttributeSyntax>().Any()));
foreach (SyntaxTree tree in classWithAttributes)
{
var semanticModel = context.Compilation.GetSemanticModel(tree);
foreach(var declaredClass in tree
.GetRoot()
.DescendantNodes()
.OfType<ClassDeclarationSyntax>()
.Where(cd => cd.DescendantNodes().OfType<AttributeSyntax>().Any()))
{
var nodes = declaredClass
.DescendantNodes()
.OfType<AttributeSyntax>()
.FirstOrDefault(a => a.DescendantTokens().Any(dt => dt.IsKind(SyntaxKind.IdentifierToken) && semanticModel.GetTypeInfo(dt.Parent).Type.Name == attributeSymbol.Name))
?.DescendantTokens()
?.Where(dt => dt.IsKind(SyntaxKind.IdentifierToken))
?.ToList();
if(nodes == null)
{
continue;
}
var relatedClass = semanticModel.GetTypeInfo(nodes.Last().Parent);
var generatedClass = this.GenerateClass(relatedClass);
foreach(MethodDeclarationSyntax classMethod in declaredClass.Members.Where(m => m.IsKind(SyntaxKind.MethodDeclaration)).OfType<MethodDeclarationSyntax>())
{
this.GenerateMethod(declaredClass.Identifier, relatedClass, classMethod, ref generatedClass);
}
this.CloseClass(generatedClass);
context.AddSource($"{declaredClass.Identifier}_{relatedClass.Type.Name}", SourceText.From(generatedClass.ToString(), Encoding.UTF8));
}
}
}
public void Initialize(GeneratorInitializationContext context)
{
// Nothing to do here
}
private void GenerateMethod(SyntaxToken moduleName, TypeInfo relatedClass, MethodDeclarationSyntax methodDeclaration, ref StringBuilder builder)
{
var signature = $"{methodDeclaration.Modifiers} {relatedClass.Type.Name} {methodDeclaration.Identifier}(";
var parameters = methodDeclaration.ParameterList.Parameters.Skip(1);
signature += string.Join(", ", parameters.Select(p => p.ToString())) + ")";
var methodCall = $"return this._wrapper.{moduleName}.{methodDeclaration.Identifier}(this, {string.Join(", ", parameters.Select(p => p.Identifier.ToString()))});";
builder.AppendLine(#"
" + signature + #"
{
" + methodCall + #"
}");
}
private StringBuilder GenerateClass(TypeInfo relatedClass)
{
var sb = new StringBuilder();
sb.Append(#"
using System;
using System.Collections.Generic;
using SpeedifyCliWrapper.Common;
namespace SpeedifyCliWrapper.ReturnTypes
{
public partial class " + relatedClass.Type.Name);
sb.Append(#"
{");
return sb;
}
private void CloseClass(StringBuilder generatedClass)
{
generatedClass.Append(
#" }
}");
}
}
}
Inside the Execute method of your generator, add this:
var classesWithAttribute = context.Compilation.SyntaxTrees
.SelectMany(st => st.GetRoot()
.DescendantNodes()
.Where(n => n is ClassDeclarationSyntax)
.Select(n => n as ClassDeclarationSyntax)
.Where(r => r.AttributeLists
.SelectMany(al => al.Attributes)
.Any(a => a.Name.GetText().ToString() == "Foo")));
This basically takes all the nodes of all the trees, filters out nodes that are not class declarations, and, for each class declaration, looks if any of its attributes matches our custom attribute, "Foo" here.
Note: if your attribute is named FooAttribute, then you look for Foo, not FooAttribute.
// In SourceGenerator
public void Initialize(GeneratorInitializationContext context)
{
#if DEBUG
if (!Debugger.IsAttached)
{
//Debugger.Launch();
}
#endif
context.RegisterForSyntaxNotifications(() => new MySyntaxReceiver());
}
public void Execute(GeneratorExecutionContext context)
{
MySyntaxReceiver syntaxReceiver = (MySyntaxReceiver)context.SyntaxReceiver;
}
class MySyntaxReceiver : ISyntaxReceiver
{
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
{
// Note that the attribute name, is without the ending 'Attribute' e.g TestAttribute -> Test
if (syntaxNode is ClassDeclarationSyntax cds && cds.AttributeLists.Count > 0)
{
var syntaxAttributes = cds.AttributeLists.SelectMany(e => e.Attributes)
.Where(e => e.Name.NormalizeWhitespace().ToFullString() == "Test")
if (syntaxAttributes.Any())
{
// Do what you want with cds
}
}
}
}
I have an application which takes an XML document and sorts it by certain attributes. I have information associated with each line of the XML document which I want to include in the sorted document. In order to do this,
When I load the file, I make sure the line info is loaded using XDocument.Load(file, LoadOptions.SetLineInfo).
Then I recursively iterate over each XElement and get its line info. When I ran the app, I noticed that each XElement has two annotations,
one of type System.Xml.Linq.LineInfoAnnotation
and one of type System.Xml.Linq.LineInfoEndElementAnnotation.
They contain the info that I need but in private fields.
I can't find any information on these classes, I can't instantiate them, they do not appear in the Object browser under System.Xml.Linq. Yet they exist and I can run "GetType()" on them and get information about the class.
If they exist, why are they not in MSDN references and why can't I instantiate them or extend them? Why can't I find them in the object browser?
P.S. My workaround for this was to use reflection to get the information contained inside each element. But I still can't pass a class name to tell the method what type it is, I have to isolate the object from XElement.Annotations(typeof(object)), and then run GetType() on it. I've illustrated this below.
public object GetInstanceField(Type type, object instance, string fieldName)
{
//reflective method that gets value of private field
}
XElement xEl = existingXElement; //existingXElement is passed in
var annotations = xEl.Annotations(typeof(object)); //contains two objects, start and end LineInfoAnnotation
var start = annotations.First();
var end = annotations.Last();
var startLineNumber = GetInstanceField(start.GetType(), start, lineNumber); //lineNumber is private field I'm trying to access.
var endLineNumber = GetInstanceField(end.GetType(), end, lineNumber);
This code works, but again, I can't just tell the method "typeof(LineInfoAnnotation)", instead I have to do GetType on the existing object. I cannot make sense of this.
Those classes are private - an implementation detail, if you will.
All XObjects (elements, attributes) implement the IXmlLineInfo interface - but they implement the inteface explicitly, so you must perform a cast to access the properties.
Once you have your IXmlLineInfo, you can use the properties LineNumber and LinePosition.
var data =
#"<example>
<someElement
someAttribute=""val"">
</someElement></example>
";
var doc = XDocument.Load(new MemoryStream(Encoding.UTF8.GetBytes(data)), LoadOptions.SetLineInfo);
foreach(var element in doc.Descendants()) {
var elLineInfo = element as IXmlLineInfo;
Console.Out.WriteLine(
$"Element '{element.Name}' at {elLineInfo.LineNumber}:{elLineInfo.LinePosition}");
foreach(var attr in element.Attributes()) {
var attrLineInfo = attr as IXmlLineInfo;
Console.Out.WriteLine(
$"Attribute '{attr.Name}' at {attrLineInfo.LineNumber}:{attrLineInfo.LinePosition}");
}
}
Output:
Element 'example' at 1:2
Element 'someElement' at 2:2
Attribute 'someAttribute' at 3:3
To get the EndElement information, you have to use a plain old XML reader, since the XObject api doesn't expose any information about where the element ends.
using(var reader = doc.CreateReader()) {
while(reader.Read()) {
var lineInfo = reader as IXmlLineInfo;
Console.Out.WriteLine($"{reader.NodeType} {reader.Name} at {lineInfo.LineNumber}:{lineInfo.LinePosition}");
if(reader.NodeType == XmlNodeType.Element && reader.HasAttributes) {
while(reader.MoveToNextAttribute()) {
Console.Out.WriteLine($"{reader.NodeType} {reader.Name} at {lineInfo.LineNumber}:{lineInfo.LinePosition}");
}
}
}
}
Output:
Element example at 1:2
Element someElement at 2:2
Attribute someAttribute at 3:3
EndElement someElement at 5:5
EndElement example at 5:19
I'm having a custom generated variable declaration using SyntaxFactory.VariableDeclaration and a list of SyntaxNode's collected according to some conditions.
I did the following:
Modify the node
var newRoot = root.ReplaceNode(expression, newVariableDeclaration)
This successfully modified the node with the newVariableDeclaration.
In a loop remove the nodes corresponding to the ones present in the list
foreach (var listObject in listObjects)
{
newRoot = newRoot.RemoveNode(listObject, SyntaxRemoveOptions.KeepNoTrivia);
}
This doesn't change the newRoot and all the listObject required to be changed remains the same.
If we use root.RemoveNode(listObject, SyntaxRemoveOptions.KeepNoTrivia) instead it will, obviously, keep on replacing the previous changes.
So here the newVariableDeclaration is the only node that is changed in the whole document, is this because newRoot SyntaxNodes have changes from that of the SyntaxNode that I obtained from root.
Please correct me if I'm doing it wrong.
EDIT:
I looked into CSharpSyntaxRewriter but it appears that it is analyzing a single node everytime it's visiting a node and can only modify a single node at a time. In my scenario, I'll have to visit a particular node, make changes to it, and remove other nodes with respect to the changes made to visited node.
The problem with your approach is that whenever you change the tree (using ReplaceNode() or RemoveNode()), it means all the nodes change as well. This is why your calls to RemoveNode() after ReplaceNode() don't do anything.
One way to fix this is to use TrackNodes(), so that you can find which nodes in the modified tree correspond to nodes in the original tree.
Uisng this, a method that replaces a sequence of nodes with a single node could look like this:
public static T ReplaceNodes<T>(
this T root, IReadOnlyList<SyntaxNode> oldNodes, SyntaxNode newNode)
where T : SyntaxNode
{
if (oldNodes.Count == 0)
throw new ArgumentException();
var newRoot = root.TrackNodes(oldNodes);
var first = newRoot.GetCurrentNode(oldNodes[0]);
newRoot = newRoot.ReplaceNode(first, newNode);
var toRemove = oldNodes.Skip(1).Select(newRoot.GetCurrentNode);
newRoot = newRoot.RemoveNodes(toRemove, SyntaxRemoveOptions.KeepNoTrivia);
return newRoot;
}
I have a simple XElement object
XElement xml = new XElement("XML",
new XElement ("TOKEN",Session["Token"]),
new XElement("ALL_INCLUSIVE", "0"),
new XElement("BEACH", "0"),
new XElement("DEST_DEP", ddlDest.SelectedValue.ToString()),
new XElement("FLEX", "0")
);
Where want to dump out the contents into a string. Exactly like how Console.Writeline(xml); does, but I want the contents in a string. I tried various methonds. xml.ToString(); doesn't return anything on its own.
ToString should most definitely work. I use it all the time. What does it return for you in this case? An empty string? My guess is that something went wrong building your XElement. To debug, rewrite the code to add each of the child XElements separately, so that you can step through your code and check on each of them. Then before you execute the .ToString, in the Locals window, look at the [xml] variable expanded to xml.
In short, your problem is happening before you ever get to the ToString() method.
ToString works, but it returns content including XElement tag itself. If you need for Inner XML without root tag ("" in your example), you may use the following extension method:
public static class XElementExtension
{
public static string InnerXML(this XElement el) {
var reader = el.CreateReader();
reader.MoveToContent();
return reader.ReadInnerXml();
}
}
Then simple call it: xml.InnerXML();
I'm using .NET 2.0, and a recent code change has invalidated my previous Assert.AreEqual call (which compared two strings of XML). Only one element of the XML is actually different in the new codebase, so my hope is that a comparison of all the other elements will give me the result I want. The comparison needs to be done programmatically, since it's part of a unit test.
At first, I was considering using a couple instances of XmlDocument. But then I found this:
http://drowningintechnicaldebt.com/blogs/scottroycraft/archive/2007/05/06/comparing-xml-files.aspx
It looks like it might work, but I was interested in Stack Overflow feedback in case there's a better way.
I'd like to avoid adding another dependency for this if at all possible.
Similar questions
Is there an XML asserts for NUnit?
How would you compare two XML Documents?
It really depends on what you want to check as "differences".
Right now, we're using Microsoft XmlDiff: http://msdn.microsoft.com/en-us/library/aa302294.aspx
You might find it's less fragile to parse the XML into an XmlDocument and base your Assert calls on XPath Query. Here are some helper assertion methods that I use frequently. Each one takes a XPathNavigator, which you can obtain by calling CreateNavigator() on the XmlDocument or on any node retrieved from the document. An example of usage would be:
XmlDocument doc = new XmlDocument( "Testdoc.xml" );
XPathNavigator nav = doc.CreateNavigator();
AssertNodeValue( nav, "/root/foo", "foo_val" );
AssertNodeCount( nav, "/root/bar", 6 )
private static void AssertNodeValue(XPathNavigator nav,
string xpath, string expected_val)
{
XPathNavigator node = nav.SelectSingleNode(xpath, nav);
Assert.IsNotNull(node, "Node '{0}' not found", xpath);
Assert.AreEqual( expected_val, node.Value );
}
private static void AssertNodeExists(XPathNavigator nav,
string xpath)
{
XPathNavigator node = nav.SelectSingleNode(xpath, nav);
Assert.IsNotNull(node, "Node '{0}' not found", xpath);
}
private static void AssertNodeDoesNotExist(XPathNavigator nav,
string xpath)
{
XPathNavigator node = nav.SelectSingleNode(xpath, nav);
Assert.IsNull(node, "Node '{0}' found when it should not exist", xpath);
}
private static void AssertNodeCount(XPathNavigator nav, string xpath, int count)
{
XPathNodeIterator nodes = nav.Select( xpath, nav );
Assert.That( nodes.Count, Is.EqualTo( count ) );
}
Doing a simple string compare on a xml string not always work. Why ?
for example both :
<MyElement></MyElmennt> and <MyElment/> are equal from an xml standpoint ..
There are algorithms for converting making an xml always look the same, they are called
canonicalization algorithms. .Net has support for canonicalization.
I wrote a small library with asserts for serialization, source.
Sample:
[Test]
public void Foo()
{
...
XmlAssert.Equal(expected, actual, XmlAssertOptions.IgnoreDeclaration | XmlAssertOptions.IgnoreNamespaces);
}
Because of the contents of an XML file can have different formatting and still be considered the same (from a DOM point of view) when you are testing the equality you need to determine what the measure of that equality is, for example is formatting ignored? does meta-data get ignored etc is positioning important, lots of edge cases.
Generally you would create a class that defines your equality rules and use it for your comparisons, and if your comparison class implements the IEqualityComparer and/or IEqualityComparer<T> interfaces, then your class can be used in a bunch of inbuilt framework lists as the equality test implementation as well. Plus of course you can have as many as you need to measure equality differently as your requirements require.
i.e
IEnumerable<T>.Contains
IEnumerable<T>.Equals
The constructior of a Dictionary etc etc
I ended up getting the result I wanted with the following code:
private static void ValidateResult(string validationXml, XPathNodeIterator iterator, params string[] excludedElements)
{
while (iterator.MoveNext())
{
if (!((IList<string>)excludedElements).Contains(iterator.Current.Name))
{
Assert.IsTrue(validationXml.Contains(iterator.Current.Value), "{0} is not the right value for {1}.", iterator.Current.Value, iterator.Current.Name);
}
}
}
Before calling the method, I create a navigator on the instance of XmlDocument this way:
XPathNavigator nav = xdoc.CreateNavigator();
Next, I create an instance of XPathExpression, like so:
XPathExpression expression = XPathExpression.Compile("/blah/*");
I call the method after creating an iterator with the expression:
XPathNodeIterator iterator = nav.Select(expression);
I'm still figuring out how to optimize it further, but it does the trick for now.
I made a method to create simple XML paths.
static XElement MakeFromXPath(string xpath)
{
XElement root = null;
XElement parent = null;
var splits = xpath.Split('/'); //split xpath into parts
foreach (var split in splits)
{
var el = new XElement(split);
if (parent != null)
parent.Add(el);
else
root = el; //first element created, set as root
parent = el;
}
return root;
}
Sample usage:
var element = MakeFromXPath("My/Path/To/Element")'
element will contain the value:
<My>
<Path>
<To>
<Element></Element>
</To>
</Path>
</My>