Search code examples
c#system.reflection

C# - Provide type information to extension method


We're currently doing some performance tuning on an application that made heavy use of annotations and extension methods, such as:

public static TClass TruncateToMaxLength<TClass>(this TClass inClass)
{
   foreach (PropertyInfo classProp in typeof(TClass).GetProperties())
   var maxLengthAttribute = classProp.GetCustomMetaDataAttribute<MaxLengthAttribute>();
   if (null != maxLengthAttribute)
   {
     int maxLength = maxLengthAttribute.MaximumLength;
     var inClassProp = classProp.GetValue(inClass);
     if (null != inClassProp)
     {
       var strProp = inClassProp.ToString();
       classProp.SetValue(inClass, 
         strProp.Length > maxLength ? strProp.Substring(0, maxLength) : strProp);
     }
   }
}

to do things like automatically trim generated strings down to the [MaxLength()] attribute on their associated properties. In some cases we're looking at switching over to manual implementations to avoid reflection overhead, so:

public class SimplePerson : IManualMaxLength {
    [MaxLength(10)]
    public IdNumber { get; set; }
    [MaxLength(100)]
    public DisplayName { get; set; }

    public void ManualMaxLength() {
       if (IdNumber?.Length > 10) IdNumber = IdNumber.Substring(0, 10);
       if (DisplayName?.Length > 100) DisplayName = DisplayName.Substring(0, 100);
    }
}

public static TClass OptimizedMaxLength<TClass>(this TClass inClass)
{
   if (inClass is IManualMaxLength manualClass) manualClass.ManualMaxLength();
   else inClass.TruncateToMaxLength();
   return inClass;
}

where the manual operation is implemented as a class method without reflection.

In order to verify that all the manual operations match what the reflection operations would be, I had the idea to set up a test rig along the following lines, to automatically check all optimization work:

foreach (var manualType in (
    from x in Assembly.GetAssembly(typeof(IManualMaxLength)).GetTypes()
    where x.GetInterfaces().Contains(typeof(IManualMaxLength)) 
    select x))
{
    var implManual = (IManualOperation)Activator.CreateInstance(manualType);
    var implAuto = Activator.CreateInstance(manualType);

    //some setup of properties

    implManual.ManualMaxLength();
    implAuto.TruncateToMaxLength();

    //assert rig
}

The rig iterates through the assembly and grabs all types that implement IManualMaxLength; it then uses Activator.CreateInstance() to set up a test case and compares the manual implementation to the expected outcome obtained by reflection.

When implAuto is a SimplePerson, I expect that to be used in TruncateToMaxLength to make the test rig accurate. But the call is being made for a TClass of object. That is what comes out of the call to Activator.CreateInstance() but it doesn't have any properties.

Now, to perform the setup and asserts I had to make a call to manualType.GetProperties(), and I can create an overload as follows:

public static TClass TruncateToMaxLength<TClass>
   (this TClass inClass, PropertyInfo[] testProperties = null)
{
   var propertyInfo = testProperties ?? typeof(TClass).GetProperties();
   foreach (PropertyInfo classProp in propertyinfo)
   var maxLengthAttribute = classProp.GetCustomMetaDataAttribute<MaxLengthAttribute>();
   if (null != maxLengthAttribute)
   {
     int maxLength = maxLengthAttribute.MaximumLength;
     var inClassProp = classProp.GetValue(inClass);
     if (null != inClassProp)
     {
       var strProp = inClassProp.ToString();
       classProp.SetValue(inClass, 
         strProp.Length > maxLength ? strProp.Substring(0, maxLength) : strProp);
     }
   }
}

but I don't really want to do that on an extension method that's never intended to take it during normal operation.

Am I stuck doing that anyway, or is there some other way to provide the elements of a created type in a way that an extension method will pick it up?


