Using Add-Ins with a ClickOnce Deployed Application

One of the attendees at the PDC had an interesting question combining ClickOnce and Add-Ins.  Basically, his application was being deployed with ClickOnce, and was running without elevating it's privileges beyond the Internet zone [fan-tastic :-)].  The problem is that the application was extensible with AddIns, and the identity of these AddIns were not known at deployment time.

The obvious solution is to simply deploy the AddIns with the application, and late bind to them.  However since ClickOnce requires that all of the files being deployed be listed in the application manifest, obviously not knowing the identity of the AddIns until runtime prevents this solution from working.

This leaves two viable options, using Assembly.LoadFrom(string url) to load the assemblies off of the deployment server at runtime, or creating a cache of assemblies in Isolated Storage and loading them using Assembly.Load(byte[]).  There are trade offs to using these two techniques, so lets look at each individually.

First, a limitation that applies to both of them.  Running with the Internet permission set is going to require that the location of the AddIn assemblies be the deployment server (since you'll only have WebPermission back to the site of origin).  If that's not a possibility, you'll need to add WebPermission to the server holding the AddIn assemblies to your manifest, which will end up causing your users to get a ClickOnce trust prompt.

Using LoadFrom(url) is going to be the easiest option, as Fusion will simply connect to the server and obtain your assembly for you, caching it locally.  It will also automatically pick up updates to the AddIn when they are detected.  However, you run into the potential problems with using the LoadFrom context. On the plus side, you won't have any limitation on the total size of the Add-In assemblies.

As an alternative you could connect to the AddIn distribution server yourself, which allows you to implement your own downloading, caching, and updating policies.  However, this method also requires that you implement your own download, caching, and updating policies :-).  If you want this level of control, downloading the assemblies into Isolated Storage, and then loading them with Load(byte[]) is your best bet.  Using application scoped Isolated Storage for the cache means that ClickOnce will migrate your AddIns forward as you provide upgrades to your application.  And since you're using Assembly.Load instead of Assembly.LoadFrom, you end up using the standard Load context.

The major limitation here will be your Isolated Storage quota, which for the Internet zone is limited to approximately 500 kilobytes.  That 500k needs to be shared among all the cached AddIns and any additional streams that your application creates in IsoStore.  If that won't be enough space, then you've got a few options.  First you could simply not cache the assemblies, and just read them from the server every time (perhaps relying on a proxy to cache for you), and passing the read bytes directly to Assembly.Load().  Obviously this method is going to result in potentially unacceptable load times for AddIns, especially if they're contained in large assemblies or your connection to the server housing them is slow.

A second alternative is to simply use an aggressive caching cleaning policy -- simply clear out older AddIns when a new one becomes available that won't fit in the remaining IsoStore space.  This method will work relatively well, though depending on your cleanup policy, you may end up causing performance problems for your users again.

Finally, you could simply let the user manage the Add-In cache.  Depending on how fancy you want to get, you could give them UI to mark certain AddIns as "never cache" or "always cache".  Providing a view of what is already in the IsoStore cache, and the ability to remove specific AddIns would be useful as well.  With that infrastructure in place, when you download an assembly that won't fit into your IsoStore cache, you could prompt the user and give them the option to not cache this assembly or to remove some other assemblies from the cache to make room.

Basically, the answer is that this scenario will absolutely work, although you may have to do some work in order to get it up and running.  Here's a quick rundown of the pros and cons of each method:

LoadFrom(url) Load(byte[])
Pros
  • Fusion cache management
  • Fusion picks up updates automatically
  • Quickest method to get running
  • No built in size limitations on the AddIns.
  • Custom download, cache, and update management
  • ClickOnce migrates cache up to new versions of the application as they become available
Cons
  • AddIns must be on deployment server
  • Assemblies end up in the LoadFrom context
  • You don't own the cache policy, so assemblies may be removed when you don't want them to be
  • AddIns must be on deployment server
  • More work -- custom cache, download, and updating logic must be written.  Cache clean options may also need to be written to present to the user
  • Limited cache space -- ~500 kilobytes for your AddIn cache and other Isolated Storage to share

[Updated 11/4/2005: Removed wrong load context]