5
\$\begingroup\$

Sometimes when I write objects for holding data (not real DTOs, no serialization involved) I also create an interface for it. The class that implements it can also perform some null checks. I don't always like to use the Validator because you need to remember to call it. Such implementations may look like this:

public interface IPerson
{
  [NotNull]
  string FirstName { get; set; }

  [NotNull]
  string LastName { get; set; }

  string NickName { get; set; }
}

public class Person : IPerson
{
  private string _firstName;
  private string _lastName;

  public string FirstName
  {
      get => _firstName ?? throw new InvalidOperationException($"{nameof(FirstName)} isn' set.");
      set => _firstName = value ?? throw new ArgumentNullException(nameof(FirstName));
  }
  public string LastName
  {
      get => _lastName ?? throw new InvalidOperationException($"{nameof(LastName)} isn' set.");
      set => _lastName = value ?? throw new ArgumentNullException(nameof(LastName));
  }

  public string NickName { get; set; }
}

But these checks are another boilerplate code that I'd rather generate then write myself each time. So I experimented with the ILGenerator and wrote this ObjectFactory helper. It creates a new type that implements properties of the specified interface and adds null checks if a property is decorated with the NotNullAttribute (this is used by ReSharper). Derived types are cached.

public static class ObjectFactory
{
    private static readonly ConcurrentDictionary<Type, Type> TypeCache = new ConcurrentDictionary<Type, Type>();

    public static T CreateInstance<T>()
    {
        if (!typeof(T).IsInterface) throw new ArgumentException($"Type {typeof(T).Name} must be an interface.");
        var newType = TypeCache.GetOrAdd(typeof(T), t => BuildType(typeof(T)));
        return (T)Activator.CreateInstance(newType);
    }

    private static Type BuildType(Type interfaceType)
    {
        var assemblyName = new AssemblyName($"DynamicAssembly_{Guid.NewGuid():N}");
        var assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
        var moduleBuilder = assemblyBuilder.DefineDynamicModule("DynamicModule");
        var typeName = $"{RemoveInterfacePrefix(interfaceType.Name)}_{Guid.NewGuid():N}";
        var typeBuilder = moduleBuilder.DefineType(typeName, TypeAttributes.Public);

        typeBuilder.AddInterfaceImplementation(interfaceType);

        var properties = interfaceType.GetProperties(BindingFlags.Instance | BindingFlags.Public);

        BuildProperties(typeBuilder, properties);

        return typeBuilder.CreateType();

        string RemoveInterfacePrefix(string name) => Regex.Replace(name, "^I", string.Empty);
    }

    private static void BuildProperties(TypeBuilder typeBuilder, IEnumerable<PropertyInfo> properties)
    {
        foreach (var property in properties)
        {
            BuildProperty(typeBuilder, property);
        }
    }

    private static PropertyBuilder BuildProperty(TypeBuilder typeBuilder, PropertyInfo property)
    {
        var fieldName = $"<{property.Name}>k__BackingField";

        var propertyBuilder = typeBuilder.DefineProperty(property.Name, System.Reflection.PropertyAttributes.None, property.PropertyType, Type.EmptyTypes);

        // Build backing-field.
        var fieldBuilder = typeBuilder.DefineField(fieldName, property.PropertyType, FieldAttributes.Private);

        var getSetAttributes = MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig | MethodAttributes.Virtual;

        var getterBuilder = BuildGetter(typeBuilder, property, fieldBuilder, getSetAttributes);
        var setterBuilder = BuildSetter(typeBuilder, property, fieldBuilder, getSetAttributes);

        propertyBuilder.SetGetMethod(getterBuilder);
        propertyBuilder.SetSetMethod(setterBuilder);

        return propertyBuilder;
    }

    private static MethodBuilder BuildGetter(TypeBuilder typeBuilder, PropertyInfo property, FieldBuilder fieldBuilder, MethodAttributes attributes)
    {
        var getterBuilder = typeBuilder.DefineMethod($"get_{property.Name}", attributes, property.PropertyType, Type.EmptyTypes);
        var ilGenerator = getterBuilder.GetILGenerator();

        ilGenerator.Emit(OpCodes.Ldarg_0);
        ilGenerator.Emit(OpCodes.Ldfld, fieldBuilder);

        if (property.GetCustomAttribute<NotNullAttribute>() != null)
        {
            // Build null check
            ilGenerator.Emit(OpCodes.Dup);

            var isFieldNull = ilGenerator.DefineLabel();
            ilGenerator.Emit(OpCodes.Brtrue_S, isFieldNull);
            ilGenerator.Emit(OpCodes.Pop);
            ilGenerator.Emit(OpCodes.Ldstr, $"{property.Name} isn't set.");

            var invalidOperationExceptionConstructor = typeof(InvalidOperationException).GetConstructor(new Type[] { typeof(string) });
            ilGenerator.Emit(OpCodes.Newobj, invalidOperationExceptionConstructor);
            ilGenerator.Emit(OpCodes.Throw);

            ilGenerator.MarkLabel(isFieldNull);
        }
        ilGenerator.Emit(OpCodes.Ret);

        return getterBuilder;
    }

