A recent post on the .NET Framework blog is titled “Leveraging existing code across .NET platforms.” Code portability is a favorite topic of mine, although I’d missed their earlier announcement of a new portability analyzer. The tool, the .NET Portability Analyzer, or apiport.exe, can be used from the command line and is also now integrated into Visual Studio.
I’ve done several ports of .NET code for the DevForce framework, the first from .NET 3.5 to Silverlight 2, and more recently .NET 4.5 to Windows 8.x and Phone 8.0. The usual approach was to create a new project for the targeted platform and then see how large the explosion was when trying to compile. The analyzer saves you from this preliminary work and quickly scans your assemblies, creating a detailed report on API differences along with a few recommendations.
I thought it would be interesting to run the analyzer on the DevForce “client” assemblies. We ported these assemblies to other platforms the old fashioned way, so I’d see how thorough the tool is. As it turns out, it’s very thorough, quite fast, and easy to use.
|What’s shown here should not be construed as a DevForce product roadmap.|
The summary, while interesting, is not all that helpful, but I’ll get to the detail in a moment. Here I included all possible platforms the analyzer supports, although when porting your own assemblies you’re probably not interested in all possible targets, nor in porting to all of them at once. You might also find you’ll want to port from, say, full .NET to Windows 8.1, and then from there to a Xamarin platform. It’s usually easier to port from an assembly which is already focused on a mobile, “reduced” .NET API, and the API gap may be less daunting.
The “IdeaBlade.Core” assembly has the lowest API compatibility numbers in the list above, so I wanted to look at what the analyzer found. As the name of this assembly might imply, it’s responsible for a number of lower-level features, including configuration, reflection, WCF, MEF, registry access, some file I/O and EventLog access, a bit of remoting, use of the Cryptographic API, and so on.
Running apiport.exe from the command line generates Excel output, which is much more useful if you do plan on acting on the information, but I like the red light / green light look of the icons from the output generated within VS, so that’s what I show here. Here’s a snip of the detail view.
You’ll see long swaths of red for unsupported features in a specific platform, along with recommendations. For example, here System.Type, along with much of the Reflection API, is radically different in Windows 8 and Windows Phone 8.1.
What? No WCF support in Phone 8.1!
Once you know what’s missing, the question then turns to how to mitigate the differences. I’ve found that there doesn’t seem to be one single best practice here, and developer and team productivity should trump any search for architectural purity. If a developer doesn’t know, and can’t easily discover, that a certain piece of code is used differently for different platforms, then your team will waste time with broken builds and worse.
If you aren’t doing continuous integration, now is also the time to start. You’ll need to port your test suite to the new platform(s) too. With the increased testing/build workload, team members may sometimes cut corners in rushed situations, and automatic builds and tests will truly be a lifesaver.
Especially when porting from full .NET, first look at reducing or eliminating functionality. Your tablet or mobile app can’t, and shouldn’t even need to, use the registry, a .config file, write to the console, etc. You can often just not include those files containing unnecessary functionality in the target project. Problem solved, unless internal politics or your customers take issue.
You should also usually avoid the temptation to “roll your own” for features missing in the platform. You really won’t miss things like the PropertyDescriptor, so apply the YAGNI principle with rigor.
You might also find that some features of your application may really be “desktop only” or “server only” features. If possible, it could be a good time to restructure and refactor any assembly such as this into multiple assemblies, leaving remaining “client” or “common” functionality easier to port. In fact, I wish we’d done this with IdeaBlade.Core.
Next, you’ll find some types supported across platforms, but not all the methods or overloads you might be used to. Or maybe there’s some slightly different way of accomplishing the same thing. Here the analyzer’s recommended changes can help. It’s usually easy enough to use Dispose rather than Close on a TextWriter; List<T> instead of ArrayList; or a different but functionally equivalent constructor or method overload. Having common code, rather than lots of platform-specific code, is much easier to maintain.
Speaking of the TextWriter and I/O in general, it could also be a good time to refactor for async, and possibly drop sync usage where possible. All the listed platforms support async-await (SL5 requires a compatibility pack).
When you do need platform-specific code, a very common approach is of course the use of compiler directives. These do have their place, but can also be overused, especially as the number of supported platforms grows. Do you really want to maintain code like this?
#if NET // do some .NET thing #elif SILVERLIGHT // do some SL thing #elif WINDOWS_APP // and so on #elif WINDOWS_PHONE_APP // #elif ANDROID // #endif
#if NET || ANDROID // some cool thing #elif SILVERLIGHT && !WINDOWS_PHONE // something else #elif NETFX_CORE || WINDOWS_PHONE // and so on #endif
Other than compiler directives, what else can you do?
- When entire classes will be wildly different across platforms, use interfaces and custom implementations for each platform.
- When some code is common, you can use abstract base classes with subtyping by platform.
- If only bits and pieces of a class will be platform-specific, you can make your classes partial and refactor platform functionality into functions for which you can use partial methods. Partial methods don’t seem to be used much, but they’re handy, and can be defined for static methods too.
- Extension methods can be useful too, with the platform-specific functionality in separate extension classes. You can also use extension methods to add functionality that you think is “missing” from one platform. This lets calling code use a common API fairly seamlessly.
- You can also use the adapter pattern, which works well for static functions. For example, a “TaskAdapter” can wrap the static methods of TaskEx in SL, and Task everywhere else.
- What about “missing” interfaces and attributes? For example, ICloneable is “missing” on most of the platforms, but if you’ve got a lot of code that wants to Clone, defining the interface yourself is an option. The same with attributes, especially those that only define simple types and contain no logic or behavior. If you have a lot of code already decorated with “missing” attributes and don’t want to wrap compiler directives around every single one, creating your own implementation of the attribute is an option.
- With all of the above, the next question might be whether to use separate files for each platform, and what naming scheme if so, or whether to continue using a single file with compiler directives separating content. I don’t have a good answer for this, although I lean toward using a single file with compiler directives, since it’s then obvious to developers working in these files where the platform-specific code is located.
Finally, one important question when looking at porting is whether a native assembly is truly the right approach, or should you take the plunge with a portable class library? The tooling around PCL is much improved, although profile-specific documentation is lagging. Porting a .NET library to a PCL, of any target profile, can be painful, and the analyzer doesn’t (yet?) handle this, possibly because the capabilities of each PCL profile are something you have to discover for yourself as a kind of initiation rite. A PCL can sometimes mean taking a lowest common denominator approach, and you might lose functionality. Nevertheless, “write once, run everywhere” is a worthy, if still somewhat elusive, goal.