Skip to content

tleviathan/Albedo

 
 

Repository files navigation

Albedo

A .NET library targeted at making Reflection programming more consistent, using a common set of abstractions and utilities.

This examples uses a PropertyInfo to read a value off a System.Version instance:

PropertyInfo pi = from v in new Properties<Version>()
                  select v.Minor;
var version = new Version(2, 7);
var visitor = new ValueCollectingVisitor(version);

var actual = new PropertyInfoElement(pi).Accept(visitor);

Assert.Equal(version.Minor, actual.Value.OfType<int>().First());

More examples further down.

Albedo follows Semantic Versioning 2.0.0.

Where do you get it?

Obviously, the source code is available here on GitHub, but you can download the compiled library with NuGet.

What problem does it address?

Albedo addresses the problem that the .NET Reflection API (mainly in System.Reflection) doesn't provide a set of good abstractions. As an example, both PropertyInfo and FieldInfo expose GetValue and SetValue functions, yet despite their similarities, these functions are defined directly on each of those two classes, so there's no polymorphic API to read a value from a property or field, or assign a value to a property or field.

It's also difficult to extract the type of a property or field in a polymorphic manner, because the type of a property is defined by the PropertyType property, while the type of a field is defined by the FieldType property.

At least PropertyInfo and FieldInfo both derive from the abstract MemberInfo class, so they still have a little in common. However, if you want to compare any of these to, say, a ParameterInfo instance, the highest common ancestor is Object!

While you can define your own interfaces or delegates to deal with this lack of polymorphism in the Reflection API, Albedo offers a common set of abstractions over the Reflection API. These abstractions are based on tried-and-true design patterns, and as the code examples below demonstrate, are very flexible.

Who cares?

People who write lots of Reflection code might benefit from Albedo: Programmers of

  • ORMs
  • Auto-Mappers
  • dynamic mock libraries
  • DI Containers
  • unit testing frameworks
  • etc.

Instead of defining a constrained and specific set of interfaces for each such tool, Albedo can provide a common, reusable abstraction over Reflection, enabling a more open architecture.

Albedo was born out of a need for such abstractions in AutoFixture. While it's easy to extract a specific interface to address a specific need, you always run the risk of defining an interface that can only be used in one very specific situation; such an interface may not be a good abstraction.

If you don't write a lot of Reflection code, you probably don't need Albedo.

How does it work?

In OOD, whenever you find yourself in a situation where you need to provide a consistent API over a final, known set of concrete classes, the much-derided Visitor pattern is very useful. Albedo is based on an IReflectionVisitor<T> interface that visits Adapters over the known Reflection types, such as PropertyInfo, ParameterInfo, etc.

While Albedo defines the IReflectionVisitor<T> interface, it also provides a ReflectionVisitor<T> abstract base class you can use to visit only those IReflectionElement Adapters you care about.

All examples shown here can be found in the Scenario class in the Albedo code base's unit tests.

Collecting values

The initial example uses a sample ValueCollectingVisitor, which is implemented like this:

public class ValueCollectingVisitor : ReflectionVisitor<IEnumerable>
{
    private readonly object target;
    private readonly object[] values;

    public ValueCollectingVisitor(object target, params object[] values)
    {
        this.target = target;
        this.values = values;
    }

    public override IEnumerable Value
    {
        get { return this.values; }
    }

    public override IReflectionVisitor<IEnumerable> Visit(
        FieldInfoElement fieldInfoElement)
    {
        var value = fieldInfoElement.FieldInfo.GetValue(this.target);
        return new ValueCollectingVisitor(
            this.target,
            this.values.Concat(new[] { value }).ToArray());
    }

    public override IReflectionVisitor<IEnumerable> Visit(
        PropertyInfoElement propertyInfoElement)
    {
        var value = 
            propertyInfoElement.PropertyInfo.GetValue(this.target, null);
        return new ValueCollectingVisitor(
            this.target,
            this.values.Concat(new[] { value }).ToArray());
    }
}

