Restrictions on Contracts [Jack Gudenkauf]
The core foundation of the Managed Add-In Framework is a “Contract”. Contracts are non-versioning interfaces that define the protocol for communicating across the boundary between to the host and the add-in. In order for these contracts to be valuable for versioning they need to follow a set of constraints: at a high level they need to ensure that all types expressed within the contracts are safe, properly versionable, and able to pass across the isolation boundaries between hosts and add-ins.
For a complete description on the definition of a contract, please refer to our second CLR Inside Out MSDN Article.
The following is a set of high level guidelines for types appropriate in contracts. See the "Specific Constraints" section below for a detailed and complete list of allowable types in a contract.
o Other contracts
o Primitive types (int, bool, string)
o Serializable System Types
§ Any serializable type in mscorlib.dll that doesn’t hold references to types that are not serializable system types
§ Defined in mscorlib
§ Defined with contracts
o Simple serializable structs to compose primitives, strings, and/or contracts
§ That provide no implementation
§ Must be packaged *with* contracts
The following types are allowed to be expressed as input parameters or return values in contract definitions. We currently do not enforce these constraints, but we *strongly* recommend them. We are also looking into FxCop rules as a means to help you meet these constraints.
1. Other contracts
2. Serializable value types defined in the contract assembly.
3. Serializable value types in assemblies that can only be loaded once per AppDomain
a. We have listed out the only assemblies that fall into this category in the attached file (UnificationList.txt)
4. Sealed serializable reference types in assemblies in the attached file
6. Arrays of items in 2-6
a. The use of the array itself must be expected to have value semantics. Otherwise the System.AddIn.Contract.Collections.IListContract<C> should be used.
* All types in 2-6 fields must be constrained to 2-6. Feel free to read that one again ;-)
F.A.Q. on Contract Constraints
1. What’s up with all the entries in the list focusing on serializable types and why don’t we see MarshalByRefObjects in this list?
The main reason serializable types are generally appropriate to be passed across the boundary while MBRO’s are not is that with serializable types there is an opportunity to “fix-up” or adapt the type to the version that’s expected on the other side. In the .Net Framework serializable types can be built using a technology called Version Tolerant Serialization (VTS) that lets developers subscribe to events during the serialization process that lets them apply fix-ups if the source version of the type is different than the destination.
MarshalByRefObjects do not have this opportunity so if they change in some way between versions there is no way to apply fix-ups directly to the object. Instead you need to define a contract for those objects and use our contract-adapter-view model to apply any version to version fix-ups.
All serializable types in the .Net Framework take advantage of VTS and thus, as long as they conform to the other constraints listed here, are valid to be passed across the boundary. If you want to pass other serializable types you either need to ensure that they either use VTS to handle multiple versions or are defined in your contract (and thus are assured that the same version is loaded on both sides).
2. Why sealed serializable reference types and not all serializable reference types?
The reason serializable reference types need to be sealed to be passed across this boundary is that you want to be sure that the actual type you’re passing across really is serializable. In the CLR, serializable is not an inherited attribute, so sub-types of serializable reference types may or may not be serializable.
If you have a method in your contract that has a parameter typed as an unsealed serializable reference type of Foo, you would be safe as long as all callers into the contract passed in Foo’s. If a caller passed in type Bar (i.e., inheriting from Foo but is not serializable) everything would compile fine, but may fail at runtime.
3. Why are we limited to assemblies that can only be loaded once per AppDomain?
The CLR allows different versions of the same assembly to be loaded multiple times in the same AppDomain. When this happens, the types that come out of the two assemblies are distinct types and cannot be exchanged with one another. For example, if you try to pass an instance of type Foo from FooAssembly V1 to someone expecting Foo from FooAssembly V2, the runtime throws an InvalidCastException with a confusing message that type Foo cannot be cast to type Foo.
This means that if you built your application and contract against v2 of an assembly and the add-in was built against v1, if you tried to pass a serializable type from the application to the add-in both v1 and v2 would get loaded in the add-ins application domain and VTS would not kick in: rather the type will be serialized across as v2 and the add-in would get an InvalidCastException when it tried to use the type.
Unfortunately in the CLR there is no way to specify that a specific assembly should only be loaded once per AppDomain. The only types that get loaded with this restriction are those types included in something the CLR calls the unification list (attached). This list is essentially all the managed assemblies that ship as part of the .Net Framework. We actually created this list for a completely different reason but it has the effect of allowing you to use serializable types in this list in your contracts as long as they conform to our other constraints.
4. Why can’t we pass arrays of contracts?
When you serialize a type which holds references to MarshalByRefObjects (MBROs) the CLR will attempt to serialize the objects themselves rather than references to them. This means that if you were to serialize an array of MBROs, even MBROs typed as contracts, the runtime will attempt to serialize the instances across as their real types rather than pass them across by reference as their contract type.
This means that you cannot include arrays, or any of the BCL collections that are implemented using arrays, as part of your contracts and should instead type them as System.AddIn.Contract.IListContract<T>. We’ll go into more detail on this class and how to use it in future posts but you should know that to make this experience easier we have included helper classes that make it easy for your adapters to convert to and from IList<T> and IListContract<T>.