TechEd 2008 notes: Managed Add-In Framework
This looks like a lot of work, but that’s because they’re solving a hard problem (both upward and downward compatibility). Don’t be scared off by the amount of code, though — lower down I have info about a tool they wrote that autogenerates some of the boilerplate code.
Still, it’s a lot of work, and it would take time to wrap your head around it. If all you want to do is delay-load parts of your application, the add-in framework is probably far, far more than you need. It’s for real add-in frameworks, where other people will write add-ins, and the add-ins will be on a different release schedule than your app is (so you need forward and/or backward compatibility).
Migrating Extensibility to the Managed Add-In Framework: Using System.AddIn to Find and Activate Add-Ins in 3 Lines of Code
Jesse Kaplan
Program Manager
Microsoft
Side note: the add-in framework is .NET 3.5 only. So we can’t use it until we drop support for Windows 2000.
Session Objectives & Agenda
- Intro to extensibility and System.AddIn
- Hosting in 3 Lines
- Keeping new host compatible with older add-ins
- Migrating to System.AddIn
Why Extensibility
- Let users or third parties add features to your app
- Turn your app into its own platform. Makes users tied to your app.
- Unexpected benefit: Componentize your app. Disconnected update cycles.
Types of Extensibility
- Add-Ins can provide a service to a host
- Browser content add-ins (Flash, Silverlight, PDF)
- File-type handlers
- Media Player: standalone app, often hosted in other apps
- Host can provide services to add-ins
- Large application that plug-ins can hook into
- Classic automation add-ins
- Office style extensibility
“Version 1″ problems
- Discovery. How do you find the add-ins?
- Activation
- Isolation. How do you put it in an AppDomain or a different process, and still communicate with it?
- Lifetime management
- Sandboxing
- Unloading
“Version 2″ problems: Problems you don’t know you have until V2. These are really the reason Microsoft developed the add-in framework.
- Backward compatibility. How do you keep old add-ins working as you change your host?
- Forward compatibility. How do you let newer add-ins work in older apps?
- Adding new isolation levels. What if it used to be AppDomain isolation, and you decide to move it to process isolation?
What is System.AddIn?
- New set of assemblies in .NetFX 3.5
- System.AddIn
- System.AddIn.Contract
- Solves “V1″ problems
- Architecture that makes building the next version as painless as possible
Guiding principles
- “My” types are never loaded into “your” domain
- Hosts and add-ins can version independently
- Make it easy and natural to build your V1 without closing the doors on V2
Three Developers
- Host developer
- Add-In developer
- Object Model (OM) developer: the one who designs the object model that host and add-in communicate through. Often the same person as the Host developer.
Host Developer Experience
- Hosting add-ins with three lines of code
- Communicate with add-in as if it were a local object
- Install:
AddInStore.Update(path) - Find:
IList<AddInToken> tokens = AddInStore.FindAddIns(typeof(IMyAddIn), path) - It doesn’t load add-ins or execute any code to return this list.
- Activate:
IMyAddIn addIn = token.Activate(AddInSecurityLevel.Internet);
Add-In Developer Experience
- Like the boundary wasn’t even there
- Implement the interface:
MyAddIn: IMyAddIn {} - Name your Add-In by adding an attribute to the class:
[AddIn("My First Add-In")]
Host code:
using System.AddIn.Hosting; ... AddInStore.Update(PipelineStoreLocation.ApplicationBase); IList<AddInToken> tokens = AddInStore.FindAddIns(typeof(ICalcAddIn), PipelineStoreLocation.ApplicationBase); ICalcAddIn calc = tokens[0].Activate<ICalcAddIn>(AddInSecurityLevel.Internet); RunCalculator(calc);
Add-in code:
[AddIn("MyAddIn")]
public class MyAddIn: ICalcAddIn
{
...
}
To run it in a separate process:
ICalcAddIn calc = tokens[0].Activate<ICalcAddIn>(new AddInProcess(), AddInSecurityLevel.Internet);
Automatically unloads AppDomains and tears down processes as needed. (Can override this for processes, if you want to reuse it.)
No special issues with multithreading.
How many add-in processes are there? A: One per AddInProcess instance you create. Can also use AddInController to query which AddInProcess was used for a given AddIn, so wouldn’t necessarily even need to keep your own reference.
You can choose to activate in the current AppDomain (use the overload that takes an AppDomain), though there are reasons not to do that.
If you use processes, and the host has threads, how does that work? A: Uses standard remoting, which starts a thread for each request. This means you can’t be an STA thread.
The Architecture
- Letting you build the right abstraction layers right from the start
- Change object model without breaking add-ins
First cut at the architecture
- “View” (object-model assembly)
- Host and AddIn assemblies both consume that same assembly
- Problem: If you change your interface, you have to rename it. Maybe you can upcast and downcast. Maybe you need to add assemblies.
- Lots of upcasting and downcasting, type checks, etc.
- Once you cross an isolation boundary, both host and add-in have to deal with MarshalByRefObject lifetime management.
The Butterfly Architecture: Separate the views and fill in the pipeline
- Host has host’s view (static dependency)
- AddIn has addin’s view
- Contract assembly has another definition of all assemblies, but it’s the only one whose types cross the isolation boundary
- Adapters on both sides that adapt from the contract to/from the host and addin views
- Contract is a contract between the adapters, not between the host and add-in
- You can replace the center, or pieces of the center, without the host or the add-in even being aware of it
Side note: After initiation, there’s not much difference between host and add-in.
Host and add-in can do events back and forth.
- Host’s view has an interface with some methods. Just a local interface definition.
- Add-in’s view looks the same, but has an
[AddInBase]attribute. Not necessary for host view, because it’s passed to the add-in mangaer directly, so we know what it is.
Contracts
- [AddInContract]
- Implements IContract
- Then the same methods again
View-to-contract adapter
- [AddInAdapter]
- Constructor takes an IFoo parameter
- Descends from ContractBase
- Implements IFooContract
- Delegates methods to the add-in that was passed to its constructor
Contract-to-view adapter
- [HostAdapter]
- Constructor takes IFooContract, and creates a new ContractHandle on that contract.
- The ContractHandle is the lifetime management. You don’t use it for anything; you just hang onto it.
- Implements IFoo
Host V2
- Old addin is based on addin view V1.
- Reuse host V2, host view V2, and host side adapter V2.
- Write an add-in side adapter that converts add-in view V1 to contract V2.
- When you want to stop supporting those old add-ins, just stop building the cross-version adapter assembly.
Typical use: in v1, you dictate which services your add-in will provide. In v2, you open it up so the add-in can provide other sorts of services as well.
Side note: when they’re reading metadata, they read the assemblies as byte arrays and parse the metadata directly. So there’s no chance of executing any code during that discovery process.
Good news: For many cases, they can autogenerate some code to build all this adapter stuff for you.
Migrating to System.AddIn
- Break compatibility. Old add-ins won’t work.
- Dual-mode extensibility. App can talk to old add-ins through its old APIs, and new ones through System.AddIn.
- Turn old interface into a view.
Can your legacy object model (OM) be a System.AddIn OM?
- System.AddIn restrictions are really just isolatable OM restrictions
- If you already isolate, should be smooth
- Interop assemblies already conform to these rules
- Their blog has details
Three mechanics for backwards compatibility (if you are able to migrate)
- Retain your existing discovery system
- Use FindAddIn (as opposed to FindAddIns) to bridge the gap. Instead of a directory name, it takes an assembly filename and some type info. So they don’t need the AddIn attribute.
- Add the [AddInBase] attribute to your legacy object model. Otherwise leave the assembly the same, including assembly version. (File version can change.)
- Build your adapter.
Other Add-In Features
- Displaying UI across isolation boundaries
- Your clicks actually go straight into the add-in AppDomain
- Relies on WPF to do the magic (working to see if it can be done all WinForms in the future)
- Automatic pipeline generation
- Meant for v1. Doesn’t know how to do V1-V2 adapters.
- Generates adapter source code for you, and adds projects as needed
- This isn’t part of a release; it’s a project on CodePlex
- Library exposed both as VS add-in and as command-line tool
Resources
Custom exception types: Possible to do, but more difficult. Recommend that you stick with system exception types and pass data to them. If you need to do your own exceptions, it’s best for your adapter to catch and adapt the exception, so you’re never loading their types into your appdomain.
After the session, I asked whether the DLR and duck typing, e.g. with IronRuby, would make some of this easier, because then (assuming your duck typing was forward and backward compatible) you could get rid of a lot of the intermediate translation layers. He said there were some people currently working to figure out how to do cross-AppDomain DLR calls. So no, at this point, that wouldn’t make it any easier. Too bad.
June 9th, 2008 at 7:25 am
[…] Joe White has posted very detailed notes from TechEd 2008: #1 ~ #2 ~ #3 ~ #4 ~ #5 ~ #6 ~ #7 ~ #8 ~ #9 ~ #10 […]