Notice that this Visitor only collects information about the values of properties and fields, and not (say) the return value of method calls. (Since this is sample code, it implicitly assumes that PropertyInfo.GetValue will succeed, which will not be the case if the property is a write-only property. However, it's trivial to add a check to see if the property can be read.)

Note: ValueCollectingVisitor has turned out to be such a generally useful implementation that it's now available by default in Albedo. Thus, you don't have to implement it yourself, but remains here as an example of how to use Albedo.

Here's a more complicated example that uses the ValueCollectingVisitor to read all public properties and fields from a TimeSpan instance:

var ts = new TimeSpan(2, 4, 3, 8, 9);
var elements = ts.GetType().GetPublicPropertiesAndFields().ToArray();

var actual = elements.Accept(new ValueCollectingVisitor(ts));

var actualValues = actual.Value.ToArray();
Assert.Equal(elements.Length, actualValues.Length);
Assert.Equal(1, actualValues.Count(ts.Days.Equals));
Assert.Equal(1, actualValues.Count(ts.Hours.Equals));
Assert.Equal(1, actualValues.Count(ts.Milliseconds.Equals));
Assert.Equal(1, actualValues.Count(ts.Minutes.Equals));
Assert.Equal(1, actualValues.Count(ts.Seconds.Equals));
Assert.Equal(1, actualValues.Count(ts.Ticks.Equals));
Assert.Equal(1, actualValues.Count(ts.TotalDays.Equals));
Assert.Equal(1, actualValues.Count(ts.TotalHours.Equals));
Assert.Equal(1, actualValues.Count(ts.TotalMilliseconds.Equals));
Assert.Equal(1, actualValues.Count(ts.TotalMinutes.Equals));
Assert.Equal(1, actualValues.Count(ts.TotalSeconds.Equals));
Assert.Equal(1, actualValues.Count(TimeSpan.MaxValue.Equals));
Assert.Equal(1, actualValues.Count(TimeSpan.MinValue.Equals));
Assert.Equal(1, actualValues.Count(TimeSpan.TicksPerDay.Equals));
Assert.Equal(1, actualValues.Count(TimeSpan.TicksPerHour.Equals));
Assert.Equal(1, actualValues.Count(TimeSpan.TicksPerMillisecond.Equals));
Assert.Equal(1, actualValues.Count(TimeSpan.TicksPerMinute.Equals));
Assert.Equal(1, actualValues.Count(TimeSpan.TicksPerSecond.Equals));
Assert.Equal(1, actualValues.Count(TimeSpan.Zero.Equals));

The ValueCollectingVisitor class is the only custom code written to implement this behaviour. The rest of the types in the above code example is either defined in the BCL (TimeSpan, etc.) or provided by Albedo (PropertyInfoElement, FieldInfoElement, CompositeReflectionElement).

Assigning values

Albedo wouldn't be a truly flexible library if the only thing it could do is to read values from properties and fields. It can also do other things; closely related to reading values is assigning values.

Assume that you have a class like this:

    public class ClassWithWritablePropertiesAndFields<T>
    {
        public T Field1;

        public T Field2;

        public T Property1 { get; set; }

        public T Property2 { get; set; }
    }

If you want to assign a value to all fields and properties, you can do it like this:

var t = new ClassWithWritablePropertiesAndFields<int>();
var elements = t.GetType().GetPublicPropertiesAndFields().ToArray();

var actual = elements.Accept(new ValueWritingVisitor(t));
actual.Value(42);
            
Assert.Equal(42, t.Field1);
Assert.Equal(42, t.Field2);
Assert.Equal(42, t.Property1);
Assert.Equal(42, t.Property2);

Apart from ClassWithWritablePropertiesAndFields<T>, the only custom type you'd have to provide to enable this feature is the ValueWritingVisitor:

public class ValueWritingVisitor : ReflectionVisitor<Action<object>>
{
    private readonly object target;
    private readonly Action<object>[] actions;

    public ValueWritingVisitor(
        object target,
        params Action<object>[] actions)
    {
        this.target = target;
        this.actions = actions;
    }

    public override Action<object> Value
    {
        get
        {
            return x =>
            {
                foreach (var a in this.actions)
                    a(x);
            };
        }
    }

    public override IReflectionVisitor<Action<object>> Visit(
        FieldInfoElement fieldInfoElement)
    {
        Action<object> a =
            v => fieldInfoElement.FieldInfo.SetValue(this.target, v);
        return new ValueWritingVisitor(
            this.target,
            this.actions.Concat(new[] { a }).ToArray());
    }

    public override IReflectionVisitor<Action<object>> Visit(
        PropertyInfoElement propertyInfoElement)
    {
        Action<object> a =
            v => propertyInfoElement.PropertyInfo.SetValue(
                this.target,
                v,
                null);
        return new ValueWritingVisitor(
            this.target,
            this.actions.Concat(new[] { a }).ToArray());
    }
}

As you can see in this example, the ValueWritingVisitor collects an array of Actions that can be executed once the Visitor has visited all the elements. (Once again, for demonstration purposes, the above sample code assumes that each assignment is possible, which may very well not be the case if the property or field is read-only, or if the types don't match.)

Comparison

Another, more advanced scenario (and the scenario that ultimately sparked the Albedo project in the first place) is to compare disparate Reflection elements.

Imagine, for example, that you want to match a constructor's ParameterInfo to a PropertyInfo that exposes the value originally passed to the constructor (Structural Inspection). For example, you might want to check whether the Version major constructor argument matches the Major property.

Since this is a comparison, it sounds like a job for IEqualityComparer<T>, but what should T be?

The challenge is that ParameterInfo shares no API with PropertyInfo, and additionally, you'll want to make a case-insensitive comparison of their names.

Albedo can address this scenario with its abstractions on top of Reflection, enabling you to declare an IEqualityComparer<IReflectionElement> and use it like this:

[Theory]
[InlineData("Major", "major", true)]
[InlineData("Major", "minor", false)]
[InlineData("Minor", "major", false)]
[InlineData("Minor", "minor", true)]
public void MatchContructorArgumentAgainstReadOnlyProperty(
    string propertyName, string parameterName, bool expected)
{
    var prop = new PropertyInfoElement(
        typeof(Version).GetProperty(propertyName));
    var param = new ParameterInfoElement(
        typeof(Version)
            .GetConstructor(new[] { typeof(int), typeof(int) })
            .GetParameters()
            .Where(p => p.Name == parameterName)
            .Single());

    var actual = 
        new SemanticElementComparer(new SemanticReflectionVisitor())
            .Equals(prop, param);

    Assert.Equal(expected, actual);
}

The custom SemanticReflectionVisitor collects comparison values, based on properties, fields, and parameters:

public class SemanticReflectionVisitor : ReflectionVisitor<IEnumerable>
{
    private readonly object[] values;

    public SemanticReflectionVisitor(
        params object[] values)
    {
        this.values = values;
    }

    public override IEnumerable Value
    {
        get { return this.values; }
    }

    public override IReflectionVisitor<IEnumerable> Visit(
        FieldInfoElement fieldInfoElement)
    {
        var v = new SemanticComparisonValue(
            fieldInfoElement.FieldInfo.Name,
            fieldInfoElement.FieldInfo.FieldType);
        return new SemanticReflectionVisitor(
            this.values.Concat(new[] { v }).ToArray());
    }

    public override IReflectionVisitor<IEnumerable> Visit(
        ParameterInfoElement parameterInfoElement)
    {
        var v = new SemanticComparisonValue(
            parameterInfoElement.ParameterInfo.Name,
            parameterInfoElement.ParameterInfo.ParameterType);
        return new SemanticReflectionVisitor(
            this.values.Concat(new[] { v }).ToArray());
    }

    public override IReflectionVisitor<IEnumerable> Visit(
        PropertyInfoElement propertyInfoElement)
    {
        var v = new SemanticComparisonValue(
            propertyInfoElement.PropertyInfo.Name,
            propertyInfoElement.PropertyInfo.PropertyType);
        return new SemanticReflectionVisitor(
            this.values.Concat(new[] { v }).ToArray());
    }
}

The SemanticComparisonValue class is a custom Value Object that collects the names and types of the fields, properties, or parameters:

public class SemanticComparisonValue
{
    private readonly string name;
    private readonly Type type;

    public SemanticComparisonValue(string name, Type type)
    {
        this.name = name;
        this.type = type;
    }

    public override bool Equals(object obj)
    {
        var other = obj as SemanticComparisonValue;
        if (other == null)
            return base.Equals(obj);

        return object.Equals(this.type, other.type)
            && string.Equals(this.name, other.name,
                StringComparison.OrdinalIgnoreCase);
    }

    public override int GetHashCode()
    {
        return
            this.name.ToUpperInvariant().GetHashCode() ^
            this.type.GetHashCode();
    }
}

The important quality of the SemanticComparisonValue class is that it implements custom equality comparison, so that it compares the names in a case-insensitive manner.

With these two custom classes, it's fairly straight-forward to implement the EqualityComparer:

public class SemanticElementComparer : IEqualityComparer<IReflectionElement>
{
    private readonly IReflectionVisitor<IEnumerable> visitor;

    public SemanticElementComparer(
        IReflectionVisitor<IEnumerable> visitor)
    {
        this.visitor = visitor;
    }

    public bool Equals(IReflectionElement x, IReflectionElement y)
    {
        var values = new CompositeReflectionElement(x, y)
            .Accept(this.visitor)
            .Value
            .Cast<object>()
            .ToArray();
        return values.Length == 2
            && values.Distinct().Count() == 1;
    }

    public int GetHashCode(IReflectionElement obj)
    {
        return obj
            .Accept(this.visitor)
            .Value
            .Cast<object>()
            .Single()
            .GetHashCode();
    }
}

Since the Equals method compares exactly two elements, there should also be exactly two collected elements. This isn't guaranteed, because x or y could also be instances of TypeElement or MethodInfoElement, and SemanticReflectionVisitor doesn't collect those. On the other hand, while there should be exactly two instances, they must be equal to each other for the Equals method to return true; thus, if the count of distinct values is one, they are equal to each other, since the Distinct method uses object equality, as implemented by each element's Equals method (and recall that SemanticComparisonValue overrides Equals).

Strongly-typed queries of type members

Sometimes, you'd like to get a PropertyInfo instance for a particular class' property, or similar for fields and methods. The problem with the BCL Reflection API is that you have to refer to the property by naming it with a string, which isn't refactoring-safe.

As a convenience, Albedo provides a couple of strongly typed queries over type members:

MethodInfo mi = new Methods<Version>().Select(v => v.ToString());
Assert.Equal("ToString", mi.Name);

Due to the signature of the Select method, you can alternatively write the above query as a LINQ query!

MethodInfo mi = from v in new Methods<Version>()
                select v.ToString();
Assert.Equal("ToString", mi.Name);

In addition to strongly-typed queries over methods, Albedo also provides strongly-typed queries over fields and properties.

About

A .NET library targeted at making Reflection programming more consistent, using a common set of abstractions and utilities

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • C# 96.2%
  • XSLT 2.8%
  • Other 1.0%