Add section for multiple framework references (dotnet/core-setup#4374)
authorSteve Harter <steveharter@users.noreply.github.com>
Tue, 31 Jul 2018 16:02:23 +0000 (11:02 -0500)
committerGitHub <noreply@github.com>
Tue, 31 Jul 2018 16:02:23 +0000 (11:02 -0500)
Design work is still ongoing so this will require another pass. We may want to separate frameworks vs. framework extensions

Commit migrated from https://github.com/dotnet/core-setup/commit/1f45274204b8b9ebc60115c37165437567453335

docs/installer/design-docs/assembly-conflict-resolution.md
docs/installer/design-docs/multilevel-sharedfx-lookup.md

index 70560ab..530ea3b 100644 (file)
@@ -51,7 +51,7 @@ The order in which each layer's deps.json is processed is:
 
 Note that for an app, its probing path comes *after* the framework's, so intuitively it would appear that "framework wins" in collisions. However, because the app's deps.json is parsed *before* the framework's deps.json and because the app will likely reference an OOB package that the framework doesn't (because a framework, at least Microsoft.NETCore.App, has its own metapackage and does not reference OOB packages), the framework probing path never matches up in step 4 for the app's deps.json package\assembly entry, so it goes to the next probing path which is the app's and because the package matches the "app wins".
 
-## Proposed changes for 2.1
+## Changes for 2.1+
 Probe the app location before the framework's. This means flip (3) and (4) under **Probe Ordering** above and treat the app as the highest-level framework. The reason is that there may be frameworks that use OOB packages like apps, and we want to have "app wins" in non roll-forward cases.
 
 Replace step 3 under **Algorithm** above with:
index c95a295..e17e25c 100644 (file)
@@ -65,7 +65,7 @@ Hostfxr must then locate the hostpolicy.dll file:
 
 The hostpolicy is then loaded into memory and executed.
 
-### Proposed hostfxr changes for 2.1 (and 2.0.x long-term-servicing)
+### Changes for 2.1 (support chained frameworks)
 There can only be one framework in 2.0. That framework is located in the app's runtimeconfig.json:
 ```javascript
 {
@@ -87,7 +87,7 @@ For 2.1, a given framework can only depend upon another single framework. An app
 
 Each framework has its own roll-forward semantics. This means ASP.NET can roll-forward independently of NETCore.App even though ASP.NET depends upon the NETCore.App framework.
 
-NETCore.App in 2.0 has its own deps.json file in its own folder that lists its assemblies. In 2.1, other frameworks will also have their own deps.jon. In addition, each framework has an optional runtimeconfig.json that describes its framework dependency including optional setting overrides (applyPatches, preReleaseRollForward, rollForwardOnNoCandidateFx). If the runtimeconfig.json file does not exist, or does not have a value for a setting, it uses the value from the higher-level runtimeconfig.json.
+NETCore.App in 2.0 has its own deps.json file in its own folder that lists its assemblies. In 2.1, other frameworks will also have their own deps.json. In addition, each framework has an optional runtimeconfig.json that describes its framework dependency including optional setting overrides (applyPatches, rollForwardOnNoCandidateFx). If the runtimeconfig.json file does not exist, or does not have a value for a setting, it uses the values from the app's runtimeconfig.json or from environment variables.
 
 For example, an MVC app's runtimeconfig.json would contain:
 ```javascript
@@ -105,6 +105,93 @@ and Microsoft.AspNetCore.App's runtimeconfig.json would contain:
 ```
 and Microsoft.NETCore.App would not have a runtimeconfig.json because it doesn't have any framework dependency or need to change settings.
 
+### Proposed changes for 3.0 (specifying multiple frameworks)
+The 2.1 release added support for a chain of frameworks, where each framework can have one dependent framework. However, with the advent of frameworks for WPF and WinForms it becomes necessary for an application to be able to reference more than one dependent framework.
+
+The runtimeconfig.json will have a new `frameworks` array section that allows more than one framework to be specified:
+```javascript
+"runtimeOptions": {
+       "rollForwardOnNoCandidateFx" : 1,
+       "applyPatches" : true,
+       "frameworks": [
+                       {
+                                       "name": "Microsoft.AspNetCore.All",
+                                       "version": "3.0.0"
+                       },
+                       {
+                                       "name": "Microsoft.Forms",
+                                       "version": "3.0.0"
+                                       "rollForwardOnNoCandidateFx" : 1
+                                       "applyPatches" : true
+                       }
+       ]
+}
+```
+
+If an entry also exists in the `framework` section, it is treated as the first element in the `frameworks` array. Thus the `framework` section is no longer required but is supported for backwards compatibility.
+
+The `applyPatches` and `rollForwardOnNoCandidateFx` continue to be supported globally in the `runtimeOptions` section, but can now also be specified individually for each framework. These per-framework settings override any corresponding values in the `runtimeOptions` section.
+
+By allowing more than one framework reference, we may encounter issues with multiple references to the same framework but with different versions or with different roll-forward settings. The rules to reconcile that include:
+- All existing roll-forward rules are applied to each reference to a framework individually, respecting each `version`, `applyPatches` and `rollForwardOnNoCandidateFx` value.
+- The *most restrictive* value of every `applyPatches` and `rollForwardOnNoCandidateFx` entry are used when resolving a given framework:
+  - `applyPatches` `false` is more restrictive than `true`
+  - `rollForwardOnNoCandidateFx` `0` (no roll-forward) is more restrictive than `1` (Patch and Minor) or `2` (Patch, Minor and Major).
+  - `rollForwardOnNoCandidateFx` `1` is more restrictive than `2`.
+  - Note that if there are no explicit values for `rollForwardOnNoCandidateFx`, then the environment variable `DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX` is used (there is no environment variable for `applyPatches`). If there is no environment or config settings, then the default values are used: `applyPatches=true`, and `rollForwardOnNoCandidateFx=1`.
+- The highest `version` value of a given framework is selected.
+
+So, for example, if there are two references:
+- `Foo 2.1.0` with `rollForwardOnNoCandidateFx=0`
+- `Foo 2.2.0` with `rollForwardOnNoCandidateFx=1`
+
+then that will always fail and result in a framework not found error. This example fails because `2.1.0` does not allow roll-forward on Minor (`rollForwardOnNoCandidateFx=0`) and because of the specified version `2.2.0`.
+
+#### Best practices for a runtimeconfig.json:
+- <B>No Restrictive Roll-Forward Overrides:</B> do not specify `applyPatches` and `rollForwardOnNoCandidateFx` in the runtimeconfig.json unless absolutely necessary. These should only be considered to work around issues in the field, by the end user, and not set by default by any framework.
+ - The rollForwardOnNoCandidateFx can also be controlled by environment variables, it should be very rare that we need to override these in the runtimeconfig.json, and especially at a per-framework level.
+ - The one exception to this is to use a *less restrictive* setting by specifying `rollForwardOnNoCandidateFx=2` which allows roll-forward by Major (in addition to Minor and Patch). The default value is `1` (Minor \ Patch only).
+- <B>No Redundant References:</B> when a given framework "foo" ships it should not create a case of having more than one reference to the another framework "bar". The reason is that base frameworks already specify "bar" so there is no reason to re-specify it. However, there are potential valid reasons to re-specify the framework:
+       - To force a newer version of a given framework which is referenced by lower-level frameworks. However assuming first-party frameworks are coordinated, this reason should not be exist for first-party runtimeconfig.json files.
+       - To be redundant if there are several "smaller" or "optional" frameworks being used and no guarantee that a base framework will always reference the smaller frameworks over time.
+       - To provide a hint of the newest framework version. This would likely only be in the app's runtimeconfig.json, and during roll-forward scenarios. This would be used to prevent re-resolving the frameworks (finding the most compatible framework on disk) which can happen when a lower-level framework requires a newer version of a another framework that was already resolved. By providing the hint at a higher-level, the correct framework version will be found the first time.
+- <B>No Circular References:</B> there should not be any circular dependencies between frameworks.
+  - It is not normally a desirable design for the same reasons why circular references in assemblies and packages are not supported or supported well (chicken-egg creation, simultaneous version changes).
+  - One potential future case is to allow "pseudo-circular" dependencies where framework "foo" loads a light-up framework which depends on "foo". Internally the foo->lightup reference may be treated as a late-bound framework reference, thus causing a cycle. This potential feature may replace the "additional deps" feature in a way that allows for richer light-up scenarios by allowing the lightup to specify framework dependency(s) and have a small deps.json.
+- <B>No Downgrading:</B> a newer version of a shared framework should keep or increase the version to another shared framework (never decrease the version number).
+By following these best practices we have optimal run-time performance (less processing and probing) and less chance of incompatible framework references.
+
+#### Algorithm
+Terminology:
+- `config list`: entries for a single runtimeconfig.json which consists of framework `name`, `version`, optional `applyPatches`, and optional `rollForwardOnNoCandidateFx`.
+- `newest list`: entries keyed off of framework name that contain the highest framework version requested. It is used to perform "soft" roll-forwards to compatible references of the same framework name without reading the disk or performing excessive re-try (Step 7).
+- `resolved list`: a list of frameworks that have been resolved, meaning a compatible framework was found on disk.
+
+Algorithm:
+1. Determine the `config list`:
+  - Parse the application's runtimeconfig.json `runtimeOptions.frameworks` section.
+  - If the `runtimeOptions.framework.name` and `runtimeOptions.framework.version` exist, Then insert that framework into the beginning of the `config list`.
+2. For each framework in `config list`:
+3. --> If the framework is not currently in the `newest list` list Then add it.
+  - By doing this here, before the next loop, we minimize the number of re-try attempts.
+4. For each framework in `config list`:
+5. --> If the framework is not in `resolved list` Then resolve the framework
+ - Use the framework version from `newest list` if newer than the reference, otherwise update `newest list` if reference is newer.
+ - We may fail here if not compatible.
+ - Probe for the framework on disk
+ - If success add it to `resolved list` and make a recursive call back to Step 2 but pass in a new `config list` based upon the values from the newly resolved framework's runtimeconfig.json which may reference additional frameworks.
+6. --> ElseIf the version is < resolved version  Then perform a "soft" roll-forward.
+  - We may fail here if not compatible.
+7. --> Else re-start the algorithm (goto Step 1) with new \ clear state except for `newest list` so we attempt to use the newer version next time.
+
+This algorithm for resolving the various framework references assumes the <B>>No Downgrading</B> best practice explained above in order to prevent loading a newer version of a framework than necessary.
+
+#### Discussion points:
+- By choosing the "most restrictive" values for `applyPatches` and `rollForwardOnNoCandidateFx` we limit what changes the app developer can do to work around issues without being forced to modify the framework's runtimeconfig.json files.
+  - For example, if a framework "foo" depends on framework "bar" version 2.0.0. with an explicit framework setting of `rollForwardOnNoCandidateFx=0` and only 2.1.0 is installed, a framework load error will occur at runtime and the app developer will not be able to force 2.1.0 to be loaded (without modifying the framework's runtimeconfig.json file).
+       - According to the best practice "No Restrictive Roll-Forward Overrides", the framework reference to "bar" should not specify `rollForwardOnNoCandidateFx=0`, and thus we would not encounter this issue.
+- If we expect this feature to be used to create several smaller-grain or "optional" frameworks, we may want to add a concept of a "private" framework reference so that lower-level references to these optional frameworks are not automatically "lifted" to the app level. This would help with forward-compatibility if lower-level frameworks remove a reference to optional framework, because the app would have its own reference to the optional framework.
+
 ## Hostpolicy
 
 Hostpolicy is in charge of looking for all dependencies files required for the application. That includes the coreclr.dll file which is necessary to run it.
@@ -118,7 +205,7 @@ Both files carry the filenames for dependencies that must be found. They can be
 
 At last, the coreclr is loaded into memory and called to run the application.
 
-### Hostpolicy changes for 2.1
+### Hostpolicy changes for 2.1+
 For 2.0, there are several probing paths that are used to find the dependencies. These paths follow a certain order and the first assembly found wins and that location will be passed to the coreclr. For example, the local app location has priority over the shared framework locations and if the same assembly exists in both locations, the coreclr will end up using the local app's copy of that assembly.
 
 These semantics will be unchanged for 2.1 except when a roll-forward is performed at a non-patch version (meaning a change to the major or minor version). For these cases, the highest assembly version wins. This is necessary in run-time scenarios to prevent assembly load exceptions which occur when an assembly is referencing a higher version of another assembly, but a lower version is actually found. This situation of having assembly conflicts (or duplicates) is more likely to occur when there are multiple frameworks (as convered in hostfxr's changes for 2.1), so it is important for 2.1+ functionality.