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);
}
asked Jan 28, 2016 at 5:08
\$\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\$ Commented Apr 9, 2016 at 10:00

0

Know someone who can answer? Share a link to this question via email, Twitter, or Facebook.

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.