2
\$\begingroup\$

While implementing things like repositories we often can see that reading and writing parts can be done totally independently. The same time client side will win from the single reference (who wants to inject two parameters instead of one in many places). What do you think about a little bit of IL magic?

Let's say we have:

interface IDocumentReader
{
   Document ReadDocument(int id);
   IEnumerable<Document> ReadDocuments(DateTime date);
}  

interface IDocumentWriter
{
   void Write(Document document);
} 

Now magic:

[Mixin]
interface IDocumentRepository : IDocumentReader, IDocumentWriter
{
}

I configured my IoC container to generate and register implementations of mixin interfaces like:

class RTYRUYTRUYTRTYU23576475623 : IDocumentRepository 
{
   public RTYRUYTRUYTRTYU23576475623(IDocumentReader a1, IDocumentWriter a2)    
   {
     _a1 = a1;
     _a2 = a2;
   }

   Document ReadDocument(int id) => _a1.ReadDocument(id);
   IEnumerable<Document> ReadDocuments(DateTime date) => _a1.ReadDocuments(date);
   void Write(Document document) => _a2.Write(document);
}    

Now I can inject just IDocumentRepository.

Here comes library code. Attribute:

[AttributeUsage(AttributeTargets.Interface, AllowMultiple = false, Inherited = false)]
public class MixinAttribute : Attribute
{
}

Facade class we use to help IoC container to emit realization types for mixin interface registration:

public class Mixin
{
    readonly static ConcurrentDictionary<Type, Type> _map = 
        new ConcurrentDictionary<Type, Type>();

    public static Type Emit(Type mixinInterface)
    {
        Contract.Requires<ArgumentNullException>(mixinInterface != null);

        return _map.GetOrAdd(
            mixinInterface, 
            mi => new MixinFactory(mi).Emit());
    }

    public static object Create(Type mixinInterface, params object[] args)
    {
        Contract.Requires<ArgumentNullException>(mixinInterface != null);
        Contract.Requires<ArgumentNullException>(args != null);

        return Activator.CreateInstance(Emit(mixinInterface), args);
    }

    public static T Create<T>(params object[] args)
    {
        Contract.Requires<ArgumentNullException>(args != null);

        return (T)Create(typeof(T), args);
    }
}

Mixin type emitter (Well, not sure that implementation really looks readable - I hardly understand IL. But it works for method delegation :)

class MixinFactory
{
    public MixinFactory(Type mixinInterface)
    {
        Contract.Requires<ArgumentNullException>(mixinInterface != null);
        Contract.Requires<ArgumentException>(mixinInterface.IsInterface);

        MixinInterface = mixinInterface;
    }

    public Type Emit()
    {
        Contract.Ensures(Contract.Result<Type>() != null);

        var tb = TypeBuilder();
        var fbs = FieldBuilders(tb);
        DefineConstructor(tb, fbs);
        foreach (var fb in fbs)
            DelegateTo(tb, fb);

        return tb.CreateType();
    }

    Type MixinInterface { get; }

    IEnumerable<Type> MixedInterfaces => MixinInterface.GetInterfaces();

