6

I'm in the process of creating a new minor release of a toy project of mine. This project is released on NuGet and is compatible with .NET 4.0 and up. Some of the new features I'm introducing require .NET 4.5 (users should be able to resolve IReadOnlyCollection<T> and IReadOnlyList<T>, both interfaces that were introduced in .NET 4.5), but I need to keep the project compatible with .NET 4.0, since not all developers can easily migrate to the latest .NET framework.

So the problem I’m facing is how to solve this ‘forward-compatibility’ problem. There are two solutions I’ve thought about, but both are not very attractive, so hopefully anybody can give me some ideas or guidance here.

Here are the two solutions I came up with:

Solution 1: Use #if compiler directives and build a DLL per .NET framework version and ship those versions using the NuGet packages and download at the project site.

Downside of this method is that when developers update their Visual Studio project from .NET 4.0 to .NET 4.5, they don't automatically get the .NET 4.5 version (with .NET 4.5 specific features). This violates the Principle of least astonishment and would leave developers dazed why the feature is not working, when they try using it a few months later.

Solution 2: Use one single DLL and emit type's on the fly that implement both new interfaces when they exist in the current app domain. This allows shipping a single DLL to the user and allows features to come available when the developer switches .NET framework versions in their project. This will make things 'just work'. This is the direction I’m currently heading btw.

Since I need to return a type that needs to implement the interfaces, the downside is that that type must be created at runtime using Reflection.Emit, ModuleBuilder, TypeBuilder, and the like. This is seriously nasty shizzle. But besides that, since this type must be created in a new (anonymous) assembly, I must make some internal types public (a type it needs to inherit from and an interface it needs to implement). Making those internal types public pollutes the API of the project and will disallow me from making changes to those types.

I believe these are my options, but I might be missing something obvious. So my question is, am I missing a possibility? Is there a way to circumvent the problems for solution 1 or would it be better to go with the hardcore root of runtime type emitting?

Steven
  • 166,672
  • 24
  • 332
  • 435
  • 1
    json.net uses solution 1. – Daniel A. White Aug 01 '13 at 14:45
  • I don't think the first option flies in the face of the principle: why would I expect my third party references to be raped like that? I'd be astonished if they did! – Grant Thomas Aug 01 '13 at 14:46
  • Not sure if it really fits here or if it is even related here but does assembly redirection strike some chord? – Sandeep Singh Rawat Aug 01 '13 at 14:46
  • @Sandeep: I'm not sure. Can you explain (in an answer if you will)? – Steven Aug 01 '13 at 15:07
  • When dealing with new features that cannot be implement in previous versions of .NET Framework its important NOT to be fancy. The simple solution is to have two versions of the library itself. One will be compiled to support previous supported versions the other will support higher supporter versions. While I suppose you can use a solution like Dynamitey the question you have to ask yourself, is the added complexity actually worth it, is it really hard to release two different 1.4.0 library files? The users of your library should understand how to reference the correct version of the library. – Security Hound Aug 01 '13 at 15:36
  • @DanielA.White: What functionality does Json.NET add in its .NET 4.5 release? Ninject seems to have a 4.0 and 4.5 lib, but both dlls seem equivalent. – Steven Aug 01 '13 at 21:55
  • @Steven win8 support and likely async – Daniel A. White Aug 01 '13 at 22:20

2 Answers2

2

Have you thought about another custom assembly with the missing items in it? Then you test if a type/method exists (that would only exist in .net 4.5) and if it does, you load the assembly in.

That way you can keep the exact same methods and classes, and save yourself pain of doing all of that crazy emit (not to mention the perf hit you will take if you find yourself doing that much).

Steven
  • 166,672
  • 24
  • 332
  • 435
ferventcoder
  • 11,952
  • 3
  • 57
  • 90
  • Well, that's actually a really good idea. The only downside of this is having an extra assembly, but I might even place that assembly in a resource (evil grin). – Steven Aug 01 '13 at 15:10
  • I think in a resource might be a safer place to put it too. – jbtule Aug 01 '13 at 15:13
  • @jbtule, Although loading the assembly in a resource would nice, it is extremely nasty to do this, while using signed assemblies, because there is a dependency cycle between the assemblies. Core must be built before this new assembly (because it depends on core), but core needs to store that assembly in its resource. You see the problem? – Steven Aug 06 '13 at 11:25
  • True, but adding resources is one of Mono.Cecil's straight forward manipulations. – jbtule Aug 06 '13 at 12:18
1

I have a project called Dynamitey that allows you to load a type at runtime and called it's static methods and constructors with the DLR. Which would be much less messy than a lot of reflection or emitting code to load an api that is not necessarily available.

dynamic bigIntType = new DynamicObjects.LateType("System.Numerics.BigInteger, System.Numerics, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089");

if (bigIntType.IsAvailable)
{

  var one = bigIntType.@new(1);
  var two = bigIntType.@new(2);

  Assert.IsFalse(one.IsEven);
  Assert.AreEqual(true, two.IsEven);

  var tParsed = bigIntType.Parse("4");

  Assert.AreEqual(true, tParsed.IsEven);
}

I also have a project called ImpromptuInterface, that will emit proxy types for interfaces around objects that duck callable match it (also uses DLR).

var targetType =Type.GetType("System.Collections.Generic.IReadOnlyList`1[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089");

var list = new List<string>{"lala", "la","lala"};
object readonlyList;
if(targetType != null){
    readonlyList = Impromptu.DynamicActLike(list, targetType);
}
jbtule
  • 31,383
  • 12
  • 95
  • 128
  • Interesting tool, but for my project, I don't want to depend drag any external libraries in. – Steven Aug 01 '13 at 21:57
  • Yeah I think loading an assembly from an embedded resource is a better solution in your case. – jbtule Aug 01 '13 at 22:05