Solution

  • Inside TruncateToMaxLength<TClass>, you can use inClass.GetType().GetProperties() instead of typeof(TClass).GetProperties.

    public static void TruncateToMaxLength<TClass>(this TClass inClass)
    {
        foreach (PropertyInfo classProp in inClass.GetType().GetProperties()) // This Line
        {
            var maxLengthAttribute = classProp.GetCustomAttribute<MaxLengthAttribute>();
            if (null != maxLengthAttribute)
            {
                int maxLength = maxLengthAttribute.MaximumLength;
                var inClassProp = classProp.GetValue(inClass);
                if (null != inClassProp)
                {
                    var strProp = inClassProp.ToString();
                    classProp.SetValue(inClass,
                        strProp.Length > maxLength ? strProp.Substring(0, maxLength) : strProp);
                }
            }
        }
    }
    

    That seemed to work for me using your case of SimplePerson. One thing to be aware of if doing it this way would cause TruncateToMaxLength to execute on the properties of inClass's type. So, if you had something like...

    public interface IHaveAnId
    {
       [MaxLength(5)]
       string Id { get; set; }
    }
    
    public class SimplePerson : IHaveAnId
    {
       [MaxLength(10)]
       public string Id { get; set; }
       [MaxLength(100)]
       public string DisplayName { get; set; }
    }
    
    public void Main()
    {
       IHaveAnId s = new SimplePerson();
       s.TruncateToMaxLength(); 
    }
    

    Then the call to s.TruncateToMaxLength will operate on all the properties of SimplePerson and use the MaxLength attribute on that class' properties.

    As an aside, I don't know what your performance requirements are, but you can speed up TruncateToMaxLength<TClass>. It'll never be as fast as your ManualMaxLength (at the very least I'm not clever enough to get something that fast with reflection), but you can make some gains over what you have now by caching the PropertyInfo instances and the MaxLength value.

    using System;
    using System.Collections.Concurrent;
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    using System.Linq;
    using System.Reflection;
    
    public static class Extensions
    {
       public static void TruncateToMaxLengthCached<TClass>(this TClass input)
       {
          var type = typeof(TClass);
          var props = _cache.GetOrAdd(type, t =>
          {
             return new Lazy<IReadOnlyCollection<MaxLengthData>>(() => BuildData(input));
          }).Value;
          foreach (var data in props)
          {
             var value = data.Property.GetValue(input)?.ToString();
             if (value?.Length > data.MaxLength)
             {
                data.Property.SetValue(input, value.Substring(0, data.MaxLength));
             }
          }
       }
    
       private static IReadOnlyCollection<MaxLengthData> BuildData<TClass>(TClass input)
       {
          Type type = typeof(TClass);
          var result = new List<MaxLengthData>();
          foreach (var prop in type.GetProperties())
          {
             var maxLengthAttribute = prop.GetCustomAttribute<MaxLengthAttribute>();
             if (null != maxLengthAttribute)
             {
                result.Add(new MaxLengthData
                {
                   MaxLength = maxLengthAttribute.Length,
                   Property = prop,
                   TargetType = type
                });
             }
          }
          return result;
       }
    
       private static ConcurrentDictionary<Type, Lazy<IReadOnlyCollection<MaxLengthData>>> _cache =
          new ConcurrentDictionary<Type, Lazy<IReadOnlyCollection<MaxLengthData>>>();
       private class MaxLengthData
       {
          public Type TargetType { get; set; }
          public PropertyInfo Property { get; set; }
          public int MaxLength { get; set; }
       }
    }
    

    And the BenchmarkDotNet results:

    |           Method |             Id |                  Name |          Mean |       Error |      StdDev | Rank |
    |----------------- |--------------- |---------------------- |--------------:|------------:|------------:|-----:|
    |   ManualTruncate | 09123456789093 |          John Johnson |     28.103 ns |   0.6188 ns |   0.8046 ns |    2 |
    | OriginalTruncate | 09123456789093 |          John Johnson | 17,953.005 ns | 356.7870 ns | 534.0220 ns |    8 |
    |   CachedTruncate | 09123456789093 |          John Johnson |    697.548 ns |  13.6592 ns |  13.4152 ns |    6 |
    |   ManualTruncate | 09123456789093 |  Mr. J(...), Esq [98] |     59.177 ns |   1.2251 ns |   1.5494 ns |    4 |
    | OriginalTruncate | 09123456789093 |  Mr. J(...), Esq [98] | 18,333.251 ns | 365.0699 ns | 461.6966 ns |    8 |
    |   CachedTruncate | 09123456789093 |  Mr. J(...), Esq [98] |    995.924 ns |  19.9356 ns |  23.7319 ns |    7 |
    |   ManualTruncate | 09123456789093 | Mr. J(...)hnson [111] |     58.787 ns |   0.4812 ns |   0.4501 ns |    4 |
    | OriginalTruncate | 09123456789093 | Mr. J(...)hnson [111] | 18,032.030 ns | 220.0009 ns | 195.0251 ns |    8 |
    |   CachedTruncate | 09123456789093 | Mr. J(...)hnson [111] |    977.168 ns |  19.2770 ns |  27.6465 ns |    7 |
    |   ManualTruncate |              1 |          John Johnson |      6.800 ns |   0.2039 ns |   0.2651 ns |    1 |
    | OriginalTruncate |              1 |          John Johnson | 18,173.803 ns | 192.1153 ns | 170.3052 ns |    8 |
    |   CachedTruncate |              1 |          John Johnson |    410.136 ns |   3.8655 ns |   3.6158 ns |    5 |
    |   ManualTruncate |              1 |  Mr. J(...), Esq [98] |     34.886 ns |   0.7203 ns |   0.6385 ns |    3 |
    | OriginalTruncate |              1 |  Mr. J(...), Esq [98] | 18,013.630 ns | 327.2057 ns | 306.0684 ns |    8 |
    |   CachedTruncate |              1 |  Mr. J(...), Esq [98] |    684.351 ns |  12.0877 ns |  11.3069 ns |    6 |
    |   ManualTruncate |              1 | Mr. J(...)hnson [111] |     34.285 ns |   0.6136 ns |   0.5124 ns |    3 |
    | OriginalTruncate |              1 | Mr. J(...)hnson [111] | 17,926.434 ns | 184.0216 ns | 172.1340 ns |    8 |
    |   CachedTruncate |              1 | Mr. J(...)hnson [111] |    685.590 ns |   9.6743 ns |   9.0493 ns |    6 |