    void DefineConstructor(TypeBuilder tb, IEnumerable<FieldBuilder> fbs)
    {
        Contract.Requires<ArgumentNullException>(tb != null);
        Contract.Requires<ArgumentNullException>(fbs != null);

        ConstructorBuilder ctor = ConstructorBuilder(tb, fbs);
        ILGenerator il = ctor.GetILGenerator();

        // Call base constructor
        ConstructorInfo ci = tb.BaseType.GetConstructor(new Type[] { });
        il.Emit(OpCodes.Ldarg_0);
        il.Emit(OpCodes.Call, typeof(object).GetConstructor(new Type[0]));

        // Store type parameters in private fields
        for (ushort i = 0; i < fbs.Count(); i++)
        {
            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Ldarg, i + 1);
            il.Emit(OpCodes.Stfld, fbs.ElementAt(i));
        }
        il.Emit(OpCodes.Ret);
    }

    void DelegateTo(TypeBuilder tb, FieldInfo fi)
    {
        Contract.Requires<ArgumentNullException>(tb != null);
        Contract.Requires<ArgumentNullException>(fi != null);

        foreach (var mi in fi.FieldType.GetMethods())
        {
            var mb = tb.DefineMethod(
                mi.Name,
                mi.Attributes & (~MethodAttributes.Abstract), // Could not call absract method, so remove flag
                mi.ReturnType,
                mi.GetParameters().Select(p => p.ParameterType).ToArray());

            if (mi.IsGenericMethod)
            {
                var gas = mi
                    .GetGenericArguments();

                var gtpbs = mb.DefineGenericParameters(gas
                    .Select(t => t.Name)
                    .ToArray());

                for (int i = 0; i < gas.Length; i++)
                {
                    var ga = gas[i];
                    var gtpb = gtpbs[i];
                    gtpb.SetGenericParameterAttributes(ga.GenericParameterAttributes);                        
                    gtpb.SetInterfaceConstraints(ga.GetGenericParameterConstraints());                        
                }
            }       

            mb.SetReturnType(mi.ReturnType);                

            // Emit method body
            ILGenerator il = mb.GetILGenerator();
            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Ldfld, fi);

            // Call with same parameters
            for (int i = 0; i < mi.GetParameters().Length; i++)
                il.Emit(OpCodes.Ldarg, i + 1);

            il.Emit(OpCodes.Callvirt, mi);
            il.Emit(OpCodes.Ret);
        }
    }

    ConstructorBuilder ConstructorBuilder(TypeBuilder tb, IEnumerable<FieldBuilder> fbs) =>
        tb.DefineConstructor(
            MethodAttributes.Public,
            CallingConventions.Standard,
            fbs.Select(fb => fb.FieldType).ToArray());

    IEnumerable<FieldBuilder> FieldBuilders(TypeBuilder tb) => MixedInterfaces
        .Select((mi, i) => tb.DefineField(
            "_i" + i,
            mi,
            FieldAttributes.Private))
        .ToArray();

    TypeBuilder TypeBuilder() => ModuleBuilder()
        .DefineType(
            Guid.NewGuid().ToString(),
            TypeAttributes.Class | TypeAttributes.Public,
            typeof(object),
            new[] { MixinInterface });

    ModuleBuilder ModuleBuilder() => AssemblyBuilder()
        .DefineDynamicModule(Guid.NewGuid().ToString());

    AssemblyBuilder AssemblyBuilder() => AppDomain.CurrentDomain.DefineDynamicAssembly(
            new AssemblyName(
                Guid.NewGuid().ToString()),
                AssemblyBuilderAccess.RunAndSave);
}
\$\endgroup\$
7
  • \$\begingroup\$ I must be missing something: what is the need for the Mixin attribute? Can't your document repository implement IDocumentRepository. Classes that read take IDocumentReader. Classes that write take IDocumentWriter. Classes that do both take IDocumentRepository. For all three classes, can't you simply pass your DocumentRepository instance? Is the problem that since you're using an IoC Container, that you have to register your DocumentRepository instance three times? Is that how the container works? \$\endgroup\$ Commented Jan 28, 2016 at 15:11
  • \$\begingroup\$ The problem is that implementation of reader and writer has nothing in common. Good design is to implement them independently - not as repository with reading and writing functionality combined. The same time client code has different preferences, it might want to get both through all inclusive repository abstraction as a single reference. There is no multiple inheritance in .NET, so assembling desired repository from reader and writer requires some tricks. See above. \$\endgroup\$ Commented Jan 29, 2016 at 8:44
  • \$\begingroup\$ I see the real problem. Don't split your repository into reading and writing. Classes are not verbs, they're nouns. Methods are verbs. Say you have a class User. It'll have methods like Login(string username, string password) and Logout(). Both of these will need to both read from and write to your repository, so your User class will need something injected in that will handle this. Inject an IUserRepository rather than an IDocumentReader and an IDocumentWriter. You should rethink your design so that you don't have to rely on "tricks". \$\endgroup\$ Commented Jan 29, 2016 at 14:59
  • \$\begingroup\$ Simultaneous implementation of reader and writer has low cohesion; they are totally unrelated. By placing them in one class we will have high coupling. The closest analogy in .NET for situation like this is to have three classes: Reader, Writer, and Serializer façade for previous two, which is written manually. In my case manual implementation is not necessary. It is a pure technical issue, which can be easily solved by technical means. Advanced solution is nothing bad by itself as long as it is easy to consume it. \$\endgroup\$ Commented Jan 29, 2016 at 18:41
  • \$\begingroup\$ @DmitryNogin: What data fields would your Reader and Writer have? Probably some sort of connection information if it is persisting to an external database, right? Or some sort of file system access information if they are reading and writing from the file system. Both read and write operations would use these same fields, which is the very definition of high cohesion. Consider the System.IO.Stream class, it is capable of both reading and writing. \$\endgroup\$
    – lenkan
    Commented Apr 9, 2016 at 10:00

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Browse other questions tagged or ask your own question.