    private static MethodBuilder BuildSetter(TypeBuilder typeBuilder, PropertyInfo property, FieldBuilder fieldBuilder, MethodAttributes attributes)
    {
        var setterBuilder = typeBuilder.DefineMethod($"set_{property.Name}", attributes, null, new Type[] { property.PropertyType });
        var ilGenerator = setterBuilder.GetILGenerator();

        ilGenerator.Emit(OpCodes.Ldarg_0);
        ilGenerator.Emit(OpCodes.Ldarg_1);

        // Build null check

        if (property.GetCustomAttribute<NotNullAttribute>() != null)
        {
            var isValueNull = ilGenerator.DefineLabel();

            ilGenerator.Emit(OpCodes.Dup);
            ilGenerator.Emit(OpCodes.Brtrue_S, isValueNull);
            ilGenerator.Emit(OpCodes.Pop);
            ilGenerator.Emit(OpCodes.Ldstr, property.Name);

            var argumentNullExceptionConstructor = typeof(ArgumentNullException).GetConstructor(new Type[] { typeof(string) });
            ilGenerator.Emit(OpCodes.Newobj, argumentNullExceptionConstructor);
            ilGenerator.Emit(OpCodes.Throw);

            ilGenerator.MarkLabel(isValueNull);
        }
        ilGenerator.Emit(OpCodes.Stfld, fieldBuilder);
        ilGenerator.Emit(OpCodes.Ret);

        return setterBuilder;
    }
}

Example

var person = ObjectFactory.CreateInstance<IPerson>();
//person.FirstName = null; // this throws ArgumentNullException
person.LastName = "Doe";
person.NickName = "JD";
person.Dump(); // InvalidOperationException appears in the dump
\$\endgroup\$
  • 1
    \$\begingroup\$ This feels like it wouldn't play well with the main reason for DTOs - serialization and deserialization. \$\endgroup\$ – Jesse C. Slicer Oct 10 '17 at 17:51
  • 1
    \$\begingroup\$ @JesseC.Slicer true, I probably should remove the DTO from the question as I didn't plan to use it for serialization... \$\endgroup\$ – t3chb0t Oct 10 '17 at 17:54
3
\$\begingroup\$

Cache the type so you don't build it each time. ConcurrentDictionary could do it. Just make a static field to hold the dictionary

private static ConcurrentDictionary<Type, Type> _typeMappings = new ConcurrentDictionary<Type, Type>();

Then you only need to change your CreateInstance method

var safeDto = _typeMappings.GetOrAdd(typeof (T), type =>
{
    var assemblyName = new AssemblyName($"DynamicAssembly_{Guid.NewGuid():N}");
    var assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName,
        AssemblyBuilderAccess.Run);
    var moduleBuilder = assemblyBuilder.DefineDynamicModule("DynamicModule");
    var typeName = $"{type.Name}_{Guid.NewGuid():N}";
    var typeBuilder = moduleBuilder.DefineType(typeName, TypeAttributes.Public);

    typeBuilder.AddInterfaceImplementation(type);

    var properties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public);

    BuildProperties(typeBuilder, properties);

   return typeBuilder.CreateType();
});
return (T)Activator.CreateInstance(safeDto);

Also your implementation is tightly coupled to this one attribute and if you wanted to add more abilities you would need to change the code. A lot of DI containers allow for this type of ability you are building. I'm most familiar with unity. They call it interception. You can see here an example of adding INotifyPropertyChanged and here is logging and caching. This is a bit different as they expect you to have a class and then using interception to extended it adding some cross cutting concerns. My IL is rusty, to say the least, so I'm not going to write a code example but you could have a list other classes that take the ILGenerator and check for it's own attribute and add the code they need and then chain it on to the next one. Then to add another attribute check you could just add to the list.

Instead of writing the check and throw calls in IL you could create a static method and just add the IL Call to that static method when needed. I've done it that way in the pass so the code is in normal C# and easier to maintain.

Side comment: I wouldn't create an interface for a DTO. Do you really need that interface or can you skip it and just create a class with no interface? Being I don't know the reason for the interface that's up to you but from my experience it's rarely needed to make an interface for a DTO and it's better to just pass the DTO

\$\endgroup\$
  • \$\begingroup\$ I've already had the same idea with the cache and actually edited the question yesterday ;-) but they are all interesting ideas, especially the link about interception and using static methods instead of generating everything manually. For now I think I try to use IL to better understand it but later definitely helper methods. Why do I use interfaces? I thought why should I create a class when an interface is all I need. I didn't want to implement the null check on all properties. It's too much work ;-] \$\endgroup\$ – t3chb0t Oct 11 '17 at 14:43
  • \$\begingroup\$ Currently I use it here for configuration properties to make sure they are all set. \$\endgroup\$ – t3chb0t Oct 11 '17 at 14:43

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.