aboutsummaryrefslogtreecommitdiff
path: root/third-party/DotNetCorePlugins
diff options
context:
space:
mode:
authorLibravatar vnugent <public@vaughnnugent.com>2024-02-14 14:10:27 -0500
committerLibravatar vnugent <public@vaughnnugent.com>2024-02-14 14:10:27 -0500
commit2b1314c1475e7e1831c691cf349cb89c66fa320c (patch)
tree091fc132a2bee2e79a68d8c6d5eb20f1d989a3d2 /third-party/DotNetCorePlugins
parentf4e4db7c5320976406feb252ae8f8bdbe9b3e351 (diff)
Squashed commit of the following:
commit ddd8a651b6eb43cfdd49d84056f8b9c34b543992 Author: vnugent <public@vaughnnugent.com> Date: Wed Feb 14 00:15:50 2024 -0500 ci: reduce output noise and update Argon2 build commit cf942959ff2feea03d3eda2ff2a263bdac4d6bc6 Author: vnugent <public@vaughnnugent.com> Date: Mon Feb 12 18:39:18 2024 -0500 chore: update packages and minor fixes commit ab506af9e2de2876b11bb45b3c7e787616c80155 Author: vnugent <public@vaughnnugent.com> Date: Fri Feb 9 21:27:24 2024 -0500 fix: patch and update core runtime service injection commit 7ed5e8b19164c28d3a238bd56878d2161fbea2e4 Author: vnugent <public@vaughnnugent.com> Date: Thu Feb 8 18:26:11 2024 -0500 fork dotnetplugins and make some intial updates/upgrades commit f4cab88d67be5da0953b14bd46fc972d4acc8606 Author: vnugent <public@vaughnnugent.com> Date: Thu Feb 8 12:16:13 2024 -0500 update some heap api functions commit 6035bf7ed8412f1da361cc5feddd860abfaf4fc1 Author: vnugent <public@vaughnnugent.com> Date: Wed Feb 7 22:09:11 2024 -0500 working file-watcher notifications/rework commit 698f8edf694ad9700ee2ce2220e692b496448ff9 Author: vnugent <public@vaughnnugent.com> Date: Wed Feb 7 20:37:28 2024 -0500 remove mem-template and add file-watcher utility commit b17591e0fb363222fcd7d93c2bad4ab1b102385f Author: vnugent <public@vaughnnugent.com> Date: Wed Feb 7 18:28:21 2024 -0500 add small memmove support for known small blocks commit 631be4d4b27fdbcd4b0526e17a128bb0d86911eb Author: vnugent <public@vaughnnugent.com> Date: Wed Feb 7 18:08:02 2024 -0500 setup some readonly ref arguments and convert copy apis to readonly refs commit 2ba8dec68d5cb192e61ad0141d4b460076d3f90a Author: vnugent <public@vaughnnugent.com> Date: Mon Feb 5 18:30:38 2024 -0500 restructure internal memmove strategies commit 25cf02872da980893ad7fb51d4eccc932380582b Author: vnugent <public@vaughnnugent.com> Date: Sun Feb 4 01:29:18 2024 -0500 add http stream interface, profiling -> file read updates commit 757668c44e78864dc69d5713a2cfba6db2ed9a2a Author: vnugent <public@vaughnnugent.com> Date: Fri Feb 2 14:27:04 2024 -0500 streamline data-copy api with proper large block support and net8 feature updates commit f22c1765fd72ab40a10d8ec92a8cb6d9ec1b1a04 Author: vnugent <public@vaughnnugent.com> Date: Mon Jan 29 16:16:23 2024 -0500 check for compression lib updates to close #2 and fix some ci build stuff commit f974bfdef6a795b4a1c04602502ef506ef2587a9 Author: vnugent <public@vaughnnugent.com> Date: Tue Jan 23 17:36:17 2024 -0500 switch allocator libs to lgpl2.1 commit 1fe5e01b329cd27b675000f1a557b784d3c88b56 Author: vnugent <public@vaughnnugent.com> Date: Tue Jan 23 17:05:59 2024 -0500 consolidate allocator packages and close #1 commit 74e1107e522f00b670526193396217f40a6bade7 Author: vnugent <public@vaughnnugent.com> Date: Tue Jan 23 15:43:40 2024 -0500 cache extension api tweaks commit 96ca2b0388a6326b9bb74f3ab2f62eaede6681e0 Author: vnugent <public@vaughnnugent.com> Date: Mon Jan 22 17:54:23 2024 -0500 explicit tcp server args reuse
Diffstat (limited to 'third-party/DotNetCorePlugins')
-rw-r--r--third-party/DotNetCorePlugins/CHANGES.md15
-rw-r--r--third-party/DotNetCorePlugins/LICENSE.txt176
-rw-r--r--third-party/DotNetCorePlugins/README.md243
-rw-r--r--third-party/DotNetCorePlugins/src/Internal/PlatformInformation.cs73
-rw-r--r--third-party/DotNetCorePlugins/src/Internal/RuntimeConfig.cs10
-rw-r--r--third-party/DotNetCorePlugins/src/Internal/RuntimeOptions.cs12
-rw-r--r--third-party/DotNetCorePlugins/src/LibraryModel/ManagedLibrary.cs73
-rw-r--r--third-party/DotNetCorePlugins/src/LibraryModel/NativeLibrary.cs69
-rw-r--r--third-party/DotNetCorePlugins/src/Loader/AssemblyLoadContextBuilder.cs341
-rw-r--r--third-party/DotNetCorePlugins/src/Loader/DependencyContextExtensions.cs207
-rw-r--r--third-party/DotNetCorePlugins/src/Loader/ManagedLoadContext.cs386
-rw-r--r--third-party/DotNetCorePlugins/src/Loader/RuntimeConfigExtensions.cs129
-rw-r--r--third-party/DotNetCorePlugins/src/McMaster.NETCore.Plugins.csproj26
-rw-r--r--third-party/DotNetCorePlugins/src/PluginConfig.cs94
-rw-r--r--third-party/DotNetCorePlugins/src/PluginLoader.cs148
-rw-r--r--third-party/DotNetCorePlugins/src/Properties/AssemblyInfo.cs6
-rw-r--r--third-party/DotNetCorePlugins/test/Plugins.Tests/BasicAssemblyLoaderTests.cs160
-rw-r--r--third-party/DotNetCorePlugins/test/Plugins.Tests/DebouncerTests.cs60
-rw-r--r--third-party/DotNetCorePlugins/test/Plugins.Tests/ManageLoadContextTests.cs69
-rw-r--r--third-party/DotNetCorePlugins/test/Plugins.Tests/McMaster.NETCore.Plugins.Tests.csproj41
-rw-r--r--third-party/DotNetCorePlugins/test/Plugins.Tests/PrivateDependencyTests.cs40
-rw-r--r--third-party/DotNetCorePlugins/test/Plugins.Tests/ShadowCopyTests.cs31
-rw-r--r--third-party/DotNetCorePlugins/test/Plugins.Tests/SharedTypesTests.cs107
-rw-r--r--third-party/DotNetCorePlugins/test/Plugins.Tests/TestProjectRefs.targets50
-rw-r--r--third-party/DotNetCorePlugins/test/Plugins.Tests/Utilities/TestProjectReferenceAttribute.cs20
-rw-r--r--third-party/DotNetCorePlugins/test/Plugins.Tests/Utilities/TestResources.cs20
-rw-r--r--third-party/DotNetCorePlugins/test/TestProjects/Banana/Banana.cs12
-rw-r--r--third-party/DotNetCorePlugins/test/TestProjects/Banana/Banana.csproj11
-rw-r--r--third-party/DotNetCorePlugins/test/TestProjects/Directory.Build.props7
-rw-r--r--third-party/DotNetCorePlugins/test/TestProjects/DrawingApp/DrawingApp.csproj11
-rw-r--r--third-party/DotNetCorePlugins/test/TestProjects/DrawingApp/Finder.cs14
-rw-r--r--third-party/DotNetCorePlugins/test/TestProjects/JsonNet10/Class1.cs9
-rw-r--r--third-party/DotNetCorePlugins/test/TestProjects/JsonNet10/JsonNet10.csproj12
-rw-r--r--third-party/DotNetCorePlugins/test/TestProjects/JsonNet11/Class1.cs9
-rw-r--r--third-party/DotNetCorePlugins/test/TestProjects/JsonNet11/JsonNet11.csproj13
-rw-r--r--third-party/DotNetCorePlugins/test/TestProjects/JsonNet9/Class1.cs9
-rw-r--r--third-party/DotNetCorePlugins/test/TestProjects/JsonNet9/JsonNet9.csproj12
-rw-r--r--third-party/DotNetCorePlugins/test/TestProjects/NativeDependency/NativeDependency.csproj11
-rw-r--r--third-party/DotNetCorePlugins/test/TestProjects/NativeDependency/NativeDependencyLoader.cs40
-rw-r--r--third-party/DotNetCorePlugins/test/TestProjects/NetCoreApp2App/NetCoreApp2App.csproj12
-rw-r--r--third-party/DotNetCorePlugins/test/TestProjects/NetCoreApp2App/Program.cs17
-rw-r--r--third-party/DotNetCorePlugins/test/TestProjects/NetStandardClassLib/Class1.cs10
-rw-r--r--third-party/DotNetCorePlugins/test/TestProjects/NetStandardClassLib/NetStandardClassLib.csproj7
-rw-r--r--third-party/DotNetCorePlugins/test/TestProjects/Plátano/Plátano.cs13
-rw-r--r--third-party/DotNetCorePlugins/test/TestProjects/Plátano/Plátano.csproj11
-rw-r--r--third-party/DotNetCorePlugins/test/TestProjects/Plátano/Strings.Designer.cs55
-rw-r--r--third-party/DotNetCorePlugins/test/TestProjects/Plátano/Strings.es.Designer.cs1
-rw-r--r--third-party/DotNetCorePlugins/test/TestProjects/Plátano/Strings.es.resx24
-rw-r--r--third-party/DotNetCorePlugins/test/TestProjects/Plátano/Strings.resx24
-rw-r--r--third-party/DotNetCorePlugins/test/TestProjects/PowerShellPlugin/PowerShellPlugin.csproj12
-rw-r--r--third-party/DotNetCorePlugins/test/TestProjects/PowerShellPlugin/Program.cs25
-rw-r--r--third-party/DotNetCorePlugins/test/TestProjects/ReferencedLibv1/Class1.cs10
-rw-r--r--third-party/DotNetCorePlugins/test/TestProjects/ReferencedLibv1/IFruit.cs10
-rw-r--r--third-party/DotNetCorePlugins/test/TestProjects/ReferencedLibv1/ReferencedLibv1.csproj9
-rw-r--r--third-party/DotNetCorePlugins/test/TestProjects/ReferencedLibv2/Class1.cs10
-rw-r--r--third-party/DotNetCorePlugins/test/TestProjects/ReferencedLibv2/ReferencedLibv2.csproj13
-rw-r--r--third-party/DotNetCorePlugins/test/TestProjects/SharedAbstraction.v1/SharedAbstraction.v1.csproj13
-rw-r--r--third-party/DotNetCorePlugins/test/TestProjects/SharedAbstraction.v1/SharedType.cs13
-rw-r--r--third-party/DotNetCorePlugins/test/TestProjects/SharedAbstraction.v2/SharedAbstraction.v2.csproj14
-rw-r--r--third-party/DotNetCorePlugins/test/TestProjects/SqlClientApp/Program.cs31
-rw-r--r--third-party/DotNetCorePlugins/test/TestProjects/SqlClientApp/SqlClientApp.csproj12
-rw-r--r--third-party/DotNetCorePlugins/test/TestProjects/Strawberry/Strawberry.cs12
-rw-r--r--third-party/DotNetCorePlugins/test/TestProjects/Strawberry/Strawberry.csproj11
-rw-r--r--third-party/DotNetCorePlugins/test/TestProjects/TransitiveDep.v1/TransitiveDep.v1.csproj9
-rw-r--r--third-party/DotNetCorePlugins/test/TestProjects/TransitiveDep.v1/TransitiveSharedType.cs7
-rw-r--r--third-party/DotNetCorePlugins/test/TestProjects/TransitiveDep.v2/TransitiveDep.v2.csproj13
-rw-r--r--third-party/DotNetCorePlugins/test/TestProjects/TransitivePlugin/PluginConfig.cs12
-rw-r--r--third-party/DotNetCorePlugins/test/TestProjects/TransitivePlugin/TransitivePlugin.csproj11
-rw-r--r--third-party/DotNetCorePlugins/test/TestProjects/WithOurPluginsPluginA/Class1.cs12
-rw-r--r--third-party/DotNetCorePlugins/test/TestProjects/WithOurPluginsPluginA/WithOurPluginsPluginA.csproj12
-rw-r--r--third-party/DotNetCorePlugins/test/TestProjects/WithOurPluginsPluginB/Class1.cs12
-rw-r--r--third-party/DotNetCorePlugins/test/TestProjects/WithOurPluginsPluginB/WithOurPluginsPluginB.csproj12
-rw-r--r--third-party/DotNetCorePlugins/test/TestProjects/WithOurPluginsPluginContract/ISayHello.cs10
-rw-r--r--third-party/DotNetCorePlugins/test/TestProjects/WithOurPluginsPluginContract/WithOurPluginsPluginContract.csproj7
-rw-r--r--third-party/DotNetCorePlugins/test/TestProjects/WithOwnPlugins/WithOwnPlugins.cs72
-rw-r--r--third-party/DotNetCorePlugins/test/TestProjects/WithOwnPlugins/WithOwnPlugins.csproj15
-rw-r--r--third-party/DotNetCorePlugins/test/TestProjects/WithOwnPluginsContract/IWithOwnPlugins.cs12
-rw-r--r--third-party/DotNetCorePlugins/test/TestProjects/WithOwnPluginsContract/WithOwnPluginsContract.csproj7
-rw-r--r--third-party/DotNetCorePlugins/test/TestProjects/XunitSample/Class1.cs9
-rw-r--r--third-party/DotNetCorePlugins/test/TestProjects/XunitSample/XunitSample.csproj12
80 files changed, 3389 insertions, 0 deletions
diff --git a/third-party/DotNetCorePlugins/CHANGES.md b/third-party/DotNetCorePlugins/CHANGES.md
new file mode 100644
index 0000000..b9efcfc
--- /dev/null
+++ b/third-party/DotNetCorePlugins/CHANGES.md
@@ -0,0 +1,15 @@
+# Changes
+
+DotnetCorePlugins is/was originally developed by Nate McMaster -> [original repo](https://github.com/natemcmaster/DotNetCorePlugins)
+
+Project maintaince has fallen off, so I've forked it to make some updates and changes. Changes are mostly specific to my application, or features that I don't think are necessary, or are experimental.
+
+Changes made to the project will be documented in each file. Here are some of the changes I've made:
+
+- Update to .NET 8.0 runtime
+- Remove hot-reload feature (my runtime implements this better for my uses)
+- Update deprecated packages
+- Change public api for more control and removed less verbose features that were never used
+- Removing conditional compilation for platform supported features
+- General .NET 8.0 stdlib updates and syntax changes
+- Foced/removed condtional compiliation features \ No newline at end of file
diff --git a/third-party/DotNetCorePlugins/LICENSE.txt b/third-party/DotNetCorePlugins/LICENSE.txt
new file mode 100644
index 0000000..c383288
--- /dev/null
+++ b/third-party/DotNetCorePlugins/LICENSE.txt
@@ -0,0 +1,176 @@
+Apache License
+Version 2.0, January 2004
+http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
diff --git a/third-party/DotNetCorePlugins/README.md b/third-party/DotNetCorePlugins/README.md
new file mode 100644
index 0000000..0fa5d57
--- /dev/null
+++ b/third-party/DotNetCorePlugins/README.md
@@ -0,0 +1,243 @@
+.NET Core Plugins
+=================
+
+[![Build Status][ci-badge]][ci] [![Code Coverage][codecov-badge]][codecov]
+[![NuGet][nuget-badge] ![NuGet Downloads][nuget-download-badge]][nuget]
+
+[ci]: https://github.com/natemcmaster/DotNetCorePlugins/actions?query=workflow%3ACI+branch%3Amain
+[ci-badge]: https://github.com/natemcmaster/DotNetCorePlugins/workflows/CI/badge.svg
+[codecov]: https://codecov.io/gh/natemcmaster/DotNetCorePlugins
+[codecov-badge]: https://codecov.io/gh/natemcmaster/DotNetCorePlugins/branch/main/graph/badge.svg?token=l6uSsHZ8nA
+[nuget]: https://www.nuget.org/packages/McMaster.NETCore.Plugins/
+[nuget-badge]: https://img.shields.io/nuget/v/McMaster.NETCore.Plugins.svg?style=flat-square
+[nuget-download-badge]: https://img.shields.io/nuget/dt/McMaster.NETCore.Plugins?style=flat-square
+
+
+This project provides API for loading .NET Core assemblies dynamically, executing them as extensions to the main application, and finding and **isolating** the dependencies of the plugin from the main application.
+This library supports .NET Core 2, but works best in .NET Core 3 and up. It allows fine-grained control over
+assembly isolation and type sharing. Read [more details about type sharing below](#shared-types).
+
+Blog post introducing this project, July 25, 2018: [.NET Core Plugins: Introducing an API for loading .dll files (and their dependencies) as 'plugins'](https://natemcmaster.com/blog/2018/07/25/netcore-plugins/).
+
+**Since 2018, .NET Core 3**
+was released, and it added stdlib API to improve assembly loading. If you are interested in understanding that API, see "[Create a .NET Core application with plugins][plugin-tutorial]" on docs.microsoft.com. The result of this tutorial would be a simpler version of DotNetCorePlugins, but missing some features like an API for unifying types across the load context boundary, hot reload, and .NET Core 2 support.
+
+[plugin-tutorial]: https://docs.microsoft.com/dotnet/core/tutorials/creating-app-with-plugin-support
+
+## Getting started
+
+Install the [`McMaster.NETCore.Plugins` NuGet package.][nuget]
+
+```
+dotnet add package McMaster.NETCore.Plugins
+```
+
+The main API to use is `PluginLoader.CreateFromAssemblyFile`.
+
+```csharp
+PluginLoader.CreateFromAssemblyFile(
+ assemblyFile: "./plugins/MyPlugin/MyPlugin1.dll",
+ sharedTypes: new [] { typeof(IPlugin), typeof(IServiceCollection), typeof(ILogger) },
+ isUnloadable: true)
+```
+
+* assemblyFile = the file path to the main .dll of the plugin
+* sharedTypes = a list of types which the loader should ensure are unified. (See [What is a shared type?](#shared-types))
+* isUnloadable = (.NET Core 3+ only). Allow this plugin to be unloaded from memory at some point in the future. (Requires ensuring that you have cleaned up all usages of types from the plugin before unloading actually happens.)
+
+See example projects in [samples/](./samples/) for more detailed, example usage.
+
+## Usage
+
+Using plugins requires at least two projects: (1) the 'host' app which loads plugins and (2) the plugin,
+but typically also uses a third, (3) an contracts project which defines the interaction between the plugin
+and the host.
+
+For a fully functional sample of this, see [samples/hello-world/](./samples/hello-world/) .
+
+### The plugin contract
+
+You can define your own plugin contract. A minimal contract might look like this.
+
+```csharp
+public interface IPlugin
+{
+ string GetName();
+}
+```
+
+There is nothing special about the name "IPlugin" or the fact that it's an interface. This is just here to illustrate a concept. Look at [samples/](./samples/) for additional examples of ways you could define the interaction between host and plugins.
+
+### The plugins
+
+Typically, it is best to implement plugins by targeting `net5.0` or higher. They can target `netstandard2.0` as well, but using `net5.0` is better because it reduces the number of redundant System.\* assemblies in the plugin output.
+
+A minimal implementation of the plugin could be as simple as this.
+
+```csharp
+internal class MyPlugin1 : IPlugin
+{
+ public string GetName() => "My plugin v1";
+}
+```
+
+As mentioned above, this is just an example. This library doesn't require the use of "IPlugin" or interfaces or "GetName()"
+methods. This code is only here to demonstrates how you can decouple hosts and plugins, but still use interfaces for type-safe
+interactions.
+
+### The host
+
+The host application can load plugins using the `PluginLoader` API. The host app needs to define a way to find
+the assemblies for the plugin on disk. One way to do this is to follow a convention, such as:
+
+```
+plugins/
+ $PluginName1/
+ $PluginName1.dll
+ (additional plugin files)
+ $PluginName2/
+ $PluginName2.dll
+```
+
+**It is important that each plugin is published into a separate directory.** This will avoid contention between plugins
+and duplicate dependency issues.
+
+You can prepare the sample plugin above by running
+
+```
+dotnet publish MyPlugin1.csproj --output plugins/MyPlugin1/
+```
+
+An implementation of a host which finds and loads this plugin might look like this. This sample uses reflection to find
+all types in plugins which implement `IPlugin`, and then initializes the types' parameter-less constructors.
+This is just one way to implement a host. More examples of how to use plugins can be found in [samples/](./samples/).
+
+```csharp
+using McMaster.NETCore.Plugins;
+
+var loaders = new List<PluginLoader>();
+
+// create plugin loaders
+var pluginsDir = Path.Combine(AppContext.BaseDirectory, "plugins");
+foreach (var dir in Directory.GetDirectories(pluginsDir))
+{
+ var dirName = Path.GetFileName(dir);
+ var pluginDll = Path.Combine(dir, dirName + ".dll");
+ if (File.Exists(pluginDll))
+ {
+ var loader = PluginLoader.CreateFromAssemblyFile(
+ pluginDll,
+ sharedTypes: new [] { typeof(IPlugin) });
+ loaders.Add(loader);
+ }
+}
+
+// Create an instance of plugin types
+foreach (var loader in loaders)
+{
+ foreach (var pluginType in loader
+ .LoadDefaultAssembly()
+ .GetTypes()
+ .Where(t => typeof(IPlugin).IsAssignableFrom(t) && !t.IsAbstract))
+ {
+ // This assumes the implementation of IPlugin has a parameterless constructor
+ IPlugin plugin = (IPlugin)Activator.CreateInstance(pluginType);
+
+ Console.WriteLine($"Created plugin instance '{plugin.GetName()}'.");
+ }
+}
+```
+
+<a id="shared-types"></a>
+
+### What is a shared type?
+
+By default, each instance of `PluginLoader` represents a unique collection of assemblies loaded into memory.
+This can make it difficult to use the plugin if you want to pass information from plugin to the host and vice versa.
+Shared types allow you define the kinds of objects that will be passed between plugin and host.
+
+For example, let's say you have a simple host app like [samples/hello-world/](./samples/hello-world/), and
+two plugins which were compiled with a reference `interface IPlugin`. This interface comes from `Contracts.dll`.
+When the application runs, by default, each plugin and the host will have their own version of `Contracts.dll`
+which .NET Core will keep isolated.
+
+The problem with this isolation is that an object of `IPlugin` created within the "PluginApple" or "PluginBanana" context does not appear to be an instance of `IPlugin` in any of the other plugin contexts.
+
+![DefaultConfigDiagram](https://i.imgur.com/fHEMBO6.png)
+
+Configuring a shared type of `IPlugin` allows the .NET to pass objects of this type across the plugin isolation
+boundary. It does this by ignoring the version of `Contracts.dll` in each plugin folder, and sharing the version that comes with the Host.
+
+![SharedTypes](https://i.imgur.com/sTGqPxa.png)
+
+Read [even more details about shared types here](./docs/what-are-shared-types.md).
+
+## Support for MVC and Razor
+
+A common usage for plugins is to load class libraries that contain MVC controllers or Razor Pages. You can
+set up an ASP.NET Core to load controllers and views from a plugin using the `McMaster.NETCore.Plugins.Mvc`
+package.
+
+```
+dotnet add package McMaster.NETCore.Plugins.Mvc
+```
+
+The main API to use is `.AddPluginFromAssemblyFile()`, which can be chained onto the call to `.AddMvc()`
+or `.AddRazorPages()` in the `Startup.ConfigureServices` method.
+
+```c#
+public class Startup
+{
+ public void ConfigureServices(IServiceCollection services)
+ {
+ var pluginFile = Path.Combine(AppContext.BaseDirectory, "plugins/MyRazorPlugin/MyRazorPlugin.dll");
+ services
+ .AddMvc()
+ // The AddPluginFromAssemblyFile method comes from McMaster.NETCore.Plugins.Mvc
+ .AddPluginFromAssemblyFile(pluginFile);
+ }
+}
+```
+
+See example projects in [samples/aspnetcore-mvc/](./samples/aspnetcore-mvc/) for more detailed, example usage.
+
+## Reflection
+
+Sometimes you may want to use a plugin along with reflection APIs such as `Type.GetType(string typeName)`
+or `Assembly.Load(string assemblyString)`. Depending on where these APIs are used, they might fail to
+load the assemblies in your plugin. In .NET Core 3+, there is an API which you can use to set the _ambient context_
+which .NET's reflection APIs will use to load the correct assemblies from your plugin.
+
+Example:
+```c#
+var loader = PluginLoader.CreateFromAssemblyFile("./plugins/MyPlugin/MyPlugin1.dll");
+
+using (loader.EnterContextualReflection())
+{
+ var myPluginType = Type.GetType("MyPlugin.PluginClass");
+ var myPluginAssembly = Assembly.Load("MyPlugin1");
+}
+
+```
+
+Read [this post written by .NET Core engineers](https://github.com/dotnet/coreclr/blob/v3.0.0/Documentation/design-docs/AssemblyLoadContext.ContextualReflection.md) for even more details on contextual reflection.
+
+## Overriding the Default Load Context
+
+Under the hood, DotNetCorePlugins is using a .NET Core API called [ApplicationLoadContext][alc-api].
+This creates a scope for resolving assemblies. By default, `PluginLoader` will create a new context
+and fallback to a **default context** if it cannot find an assembly or if type sharing is enabled.
+The default fallback context is inferred when `PluginLoader` is instantiated. In certain advanced scenarios,
+you may need to manually change the default context, for instance, plugins which then load more plugins,
+or when running .NET in a custom native host.
+
+[alc-api]: https://docs.microsoft.com/dotnet/api/system.runtime.loader.assemblyloadcontext
+
+To override the default assembly load context, set `PluginConfig.DefaultContext`. Example:
+
+
+```csharp
+AssemblyLoadContext myCustomDefaultContext = // (something).
+PluginLoader.CreateFromAssemblyFile(dllPath,
+ config => config.DefaultContext = myCustomDefaultContext);
+```
diff --git a/third-party/DotNetCorePlugins/src/Internal/PlatformInformation.cs b/third-party/DotNetCorePlugins/src/Internal/PlatformInformation.cs
new file mode 100644
index 0000000..0b4ac27
--- /dev/null
+++ b/third-party/DotNetCorePlugins/src/Internal/PlatformInformation.cs
@@ -0,0 +1,73 @@
+// Copyright (c) Nate McMaster.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+/*
+ * Modifications Copyright (c) 2024 Vaughn Nugent
+ *
+ * Changes:
+ * - Use the new .NET 8.0 collection syntax
+ * - Use the new OperatingSystem class for platform detection
+ * - Remove static constructor for new best practice guidlines
+ */
+
+using System;
+using System.Diagnostics;
+
+namespace McMaster.NETCore.Plugins
+{
+ internal class PlatformInformation
+ {
+ public static readonly string[] NativeLibraryExtensions = GetNativeLibExtension();
+ public static readonly string[] NativeLibraryPrefixes = GetNativeLibPrefixs();
+
+ public static readonly string[] ManagedAssemblyExtensions =
+ [
+ ".dll",
+ ".ni.dll",
+ ".exe",
+ ".ni.exe"
+ ];
+
+ private static string[] GetNativeLibPrefixs()
+ {
+ if (OperatingSystem.IsWindows())
+ {
+ return [""];
+ }
+ else if (OperatingSystem.IsMacOS())
+ {
+ return ["", "lib",];
+ }
+ else if (OperatingSystem.IsLinux())
+ {
+ return ["", "lib"];
+ }
+ else
+ {
+ Debug.Fail("Unknown OS type");
+ return [];
+ }
+ }
+
+ private static string[] GetNativeLibExtension()
+ {
+ if (OperatingSystem.IsWindows())
+ {
+ return [".dll"];
+ }
+ else if (OperatingSystem.IsMacOS())
+ {
+ return [".dylib"];
+ }
+ else if (OperatingSystem.IsLinux())
+ {
+ return [".so", ".so.1"];
+ }
+ else
+ {
+ Debug.Fail("Unknown OS type");
+ return [];
+ }
+ }
+ }
+}
diff --git a/third-party/DotNetCorePlugins/src/Internal/RuntimeConfig.cs b/third-party/DotNetCorePlugins/src/Internal/RuntimeConfig.cs
new file mode 100644
index 0000000..6da5dd4
--- /dev/null
+++ b/third-party/DotNetCorePlugins/src/Internal/RuntimeConfig.cs
@@ -0,0 +1,10 @@
+// Copyright (c) Nate McMaster.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace McMaster.NETCore.Plugins
+{
+ internal class RuntimeConfig
+ {
+ public RuntimeOptions? runtimeOptions { get; set; }
+ }
+}
diff --git a/third-party/DotNetCorePlugins/src/Internal/RuntimeOptions.cs b/third-party/DotNetCorePlugins/src/Internal/RuntimeOptions.cs
new file mode 100644
index 0000000..748aa38
--- /dev/null
+++ b/third-party/DotNetCorePlugins/src/Internal/RuntimeOptions.cs
@@ -0,0 +1,12 @@
+// Copyright (c) Nate McMaster.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace McMaster.NETCore.Plugins
+{
+ internal class RuntimeOptions
+ {
+ public string? Tfm { get; set; }
+
+ public string[]? AdditionalProbingPaths { get; set; }
+ }
+}
diff --git a/third-party/DotNetCorePlugins/src/LibraryModel/ManagedLibrary.cs b/third-party/DotNetCorePlugins/src/LibraryModel/ManagedLibrary.cs
new file mode 100644
index 0000000..ca45cc5
--- /dev/null
+++ b/third-party/DotNetCorePlugins/src/LibraryModel/ManagedLibrary.cs
@@ -0,0 +1,73 @@
+// Copyright (c) Nate McMaster.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Reflection;
+
+namespace McMaster.NETCore.Plugins.LibraryModel
+{
+ /// <summary>
+ /// Represents a managed, .NET assembly.
+ /// </summary>
+ [DebuggerDisplay("{Name} = {AdditionalProbingPath}")]
+ public class ManagedLibrary
+ {
+ private ManagedLibrary(AssemblyName name, string additionalProbingPath, string appLocalPath)
+ {
+ Name = name ?? throw new ArgumentNullException(nameof(name));
+ AdditionalProbingPath = additionalProbingPath ?? throw new ArgumentNullException(nameof(additionalProbingPath));
+ AppLocalPath = appLocalPath ?? throw new ArgumentNullException(nameof(appLocalPath));
+ }
+
+ /// <summary>
+ /// Name of the managed library
+ /// </summary>
+ public AssemblyName Name { get; private set; }
+
+ /// <summary>
+ /// Contains path to file within an additional probing path root. This is typically a combination
+ /// of the NuGet package ID (lowercased), version, and path within the package.
+ /// <para>
+ /// For example, <c>microsoft.data.sqlite/1.0.0/lib/netstandard1.3/Microsoft.Data.Sqlite.dll</c>
+ /// </para>
+ /// </summary>
+ public string AdditionalProbingPath { get; private set; }
+
+ /// <summary>
+ /// Contains path to file within a deployed, framework-dependent application.
+ /// <para>
+ /// For most managed libraries, this will be the file name.
+ /// For example, <c>MyPlugin1.dll</c>.
+ /// </para>
+ /// <para>
+ /// For runtime-specific managed implementations, this may include a sub folder path.
+ /// For example, <c>runtimes/win/lib/netcoreapp2.0/System.Diagnostics.EventLog.dll</c>
+ /// </para>
+ /// </summary>
+ public string AppLocalPath { get; private set; }
+
+ /// <summary>
+ /// Create an instance of <see cref="ManagedLibrary" /> from a NuGet package.
+ /// </summary>
+ /// <param name="packageId">The name of the package.</param>
+ /// <param name="packageVersion">The version of the package.</param>
+ /// <param name="assetPath">The path within the NuGet package.</param>
+ /// <returns></returns>
+ public static ManagedLibrary CreateFromPackage(string packageId, string packageVersion, string assetPath)
+ {
+ // When the asset comes from "lib/$tfm/", Microsoft.NET.Sdk will flatten this during publish based on the most compatible TFM.
+ // The SDK will not flatten managed libraries found under runtimes/
+ var appLocalPath = assetPath.StartsWith("lib/")
+ ? Path.GetFileName(assetPath)
+ : assetPath;
+
+ return new ManagedLibrary(
+ new AssemblyName(Path.GetFileNameWithoutExtension(assetPath)),
+ Path.Combine(packageId.ToLowerInvariant(), packageVersion, assetPath),
+ appLocalPath
+ );
+ }
+ }
+}
diff --git a/third-party/DotNetCorePlugins/src/LibraryModel/NativeLibrary.cs b/third-party/DotNetCorePlugins/src/LibraryModel/NativeLibrary.cs
new file mode 100644
index 0000000..ac5e061
--- /dev/null
+++ b/third-party/DotNetCorePlugins/src/LibraryModel/NativeLibrary.cs
@@ -0,0 +1,69 @@
+// Copyright (c) Nate McMaster.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Diagnostics;
+using System.IO;
+
+namespace McMaster.NETCore.Plugins.LibraryModel
+{
+ /// <summary>
+ /// Represents an unmanaged library, such as `libsqlite3`, which may need to be loaded
+ /// for P/Invoke to work.
+ /// </summary>
+ [DebuggerDisplay("{Name} = {AdditionalProbingPath}")]
+ public class NativeLibrary
+ {
+ private NativeLibrary(string name, string appLocalPath, string additionalProbingPath)
+ {
+ Name = name ?? throw new ArgumentNullException(nameof(name));
+ AppLocalPath = appLocalPath ?? throw new ArgumentNullException(nameof(appLocalPath));
+ AdditionalProbingPath = additionalProbingPath ?? throw new ArgumentNullException(nameof(additionalProbingPath));
+ }
+
+ /// <summary>
+ /// Name of the native library. This should match the name of the P/Invoke call.
+ /// <para>
+ /// For example, if specifying `[DllImport("sqlite3")]`, <see cref="Name" /> should be <c>sqlite3</c>.
+ /// This may not match the exact file name as loading will attempt variations on the name according
+ /// to OS convention. On Windows, P/Invoke will attempt to load `sqlite3.dll`. On macOS, it will
+ /// attempt to find `sqlite3.dylib` and `libsqlite3.dylib`. On Linux, it will attempt to find
+ /// `sqlite3.so` and `libsqlite3.so`.
+ /// </para>
+ /// </summary>
+ public string Name { get; private set; }
+
+ /// <summary>
+ /// Contains path to file within a deployed, framework-dependent application
+ /// <para>
+ /// For example, <c>runtimes/linux-x64/native/libsqlite.so</c>
+ /// </para>
+ /// </summary>
+ public string AppLocalPath { get; private set; }
+
+ /// <summary>
+ /// Contains path to file within an additional probing path root. This is typically a combination
+ /// of the NuGet package ID (lowercased), version, and path within the package.
+ /// <para>
+ /// For example, <c>sqlite/3.13.3/runtimes/linux-x64/native/libsqlite.so</c>
+ /// </para>
+ /// </summary>
+ public string AdditionalProbingPath { get; private set; }
+
+ /// <summary>
+ /// Create an instance of <see cref="NativeLibrary" /> from a NuGet package.
+ /// </summary>
+ /// <param name="packageId">The name of the package.</param>
+ /// <param name="packageVersion">The version of the package.</param>
+ /// <param name="assetPath">The path within the NuGet package.</param>
+ /// <returns></returns>
+ public static NativeLibrary CreateFromPackage(string packageId, string packageVersion, string assetPath)
+ {
+ return new NativeLibrary(
+ Path.GetFileNameWithoutExtension(assetPath),
+ assetPath,
+ Path.Combine(packageId.ToLowerInvariant(), packageVersion, assetPath)
+ );
+ }
+ }
+}
diff --git a/third-party/DotNetCorePlugins/src/Loader/AssemblyLoadContextBuilder.cs b/third-party/DotNetCorePlugins/src/Loader/AssemblyLoadContextBuilder.cs
new file mode 100644
index 0000000..05d6238
--- /dev/null
+++ b/third-party/DotNetCorePlugins/src/Loader/AssemblyLoadContextBuilder.cs
@@ -0,0 +1,341 @@
+// Copyright (c) Nate McMaster.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+/*
+ * Modifications Copyright (c) 2024 Vaughn Nugent
+ *
+ * Changes:
+ * - Removed unloadable feature as an optional pragma (aka always on)
+ * - Removed unused
+ */
+using System;
+using System.IO;
+using System.Reflection;
+using System.Runtime.Loader;
+using System.Collections.Generic;
+
+using McMaster.NETCore.Plugins.LibraryModel;
+
+namespace McMaster.NETCore.Plugins.Loader
+{
+ /// <summary>
+ /// A builder for creating an instance of <see cref="AssemblyLoadContext" />.
+ /// </summary>
+ public class AssemblyLoadContextBuilder
+ {
+ private readonly List<string> _additionalProbingPaths = new();
+ private readonly List<string> _resourceProbingPaths = new();
+ private readonly List<string> _resourceProbingSubpaths = new();
+ private readonly Dictionary<string, ManagedLibrary> _managedLibraries = new(StringComparer.Ordinal);
+ private readonly Dictionary<string, NativeLibrary> _nativeLibraries = new(StringComparer.Ordinal);
+ private readonly HashSet<string> _privateAssemblies = new(StringComparer.Ordinal);
+ private readonly HashSet<string> _defaultAssemblies = new(StringComparer.Ordinal);
+ private AssemblyLoadContext _defaultLoadContext = AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly()) ?? AssemblyLoadContext.Default;
+ private string? _mainAssemblyPath;
+ private bool _preferDefaultLoadContext;
+ private bool _isCollectible;
+ private bool _loadInMemory;
+ private bool _shadowCopyNativeLibraries;
+
+
+ /// <summary>
+ /// Creates an assembly load context using settings specified on the builder.
+ /// </summary>
+ /// <returns>A new ManagedLoadContext.</returns>
+ public AssemblyLoadContext Build()
+ {
+ var resourceProbingPaths = new List<string>(_resourceProbingPaths);
+ foreach (var additionalPath in _additionalProbingPaths)
+ {
+ foreach (var subPath in _resourceProbingSubpaths)
+ {
+ resourceProbingPaths.Add(Path.Combine(additionalPath, subPath));
+ }
+ }
+
+ if (_mainAssemblyPath == null)
+ {
+ throw new InvalidOperationException($"Missing required property. You must call '{nameof(SetMainAssemblyPath)}' to configure the default assembly.");
+ }
+
+ //Lots of arguments, make it clear
+ return new ManagedLoadContext(
+ mainAssemblyPath: _mainAssemblyPath,
+ managedAssemblies: _managedLibraries,
+ nativeLibraries: _nativeLibraries,
+ privateAssemblies: _privateAssemblies,
+ defaultAssemblies: _defaultAssemblies,
+ additionalProbingPaths: _additionalProbingPaths,
+ resourceProbingPaths: resourceProbingPaths,
+ defaultLoadContext: _defaultLoadContext,
+ preferDefaultLoadContext: _preferDefaultLoadContext,
+ isCollectible: _isCollectible,
+ loadInMemory: _loadInMemory,
+ shadowCopyNativeLibraries: _shadowCopyNativeLibraries
+ );
+ }
+
+ /// <summary>
+ /// Set the file path to the main assembly for the context. This is used as the starting point for loading
+ /// other assemblies. The directory that contains it is also known as the 'app local' directory.
+ /// </summary>
+ /// <param name="path">The file path. Must not be null or empty. Must be an absolute path.</param>
+ /// <returns>The builder.</returns>
+ public AssemblyLoadContextBuilder SetMainAssemblyPath(string path)
+ {
+ if (string.IsNullOrEmpty(path))
+ {
+ throw new ArgumentException("Argument must not be null or empty.", nameof(path));
+ }
+
+ if (!Path.IsPathRooted(path))
+ {
+ throw new ArgumentException("Argument must be a full path.", nameof(path));
+ }
+
+ _mainAssemblyPath = path;
+ return this;
+ }
+
+ /// <summary>
+ /// Replaces the default <see cref="AssemblyLoadContext"/> used by the <see cref="AssemblyLoadContextBuilder"/>.
+ /// Use this feature if the <see cref="AssemblyLoadContext"/> of the <see cref="Assembly"/> is not the Runtime's default load context.
+ /// i.e. (AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly) != <see cref="AssemblyLoadContext.Default"/>
+ /// </summary>
+ /// <param name="context">The context to set.</param>
+ /// <returns>The builder.</returns>
+ public AssemblyLoadContextBuilder SetDefaultContext(AssemblyLoadContext context)
+ {
+ _defaultLoadContext = context ?? throw new ArgumentException($"Bad Argument: AssemblyLoadContext in {nameof(AssemblyLoadContextBuilder)}.{nameof(SetDefaultContext)} is null.");
+ return this;
+ }
+
+ /// <summary>
+ /// Instructs the load context to prefer a private version of this assembly, even if that version is
+ /// different from the version used by the host application.
+ /// Use this when you do not need to exchange types created from within the load context with other contexts
+ /// or the default app context.
+ /// <para>
+ /// This may mean the types loaded from
+ /// this assembly will not match the types from an assembly with the same name, but different version,
+ /// in the host application.
+ /// </para>
+ /// <para>
+ /// For example, if the host application has a type named <c>Foo</c> from assembly <c>Banana, Version=1.0.0.0</c>
+ /// and the load context prefers a private version of <c>Banan, Version=2.0.0.0</c>, when comparing two objects,
+ /// one created by the host (Foo1) and one created from within the load context (Foo2), they will not have the same
+ /// type. <c>Foo1.GetType() != Foo2.GetType()</c>
+ /// </para>
+ /// </summary>
+ /// <param name="assemblyName">The name of the assembly.</param>
+ /// <returns>The builder.</returns>
+ public AssemblyLoadContextBuilder PreferLoadContextAssembly(AssemblyName assemblyName)
+ {
+ if (assemblyName.Name != null)
+ {
+ _privateAssemblies.Add(assemblyName.Name);
+ }
+
+ return this;
+ }
+
+ /// <summary>
+ /// Instructs the load context to first attempt to load assemblies by this name from the default app context, even
+ /// if other assemblies in this load context express a dependency on a higher or lower version.
+ /// Use this when you need to exchange types created from within the load context with other contexts
+ /// or the default app context.
+ /// </summary>
+ /// <param name="assemblyName">The name of the assembly.</param>
+ /// <returns>The builder.</returns>
+ public AssemblyLoadContextBuilder PreferDefaultLoadContextAssembly(AssemblyName assemblyName)
+ {
+ var names = new Queue<AssemblyName>();
+ names.Enqueue(assemblyName);
+ while (names.TryDequeue(out var name))
+ {
+ if (name.Name == null || _defaultAssemblies.Contains(name.Name))
+ {
+ // base cases
+ continue;
+ }
+
+ _defaultAssemblies.Add(name.Name);
+
+ // Load and find all dependencies of default assemblies.
+ // This sacrifices some performance for determinism in how transitive
+ // dependencies will be shared between host and plugin.
+ var assembly = _defaultLoadContext.LoadFromAssemblyName(name);
+
+ foreach (var reference in assembly.GetReferencedAssemblies())
+ {
+ names.Enqueue(reference);
+ }
+ }
+
+ return this;
+ }
+
+ /// <summary>
+ /// Instructs the load context to first search for binaries from the default app context, even
+ /// if other assemblies in this load context express a dependency on a higher or lower version.
+ /// Use this when you need to exchange types created from within the load context with other contexts
+ /// or the default app context.
+ /// <para>
+ /// This may mean the types loaded from within the context are force-downgraded to the version provided
+ /// by the host. <seealso cref="PreferLoadContextAssembly" /> can be used to selectively identify binaries
+ /// which should not be loaded from the default load context.
+ /// </para>
+ /// </summary>
+ /// <param name="preferDefaultLoadContext">When true, first attemp to load binaries from the default load context.</param>
+ /// <returns>The builder.</returns>
+ public AssemblyLoadContextBuilder PreferDefaultLoadContext(bool preferDefaultLoadContext)
+ {
+ _preferDefaultLoadContext = preferDefaultLoadContext;
+ return this;
+ }
+
+
+ /// <summary>
+ /// Add a managed library to the load context.
+ /// </summary>
+ /// <param name="library">The managed library.</param>
+ /// <returns>The builder.</returns>
+ public AssemblyLoadContextBuilder AddManagedLibrary(ManagedLibrary library)
+ {
+ ValidateRelativePath(library.AdditionalProbingPath);
+
+ if (library.Name.Name != null)
+ {
+ _managedLibraries.Add(library.Name.Name, library);
+ }
+
+ return this;
+ }
+
+ /// <summary>
+ /// Add a native library to the load context.
+ /// </summary>
+ /// <param name="library"></param>
+ /// <returns></returns>
+ public AssemblyLoadContextBuilder AddNativeLibrary(NativeLibrary library)
+ {
+ ValidateRelativePath(library.AppLocalPath);
+ ValidateRelativePath(library.AdditionalProbingPath);
+
+ _nativeLibraries.Add(library.Name, library);
+ return this;
+ }
+
+ /// <summary>
+ /// Add a <paramref name="path"/> that should be used to search for native and managed libraries.
+ /// </summary>
+ /// <param name="path">The file path. Must be a full file path.</param>
+ /// <returns>The builder</returns>
+ public AssemblyLoadContextBuilder AddProbingPath(string path)
+ {
+ if (string.IsNullOrEmpty(path))
+ {
+ throw new ArgumentException("Value must not be null or empty.", nameof(path));
+ }
+
+ if (!Path.IsPathRooted(path))
+ {
+ throw new ArgumentException("Argument must be a full path.", nameof(path));
+ }
+
+ _additionalProbingPaths.Add(path);
+ return this;
+ }
+
+ /// <summary>
+ /// Add a <paramref name="path"/> that should be use to search for resource assemblies (aka satellite assemblies).
+ /// </summary>
+ /// <param name="path">The file path. Must be a full file path.</param>
+ /// <returns>The builder</returns>
+ public AssemblyLoadContextBuilder AddResourceProbingPath(string path)
+ {
+ if (string.IsNullOrEmpty(path))
+ {
+ throw new ArgumentException("Value must not be null or empty.", nameof(path));
+ }
+
+ if (!Path.IsPathRooted(path))
+ {
+ throw new ArgumentException("Argument must be a full path.", nameof(path));
+ }
+
+ _resourceProbingPaths.Add(path);
+ return this;
+ }
+
+
+ /// <summary>
+ /// Enable unloading the assembly load context.
+ /// </summary>
+ /// <returns>The builder</returns>
+ public AssemblyLoadContextBuilder EnableUnloading()
+ {
+ _isCollectible = true;
+ return this;
+ }
+
+ /// <summary>
+ /// Read .dll files into memory to avoid locking the files.
+ /// This is not as efficient, so is not enabled by default, but is required for scenarios
+ /// like hot reloading.
+ /// </summary>
+ /// <returns>The builder</returns>
+ public AssemblyLoadContextBuilder PreloadAssembliesIntoMemory()
+ {
+ _loadInMemory = true; // required to prevent dotnet from locking loaded files
+ return this;
+ }
+
+ /// <summary>
+ /// Shadow copy native libraries (unmanaged DLLs) to avoid locking of these files.
+ /// This is not as efficient, so is not enabled by default, but is required for scenarios
+ /// like hot reloading of plugins dependent on native libraries.
+ /// </summary>
+ /// <returns>The builder</returns>
+ public AssemblyLoadContextBuilder ShadowCopyNativeLibraries()
+ {
+ _shadowCopyNativeLibraries = true;
+ return this;
+ }
+
+ /// <summary>
+ /// Add a <paramref name="path"/> that should be use to search for resource assemblies (aka satellite assemblies)
+ /// relative to any paths specified as <see cref="AddProbingPath"/>
+ /// </summary>
+ /// <param name="path">The file path. Must not be a full file path since it will be appended to additional probing path roots.</param>
+ /// <returns>The builder</returns>
+ internal AssemblyLoadContextBuilder AddResourceProbingSubpath(string path)
+ {
+ if (string.IsNullOrEmpty(path))
+ {
+ throw new ArgumentException("Value must not be null or empty.", nameof(path));
+ }
+
+ if (Path.IsPathRooted(path))
+ {
+ throw new ArgumentException("Argument must be not a full path.", nameof(path));
+ }
+
+ _resourceProbingSubpaths.Add(path);
+ return this;
+ }
+
+ private static void ValidateRelativePath(string probingPath)
+ {
+ if (string.IsNullOrEmpty(probingPath))
+ {
+ throw new ArgumentException("Value must not be null or empty.", nameof(probingPath));
+ }
+
+ if (Path.IsPathRooted(probingPath))
+ {
+ throw new ArgumentException("Argument must be a relative path.", nameof(probingPath));
+ }
+ }
+ }
+}
diff --git a/third-party/DotNetCorePlugins/src/Loader/DependencyContextExtensions.cs b/third-party/DotNetCorePlugins/src/Loader/DependencyContextExtensions.cs
new file mode 100644
index 0000000..fc2d0a9
--- /dev/null
+++ b/third-party/DotNetCorePlugins/src/Loader/DependencyContextExtensions.cs
@@ -0,0 +1,207 @@
+// Copyright (c) Nate McMaster.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices;
+using McMaster.NETCore.Plugins.LibraryModel;
+using Microsoft.Extensions.DependencyModel;
+using NativeLibrary = McMaster.NETCore.Plugins.LibraryModel.NativeLibrary;
+
+namespace McMaster.NETCore.Plugins.Loader
+{
+ /// <summary>
+ /// Extensions for configuring a load context using .deps.json files.
+ /// </summary>
+ public static class DependencyContextExtensions
+ {
+ /// <summary>
+ /// Add dependency information to a load context from a .deps.json file.
+ /// </summary>
+ /// <param name="builder">The builder.</param>
+ /// <param name="depsFilePath">The full path to the .deps.json file.</param>
+ /// <param name="error">An error, if one occurs while reading .deps.json</param>
+ /// <returns>The builder.</returns>
+ public static AssemblyLoadContextBuilder TryAddDependencyContext(this AssemblyLoadContextBuilder builder, string depsFilePath, out Exception? error)
+ {
+ error = null;
+ try
+ {
+ builder.AddDependencyContext(depsFilePath);
+ }
+ catch (Exception ex)
+ {
+ error = ex;
+ }
+
+ return builder;
+ }
+
+ /// <summary>
+ /// Add dependency information to a load context from a .deps.json file.
+ /// </summary>
+ /// <param name="builder">The builder.</param>
+ /// <param name="depsFilePath">The full path to the .deps.json file.</param>
+ /// <returns>The builder.</returns>
+ public static AssemblyLoadContextBuilder AddDependencyContext(this AssemblyLoadContextBuilder builder, string depsFilePath)
+ {
+
+ var reader = new DependencyContextJsonReader();
+ using (var file = File.OpenRead(depsFilePath))
+ {
+ var deps = reader.Read(file);
+ builder.AddDependencyContext(deps);
+ }
+
+ return builder;
+ }
+
+ private static string GetFallbackRid()
+ {
+ // see https://github.com/dotnet/core-setup/blob/b64f7fffbd14a3517186b9a9d5cc001ab6e5bde6/src/corehost/common/pal.h#L53-L73
+
+ string ridBase;
+
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ ridBase = "win10";
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+ {
+ ridBase = "linux";
+
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ {
+ ridBase = "osx.10.12";
+ }
+ else
+ {
+ return "any";
+ }
+
+ return RuntimeInformation.OSArchitecture switch
+ {
+ Architecture.X86 => ridBase + "-x86",
+ Architecture.X64 => ridBase + "-x64",
+ Architecture.Arm => ridBase + "-arm",
+ Architecture.Arm64 => ridBase + "-arm64",
+ _ => ridBase,
+ };
+ }
+
+ /// <summary>
+ /// Add a pre-parsed <see cref="DependencyContext" /> to the load context.
+ /// </summary>
+ /// <param name="builder">The builder.</param>
+ /// <param name="dependencyContext">The dependency context.</param>
+ /// <returns>The builder.</returns>
+ public static AssemblyLoadContextBuilder AddDependencyContext(this AssemblyLoadContextBuilder builder, DependencyContext dependencyContext)
+ {
+ var ridGraph = dependencyContext.RuntimeGraph.Any() || DependencyContext.Default == null
+ ? dependencyContext.RuntimeGraph
+ : DependencyContext.Default.RuntimeGraph;
+
+ var rid = Microsoft.DotNet.PlatformAbstractions.RuntimeEnvironment.GetRuntimeIdentifier();
+ var fallbackRid = GetFallbackRid();
+ var fallbackGraph = ridGraph.FirstOrDefault(g => g.Runtime == rid)
+ ?? ridGraph.FirstOrDefault(g => g.Runtime == fallbackRid)
+ ?? new RuntimeFallbacks("any");
+
+ foreach (var managed in dependencyContext.ResolveRuntimeAssemblies(fallbackGraph))
+ {
+ builder.AddManagedLibrary(managed);
+ }
+
+ foreach (var library in dependencyContext.ResolveResourceAssemblies())
+ {
+ foreach (var resource in library.ResourceAssemblies)
+ {
+ /*
+ * For resource assemblies, look in $packageRoot/$packageId/$version/$resourceGrandparent
+ *
+ * For example, a deps file may contain
+ *
+ * "Example/1.0.0": {
+ * "runtime": {
+ * "lib/netcoreapp2.0/Example.dll": { }
+ * },
+ * "resources": {
+ * "lib/netcoreapp2.0/es/Example.resources.dll": {
+ * "locale": "es"
+ * }
+ * }
+ * }
+ *
+ * In this case, probing should happen in $packageRoot/example/1.0.0/lib/netcoreapp2.0
+ */
+
+ var resourceDir = Path.GetDirectoryName(Path.GetDirectoryName(resource.Path));
+
+ if (resourceDir != null)
+ {
+ var path = Path.Combine(library.Name.ToLowerInvariant(),
+ library.Version,
+ resourceDir);
+
+ builder.AddResourceProbingSubpath(path);
+ }
+ }
+ }
+
+ foreach (var native in dependencyContext.ResolveNativeAssets(fallbackGraph))
+ {
+ builder.AddNativeLibrary(native);
+ }
+
+ return builder;
+ }
+
+ private static IEnumerable<ManagedLibrary> ResolveRuntimeAssemblies(this DependencyContext depContext, RuntimeFallbacks runtimeGraph)
+ {
+ var rids = GetRids(runtimeGraph);
+ return from library in depContext.RuntimeLibraries
+ from assetPath in SelectAssets(rids, library.RuntimeAssemblyGroups)
+ select ManagedLibrary.CreateFromPackage(library.Name, library.Version, assetPath);
+ }
+
+ private static IEnumerable<RuntimeLibrary> ResolveResourceAssemblies(this DependencyContext depContext)
+ {
+ return from library in depContext.RuntimeLibraries
+ where library.ResourceAssemblies != null && library.ResourceAssemblies.Count > 0
+ select library;
+ }
+
+ private static IEnumerable<NativeLibrary> ResolveNativeAssets(this DependencyContext depContext, RuntimeFallbacks runtimeGraph)
+ {
+ var rids = GetRids(runtimeGraph);
+ return from library in depContext.RuntimeLibraries
+ from assetPath in SelectAssets(rids, library.NativeLibraryGroups)
+ // some packages include symbols alongside native assets, such as System.Native.a or pwshplugin.pdb
+ where PlatformInformation.NativeLibraryExtensions.Contains(Path.GetExtension(assetPath), StringComparer.OrdinalIgnoreCase)
+ select NativeLibrary.CreateFromPackage(library.Name, library.Version, assetPath);
+ }
+
+ private static IEnumerable<string> GetRids(RuntimeFallbacks runtimeGraph)
+ {
+ return new[] { runtimeGraph.Runtime }.Concat(runtimeGraph?.Fallbacks ?? Enumerable.Empty<string>());
+ }
+
+ private static IEnumerable<string> SelectAssets(IEnumerable<string> rids, IEnumerable<RuntimeAssetGroup> groups)
+ {
+ foreach (var rid in rids)
+ {
+ var group = groups.FirstOrDefault(g => g.Runtime == rid);
+ if (group != null)
+ {
+ return group.AssetPaths;
+ }
+ }
+
+ // Return the RID-agnostic group
+ return groups.GetDefaultAssets();
+ }
+ }
+}
diff --git a/third-party/DotNetCorePlugins/src/Loader/ManagedLoadContext.cs b/third-party/DotNetCorePlugins/src/Loader/ManagedLoadContext.cs
new file mode 100644
index 0000000..01b985e
--- /dev/null
+++ b/third-party/DotNetCorePlugins/src/Loader/ManagedLoadContext.cs
@@ -0,0 +1,386 @@
+// Copyright (c) Nate McMaster.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+/*
+ * Modifications Copyright (c) 2024 Vaughn Nugent
+ *
+ * Changes:
+ * - Removed lazy loading and hot-reload features
+ */
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.Loader;
+
+using McMaster.NETCore.Plugins.LibraryModel;
+
+namespace McMaster.NETCore.Plugins.Loader
+{
+ /// <summary>
+ /// An implementation of <see cref="AssemblyLoadContext" /> which attempts to load managed and native
+ /// binaries at runtime immitating some of the behaviors of corehost.
+ /// </summary>
+ [DebuggerDisplay("'{Name}' ({_mainAssemblyPath})")]
+ internal class ManagedLoadContext : AssemblyLoadContext
+ {
+ private readonly string _basePath;
+ private readonly string _mainAssemblyPath;
+ private readonly IReadOnlyDictionary<string, ManagedLibrary> _managedAssemblies;
+ private readonly IReadOnlyDictionary<string, NativeLibrary> _nativeLibraries;
+ private readonly IReadOnlyCollection<string> _privateAssemblies;
+ private readonly ICollection<string> _defaultAssemblies;
+ private readonly IReadOnlyCollection<string> _additionalProbingPaths;
+ private readonly bool _preferDefaultLoadContext;
+ private readonly string[] _resourceRoots;
+ private readonly bool _loadInMemory;
+ private readonly AssemblyLoadContext _defaultLoadContext;
+ private readonly AssemblyDependencyResolver _dependencyResolver;
+ private readonly bool _shadowCopyNativeLibraries;
+ private readonly string _unmanagedDllShadowCopyDirectoryPath;
+
+ public ManagedLoadContext(string mainAssemblyPath,
+ IReadOnlyDictionary<string, ManagedLibrary> managedAssemblies,
+ IReadOnlyDictionary<string, NativeLibrary> nativeLibraries,
+ IReadOnlyCollection<string> privateAssemblies,
+ IReadOnlyCollection<string> defaultAssemblies,
+ IReadOnlyCollection<string> additionalProbingPaths,
+ IReadOnlyCollection<string> resourceProbingPaths,
+ AssemblyLoadContext defaultLoadContext,
+ bool preferDefaultLoadContext,
+ bool isCollectible,
+ bool loadInMemory,
+ bool shadowCopyNativeLibraries)
+ : base(Path.GetFileNameWithoutExtension(mainAssemblyPath), isCollectible)
+
+ {
+ ArgumentNullException.ThrowIfNull(resourceProbingPaths);
+
+ _mainAssemblyPath = mainAssemblyPath ?? throw new ArgumentNullException(nameof(mainAssemblyPath));
+ _dependencyResolver = new AssemblyDependencyResolver(mainAssemblyPath);
+ _basePath = Path.GetDirectoryName(mainAssemblyPath) ?? throw new ArgumentException(nameof(mainAssemblyPath));
+ _managedAssemblies = managedAssemblies ?? throw new ArgumentNullException(nameof(managedAssemblies));
+ _privateAssemblies = privateAssemblies ?? throw new ArgumentNullException(nameof(privateAssemblies));
+ _defaultAssemblies = defaultAssemblies != null ? defaultAssemblies.ToList() : throw new ArgumentNullException(nameof(defaultAssemblies));
+ _nativeLibraries = nativeLibraries ?? throw new ArgumentNullException(nameof(nativeLibraries));
+ _additionalProbingPaths = additionalProbingPaths ?? throw new ArgumentNullException(nameof(additionalProbingPaths));
+ _defaultLoadContext = defaultLoadContext;
+ _preferDefaultLoadContext = preferDefaultLoadContext;
+ _loadInMemory = loadInMemory;
+
+ _resourceRoots = new[] { _basePath }
+ .Concat(resourceProbingPaths)
+ .ToArray();
+
+ _shadowCopyNativeLibraries = shadowCopyNativeLibraries;
+ _unmanagedDllShadowCopyDirectoryPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
+
+ if (shadowCopyNativeLibraries)
+ {
+ Unloading += _ => OnUnloaded();
+ }
+ }
+
+ /// <summary>
+ /// Load an assembly.
+ /// </summary>
+ /// <param name="assemblyName"></param>
+ /// <returns></returns>
+ protected override Assembly? Load(AssemblyName assemblyName)
+ {
+ if (assemblyName.Name == null)
+ {
+ // not sure how to handle this case. It's technically possible.
+ return null;
+ }
+
+ if ((_preferDefaultLoadContext || _defaultAssemblies.Contains(assemblyName.Name)) && !_privateAssemblies.Contains(assemblyName.Name))
+ {
+ // If default context is preferred, check first for types in the default context unless the dependency has been declared as private
+ try
+ {
+ var defaultAssembly = _defaultLoadContext.LoadFromAssemblyName(assemblyName);
+ if (defaultAssembly != null)
+ {
+ // Older versions used to return null here such that returned assembly would be resolved from the default ALC.
+ // However, with the addition of custom default ALCs, the Default ALC may not be the user's chosen ALC when
+ // this context was built. As such, we simply return the Assembly from the user's chosen default load context.
+ return defaultAssembly;
+ }
+ }
+ catch
+ {
+ // Swallow errors in loading from the default context
+ }
+ }
+
+ var resolvedPath = _dependencyResolver.ResolveAssemblyToPath(assemblyName);
+ if (!string.IsNullOrEmpty(resolvedPath) && File.Exists(resolvedPath))
+ {
+ return LoadAssemblyFromFilePath(resolvedPath);
+ }
+
+ // Resource assembly binding does not use the TPA. Instead, it probes PLATFORM_RESOURCE_ROOTS (a list of folders)
+ // for $folder/$culture/$assemblyName.dll
+ // See https://github.com/dotnet/coreclr/blob/3fca50a36e62a7433d7601d805d38de6baee7951/src/binder/assemblybinder.cpp#L1232-L1290
+
+ if (!string.IsNullOrEmpty(assemblyName.CultureName) && !string.Equals("neutral", assemblyName.CultureName))
+ {
+ foreach (var resourceRoot in _resourceRoots)
+ {
+ var resourcePath = Path.Combine(resourceRoot, assemblyName.CultureName, assemblyName.Name + ".dll");
+ if (File.Exists(resourcePath))
+ {
+ return LoadAssemblyFromFilePath(resourcePath);
+ }
+ }
+
+ return null;
+ }
+
+ if (_managedAssemblies.TryGetValue(assemblyName.Name, out var library) && library != null)
+ {
+ if (SearchForLibrary(library, out var path) && path != null)
+ {
+ return LoadAssemblyFromFilePath(path);
+ }
+ }
+ else
+ {
+ // if an assembly was not listed in the list of known assemblies,
+ // fallback to the load context base directory
+ var dllName = assemblyName.Name + ".dll";
+ foreach (var probingPath in _additionalProbingPaths.Prepend(_basePath))
+ {
+ var localFile = Path.Combine(probingPath, dllName);
+ if (File.Exists(localFile))
+ {
+ return LoadAssemblyFromFilePath(localFile);
+ }
+ }
+ }
+
+ return null;
+ }
+
+ public Assembly LoadAssemblyFromFilePath(string path)
+ {
+ if (!_loadInMemory)
+ {
+ return LoadFromAssemblyPath(path);
+ }
+
+ using var file = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read);
+ var pdbPath = Path.ChangeExtension(path, ".pdb");
+ if (File.Exists(pdbPath))
+ {
+ using var pdbFile = File.Open(pdbPath, FileMode.Open, FileAccess.Read, FileShare.Read);
+ return LoadFromStream(file, pdbFile);
+ }
+ return LoadFromStream(file);
+
+ }
+
+ /// <summary>
+ /// Loads the unmanaged binary using configured list of native libraries.
+ /// </summary>
+ /// <param name="unmanagedDllName"></param>
+ /// <returns></returns>
+ protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
+ {
+ var resolvedPath = _dependencyResolver.ResolveUnmanagedDllToPath(unmanagedDllName);
+ if (!string.IsNullOrEmpty(resolvedPath) && File.Exists(resolvedPath))
+ {
+ return LoadUnmanagedDllFromResolvedPath(resolvedPath, normalizePath: false);
+ }
+
+ foreach (var prefix in PlatformInformation.NativeLibraryPrefixes)
+ {
+ if (_nativeLibraries.TryGetValue(prefix + unmanagedDllName, out var library))
+ {
+ if (SearchForLibrary(library, prefix, out var path) && path != null)
+ {
+ return LoadUnmanagedDllFromResolvedPath(path);
+ }
+ }
+ else
+ {
+ // coreclr allows code to use [DllImport("sni")] or [DllImport("sni.dll")]
+ // This library treats the file name without the extension as the lookup name,
+ // so this loop is necessary to check if the unmanaged name matches a library
+ // when the file extension has been trimmed.
+ foreach (var suffix in PlatformInformation.NativeLibraryExtensions)
+ {
+ if (!unmanagedDllName.EndsWith(suffix, StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
+ // check to see if there is a library entry for the library without the file extension
+ var trimmedName = unmanagedDllName.Substring(0, unmanagedDllName.Length - suffix.Length);
+
+ if (_nativeLibraries.TryGetValue(prefix + trimmedName, out library))
+ {
+ if (SearchForLibrary(library, prefix, out var path) && path != null)
+ {
+ return LoadUnmanagedDllFromResolvedPath(path);
+ }
+ }
+ else
+ {
+ // fallback to native assets which match the file name in the plugin base directory
+ var prefixSuffixDllName = prefix + unmanagedDllName + suffix;
+ var prefixDllName = prefix + unmanagedDllName;
+
+ foreach (var probingPath in _additionalProbingPaths.Prepend(_basePath))
+ {
+ var localFile = Path.Combine(probingPath, prefixSuffixDllName);
+ if (File.Exists(localFile))
+ {
+ return LoadUnmanagedDllFromResolvedPath(localFile);
+ }
+
+ var localFileWithoutSuffix = Path.Combine(probingPath, prefixDllName);
+ if (File.Exists(localFileWithoutSuffix))
+ {
+ return LoadUnmanagedDllFromResolvedPath(localFileWithoutSuffix);
+ }
+ }
+
+ }
+ }
+
+ }
+ }
+
+ return base.LoadUnmanagedDll(unmanagedDllName);
+ }
+
+ private bool SearchForLibrary(ManagedLibrary library, out string? path)
+ {
+ // 1. Check for in _basePath + app local path
+ var localFile = Path.Combine(_basePath, library.AppLocalPath);
+ if (File.Exists(localFile))
+ {
+ path = localFile;
+ return true;
+ }
+
+ // 2. Search additional probing paths
+ foreach (var searchPath in _additionalProbingPaths)
+ {
+ var candidate = Path.Combine(searchPath, library.AdditionalProbingPath);
+ if (File.Exists(candidate))
+ {
+ path = candidate;
+ return true;
+ }
+ }
+
+ // 3. Search in base path
+ foreach (var ext in PlatformInformation.ManagedAssemblyExtensions)
+ {
+ var local = Path.Combine(_basePath, library.Name.Name + ext);
+ if (File.Exists(local))
+ {
+ path = local;
+ return true;
+ }
+ }
+
+ path = null;
+ return false;
+ }
+
+ private bool SearchForLibrary(NativeLibrary library, string prefix, out string? path)
+ {
+ // 1. Search in base path
+ foreach (var ext in PlatformInformation.NativeLibraryExtensions)
+ {
+ var candidate = Path.Combine(_basePath, $"{prefix}{library.Name}{ext}");
+ if (File.Exists(candidate))
+ {
+ path = candidate;
+ return true;
+ }
+ }
+
+ // 2. Search in base path + app local (for portable deployments of netcoreapp)
+ var local = Path.Combine(_basePath, library.AppLocalPath);
+ if (File.Exists(local))
+ {
+ path = local;
+ return true;
+ }
+
+ // 3. Search additional probing paths
+ foreach (var searchPath in _additionalProbingPaths)
+ {
+ var candidate = Path.Combine(searchPath, library.AdditionalProbingPath);
+ if (File.Exists(candidate))
+ {
+ path = candidate;
+ return true;
+ }
+ }
+
+ path = null;
+ return false;
+ }
+
+ private IntPtr LoadUnmanagedDllFromResolvedPath(string unmanagedDllPath, bool normalizePath = true)
+ {
+ if (normalizePath)
+ {
+ unmanagedDllPath = Path.GetFullPath(unmanagedDllPath);
+ }
+
+ return _shadowCopyNativeLibraries
+ ? LoadUnmanagedDllFromShadowCopy(unmanagedDllPath)
+ : LoadUnmanagedDllFromPath(unmanagedDllPath);
+ }
+
+ private IntPtr LoadUnmanagedDllFromShadowCopy(string unmanagedDllPath)
+ {
+ var shadowCopyDllPath = CreateShadowCopy(unmanagedDllPath);
+
+ return LoadUnmanagedDllFromPath(shadowCopyDllPath);
+ }
+
+ private string CreateShadowCopy(string dllPath)
+ {
+ Directory.CreateDirectory(_unmanagedDllShadowCopyDirectoryPath);
+
+ var dllFileName = Path.GetFileName(dllPath);
+ var shadowCopyPath = Path.Combine(_unmanagedDllShadowCopyDirectoryPath, dllFileName);
+
+ if (!File.Exists(shadowCopyPath))
+ {
+ File.Copy(dllPath, shadowCopyPath);
+ }
+
+ return shadowCopyPath;
+ }
+
+ private void OnUnloaded()
+ {
+ if (!_shadowCopyNativeLibraries || !Directory.Exists(_unmanagedDllShadowCopyDirectoryPath))
+ {
+ return;
+ }
+
+ // Attempt to delete shadow copies
+ try
+ {
+ Directory.Delete(_unmanagedDllShadowCopyDirectoryPath, recursive: true);
+ }
+ catch (Exception)
+ {
+ // Files might be locked by host process. Nothing we can do about it, I guess.
+ }
+ }
+ }
+}
diff --git a/third-party/DotNetCorePlugins/src/Loader/RuntimeConfigExtensions.cs b/third-party/DotNetCorePlugins/src/Loader/RuntimeConfigExtensions.cs
new file mode 100644
index 0000000..679c56a
--- /dev/null
+++ b/third-party/DotNetCorePlugins/src/Loader/RuntimeConfigExtensions.cs
@@ -0,0 +1,129 @@
+// Copyright (c) Nate McMaster.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Text.Json;
+
+namespace McMaster.NETCore.Plugins.Loader
+{
+ /// <summary>
+ /// Extensions for creating a load context using settings from a runtimeconfig.json file
+ /// </summary>
+ public static class RuntimeConfigExtensions
+ {
+ private const string JsonExt = ".json";
+ private static readonly JsonSerializerOptions s_serializerOptions = new()
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+ };
+
+ /// <summary>
+ /// Adds additional probing paths to a managed load context using settings found in the runtimeconfig.json
+ /// and runtimeconfig.dev.json files.
+ /// </summary>
+ /// <param name="builder">The context builder</param>
+ /// <param name="runtimeConfigPath">The path to the runtimeconfig.json file</param>
+ /// <param name="includeDevConfig">Also read runtimeconfig.dev.json file, if present.</param>
+ /// <param name="error">The error, if one occurs while parsing runtimeconfig.json</param>
+ /// <returns>The builder.</returns>
+ public static AssemblyLoadContextBuilder TryAddAdditionalProbingPathFromRuntimeConfig(
+ this AssemblyLoadContextBuilder builder,
+ string runtimeConfigPath,
+ bool includeDevConfig,
+ out Exception? error)
+ {
+ error = null;
+ try
+ {
+ var config = TryReadConfig(runtimeConfigPath);
+ if (config == null)
+ {
+ return builder;
+ }
+
+ RuntimeConfig? devConfig = null;
+ if (includeDevConfig)
+ {
+ var configDevPath = runtimeConfigPath.Substring(0, runtimeConfigPath.Length - JsonExt.Length) + ".dev.json";
+ devConfig = TryReadConfig(configDevPath);
+ }
+
+ var tfm = config.runtimeOptions?.Tfm ?? devConfig?.runtimeOptions?.Tfm;
+
+ if (config.runtimeOptions != null)
+ {
+ AddProbingPaths(builder, config.runtimeOptions, tfm);
+ }
+
+ if (devConfig?.runtimeOptions != null)
+ {
+ AddProbingPaths(builder, devConfig.runtimeOptions, tfm);
+ }
+
+ if (tfm != null)
+ {
+ var dotnet = Process.GetCurrentProcess().MainModule.FileName;
+ if (string.Equals(Path.GetFileNameWithoutExtension(dotnet), "dotnet", StringComparison.OrdinalIgnoreCase))
+ {
+ var dotnetHome = Path.GetDirectoryName(dotnet);
+ if (dotnetHome != null)
+ {
+ builder.AddProbingPath(Path.Combine(dotnetHome, "store", RuntimeInformation.OSArchitecture.ToString().ToLowerInvariant(), tfm));
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ error = ex;
+ }
+ return builder;
+ }
+
+ private static void AddProbingPaths(AssemblyLoadContextBuilder builder, RuntimeOptions options, string? tfm)
+ {
+ if (options.AdditionalProbingPaths == null)
+ {
+ return;
+ }
+
+ foreach (var item in options.AdditionalProbingPaths)
+ {
+ var path = item;
+ if (path.Contains("|arch|"))
+ {
+ path = path.Replace("|arch|", RuntimeInformation.OSArchitecture.ToString().ToLowerInvariant());
+ }
+
+ if (path.Contains("|tfm|"))
+ {
+ if (tfm == null)
+ {
+ // We don't have enough information to parse this
+ continue;
+ }
+
+ path = path.Replace("|tfm|", tfm);
+ }
+
+ builder.AddProbingPath(path);
+ }
+ }
+
+ private static RuntimeConfig? TryReadConfig(string path)
+ {
+ try
+ {
+ var file = File.ReadAllBytes(path);
+ return JsonSerializer.Deserialize<RuntimeConfig>(file, s_serializerOptions);
+ }
+ catch
+ {
+ return null;
+ }
+ }
+ }
+}
diff --git a/third-party/DotNetCorePlugins/src/McMaster.NETCore.Plugins.csproj b/third-party/DotNetCorePlugins/src/McMaster.NETCore.Plugins.csproj
new file mode 100644
index 0000000..fbc293c
--- /dev/null
+++ b/third-party/DotNetCorePlugins/src/McMaster.NETCore.Plugins.csproj
@@ -0,0 +1,26 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFrameworks>net8.0</TargetFrameworks>
+ <OutputType>library</OutputType>
+ <Nullable>enable</Nullable>
+ <IsPublishable>False</IsPublishable>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
+ <PackageDescription>Provides API for dynamically loading assemblies into a .NET application.
+
+This package should be used by the host application which needs to load plugins.
+See https://github.com/natemcmaster/DotNetCorePlugins/blob/main/README.md for more samples and documentation.
+ </PackageDescription>
+ <PackageTags>.NET Core;plugins</PackageTags>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.DotNet.PlatformAbstractions" Version="3.1.6" />
+ <PackageReference Include="Microsoft.Extensions.DependencyModel" Version="8.0.0" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\..\lib\Utils\src\VNLib.Utils.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/third-party/DotNetCorePlugins/src/PluginConfig.cs b/third-party/DotNetCorePlugins/src/PluginConfig.cs
new file mode 100644
index 0000000..d9f781c
--- /dev/null
+++ b/third-party/DotNetCorePlugins/src/PluginConfig.cs
@@ -0,0 +1,94 @@
+// Copyright (c) Nate McMaster.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+/*
+ * Modifications Copyright (c) 2024 Vaughn Nugent
+ *
+ * Changes:
+ * - Removed unloadable feature as an optional pragma (aka always on)
+ * - Removed hot reload as an option
+ * - Used net 8.0 auto properties instead of field indexers
+ * - Removed experimental lazy loading since it's not safe for my use cases
+ */
+
+using System;
+using System.IO;
+using System.Reflection;
+using System.Runtime.Loader;
+using System.Collections.Generic;
+
+namespace McMaster.NETCore.Plugins
+{
+ /// <summary>
+ /// Represents the configuration for a .NET Core plugin.
+ /// </summary>
+ public class PluginConfig
+ {
+ /// <summary>
+ /// Initializes a new instance of <see cref="PluginConfig" />
+ /// </summary>
+ /// <param name="mainAssemblyPath">The full file path to the main assembly for the plugin.</param>
+ public PluginConfig(string mainAssemblyPath)
+ {
+ if (string.IsNullOrEmpty(mainAssemblyPath))
+ {
+ throw new ArgumentException("Value must be null or not empty", nameof(mainAssemblyPath));
+ }
+
+ if (!Path.IsPathRooted(mainAssemblyPath))
+ {
+ throw new ArgumentException("Value must be an absolute file path", nameof(mainAssemblyPath));
+ }
+
+ MainAssemblyPath = mainAssemblyPath;
+ }
+
+ /// <summary>
+ /// The file path to the main assembly.
+ /// </summary>
+ public string MainAssemblyPath { get; }
+
+ /// <summary>
+ /// A list of assemblies which should be treated as private.
+ /// </summary>
+ public ICollection<AssemblyName> PrivateAssemblies { get; protected set; } = new List<AssemblyName>();
+
+ /// <summary>
+ /// A list of assemblies which should be unified between the host and the plugin.
+ /// </summary>
+ /// <seealso href="https://github.com/natemcmaster/DotNetCorePlugins/blob/main/docs/what-are-shared-types.md">
+ /// https://github.com/natemcmaster/DotNetCorePlugins/blob/main/docs/what-are-shared-types.md
+ /// </seealso>
+ public ICollection<AssemblyName> SharedAssemblies { get; protected set; } = new List<AssemblyName>();
+
+ /// <summary>
+ /// Attempt to unify all types from a plugin with the host.
+ /// <para>
+ /// This does not guarantee types will unify.
+ /// </para>
+ /// <seealso href="https://github.com/natemcmaster/DotNetCorePlugins/blob/main/docs/what-are-shared-types.md">
+ /// https://github.com/natemcmaster/DotNetCorePlugins/blob/main/docs/what-are-shared-types.md
+ /// </seealso>
+ /// </summary>
+ public bool PreferSharedTypes { get; set; }
+
+ /// <summary>
+ /// If set, replaces the default <see cref="AssemblyLoadContext"/> used by the <see cref="PluginLoader"/>.
+ /// Use this feature if the <see cref="AssemblyLoadContext"/> of the <see cref="Assembly"/> is not the Runtime's default load context.
+ /// i.e. (AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly) != <see cref="AssemblyLoadContext.Default"/>
+ /// </summary>
+ public AssemblyLoadContext DefaultContext { get; set; } = AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly()) ?? AssemblyLoadContext.Default;
+
+ /// <summary>
+ /// The plugin can be unloaded from memory.
+ /// </summary>
+ public bool IsUnloadable { get; set; }
+
+ /// <summary>
+ /// Loads assemblies into memory in order to not lock files.
+ /// As example use case here would be: no hot reloading but able to
+ /// replace files and reload manually at later time
+ /// </summary>
+ public bool LoadInMemory { get; set; }
+ }
+}
diff --git a/third-party/DotNetCorePlugins/src/PluginLoader.cs b/third-party/DotNetCorePlugins/src/PluginLoader.cs
new file mode 100644
index 0000000..cbe46f8
--- /dev/null
+++ b/third-party/DotNetCorePlugins/src/PluginLoader.cs
@@ -0,0 +1,148 @@
+// Copyright (c) Nate McMaster.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+/*
+ * Modifications Copyright (c) 2024 Vaughn Nugent
+ *
+ * Changes:
+ * - Removed unloadable feature as an optional pragma (aka always on)
+ * - Removed internal hot-reload since my plugin runtime handles this feature better
+ * - Removed all static creation functions as plugin config is far more direct and simple
+ * - Removed old/depricated features such as native deps resolution since thats handled now
+ * - Remove experimenal lazy loading
+ * - Only store the main asm file path instead of the mutable config instance
+ */
+
+using System;
+using System.Reflection;
+using System.Runtime.Loader;
+
+using McMaster.NETCore.Plugins.Loader;
+
+namespace McMaster.NETCore.Plugins
+{
+ /// <summary>
+ /// This loader attempts to load binaries for execution (both managed assemblies and native libraries)
+ /// in the same way that .NET Core would if they were originally part of the .NET Core application.
+ /// <para>
+ /// This loader reads configuration files produced by .NET Core (.deps.json and runtimeconfig.json)
+ /// as well as a custom file (*.config files). These files describe a list of .dlls and a set of dependencies.
+ /// The loader searches the plugin path, as well as any additionally specified paths, for binaries
+ /// which satisfy the plugin's requirements.
+ /// </para>
+ /// </summary>
+ public class PluginLoader
+ {
+
+ private readonly string _mainAssemblyPath;
+ private readonly AssemblyLoadContextBuilder _contextBuilder;
+
+ private ManagedLoadContext _context;
+
+ /// <summary>
+ /// Initialize an instance of <see cref="PluginLoader" />
+ /// </summary>
+ /// <param name="config">The configuration for the plugin.</param>
+ public PluginLoader(PluginConfig config)
+ {
+ ArgumentNullException.ThrowIfNull(config);
+ ArgumentException.ThrowIfNullOrWhiteSpace(config.MainAssemblyPath);
+
+ _mainAssemblyPath = config.MainAssemblyPath;
+ _contextBuilder = CreateLoadContextBuilder(config);
+ }
+
+ /// <summary>
+ /// True when this plugin is capable of being unloaded.
+ /// </summary>
+ public bool IsUnloadable => _context.IsCollectible;
+
+ /// <summary>
+ /// Initializes the new load context
+ /// </summary>
+ public void Load() => _context = (ManagedLoadContext)_contextBuilder.Build();
+
+ internal AssemblyLoadContext LoadContext => _context;
+
+ /// <summary>
+ /// Load the main assembly for the plugin.
+ /// </summary>
+ public Assembly LoadDefaultAssembly() => _context.LoadAssemblyFromFilePath(_mainAssemblyPath);
+
+ /// <summary>
+ /// Load an assembly by name.
+ /// </summary>
+ /// <param name="assemblyName">The assembly name.</param>
+ /// <returns>The assembly.</returns>
+ public Assembly LoadAssembly(AssemblyName assemblyName) => _context.LoadFromAssemblyName(assemblyName);
+
+ /// <summary>
+ /// Sets the scope used by some System.Reflection APIs which might trigger assembly loading.
+ /// <para>
+ /// See https://github.com/dotnet/coreclr/blob/v3.0.0/Documentation/design-docs/AssemblyLoadContext.ContextualReflection.md for more details.
+ /// </para>
+ /// </summary>
+ /// <returns></returns>
+ public AssemblyLoadContext.ContextualReflectionScope EnterContextualReflection()
+ => _context.EnterContextualReflection();
+
+ /// <summary>
+ /// Unloads the internal assembly load context
+ /// </summary>
+ /// <param name="invokeGc">A value that indicates if a garbage collection should be run</param>
+ /// <exception cref="InvalidOperationException"></exception>
+ public void Destroy(bool invokeGc)
+ {
+ if (!IsUnloadable)
+ {
+ throw new InvalidOperationException("The current assembly context cannot be unloaded");
+ }
+
+ _context.Unload();
+
+ //Optionally wait for GC to finish
+ if (invokeGc)
+ {
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ }
+ }
+
+
+ private static AssemblyLoadContextBuilder CreateLoadContextBuilder(PluginConfig config)
+ {
+ var builder = new AssemblyLoadContextBuilder();
+
+ builder.SetMainAssemblyPath(config.MainAssemblyPath);
+ builder.SetDefaultContext(config.DefaultContext);
+
+ foreach (var ext in config.PrivateAssemblies)
+ {
+ builder.PreferLoadContextAssembly(ext);
+ }
+
+ if (config.PreferSharedTypes)
+ {
+ builder.PreferDefaultLoadContext(true);
+ }
+
+ if (config.IsUnloadable)
+ {
+ builder.EnableUnloading();
+ }
+
+ if (config.LoadInMemory)
+ {
+ builder.PreloadAssembliesIntoMemory();
+ builder.ShadowCopyNativeLibraries();
+ }
+
+ foreach (var assemblyName in config.SharedAssemblies)
+ {
+ builder.PreferDefaultLoadContextAssembly(assemblyName);
+ }
+
+ return builder;
+ }
+ }
+}
diff --git a/third-party/DotNetCorePlugins/src/Properties/AssemblyInfo.cs b/third-party/DotNetCorePlugins/src/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..7ed81e4
--- /dev/null
+++ b/third-party/DotNetCorePlugins/src/Properties/AssemblyInfo.cs
@@ -0,0 +1,6 @@
+// Copyright (c) Nate McMaster.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("McMaster.NETCore.Plugins.Tests, PublicKey=00240000048000009400000006020000002400005253413100040000010001001df0eba4297c8ffdf114a13714ad787744619dfb18e29191703f6f782d6a09e4a4cac35b8c768cbbd9ade8197bc0f66ec66fabc9071a206c8060af8b7a332236968d3ee44b90bd2f30d0edcb6150555c6f8d988e48234debaf2d427a08d7c06ba1343411142dc8ac996f7f7dbe0e93d13f17a7624db5400510e6144b0fd683b9")]
diff --git a/third-party/DotNetCorePlugins/test/Plugins.Tests/BasicAssemblyLoaderTests.cs b/third-party/DotNetCorePlugins/test/Plugins.Tests/BasicAssemblyLoaderTests.cs
new file mode 100644
index 0000000..c094951
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/Plugins.Tests/BasicAssemblyLoaderTests.cs
@@ -0,0 +1,160 @@
+// Copyright (c) Nate McMaster.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using McMaster.Extensions.Xunit;
+using Test.Referenced.Library;
+using Xunit;
+
+namespace McMaster.NETCore.Plugins.Tests
+{
+ public class BasicAssemblyLoaderTests
+ {
+#if !NETCOREAPP2_1
+ [Fact]
+ public void PluginLoaderCanUnload()
+ {
+ var path = TestResources.GetTestProjectAssembly("NetCoreApp2App");
+
+ // See https://github.com/dotnet/coreclr/pull/22221
+
+ ExecuteAndUnload(path, out var weakRef);
+
+ // Force a GC collect to ensure unloaded has completed
+ for (var i = 0; weakRef.IsAlive && (i < 10); i++)
+ {
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ }
+
+ Assert.False(weakRef.IsAlive);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)] // ensure no local vars are create
+ private void ExecuteAndUnload(string path, out WeakReference weakRef)
+ {
+ var loader = PluginLoader.CreateFromAssemblyFile(path, c => { c.IsUnloadable = true; });
+ var assembly = loader.LoadDefaultAssembly();
+
+ var method = assembly
+ .GetType("NetCoreApp2App.Program", throwOnError: true)!
+ .GetMethod("GetGreeting", BindingFlags.Static | BindingFlags.Public);
+
+ Assert.True(loader.IsUnloadable);
+ Assert.NotNull(method);
+ Assert.Equal("Hello world!", method!.Invoke(null, Array.Empty<object>()));
+ loader.Dispose();
+ Assert.Throws<ObjectDisposedException>(() => loader.LoadDefaultAssembly());
+
+ weakRef = new WeakReference(loader.LoadContext, trackResurrection: true);
+ }
+#endif
+
+ [Fact]
+ public void LoadsNetCoreProjectWithNativeDeps()
+ {
+ var path = TestResources.GetTestProjectAssembly("PowerShellPlugin");
+ var loader = PluginLoader.CreateFromAssemblyFile(path);
+ var assembly = loader.LoadDefaultAssembly();
+
+ var method = assembly
+ .GetType("PowerShellPlugin.Program", throwOnError: true)!
+ .GetMethod("GetGreeting", BindingFlags.Static | BindingFlags.Public);
+ Assert.NotNull(method);
+ Assert.Equal("hello", method!.Invoke(null, Array.Empty<object>()));
+ }
+
+ [SkippableFact]
+ [SkipOnOS(OS.Linux | OS.MacOS)]
+ public void LoadsNativeDependenciesWhenDllImportUsesFilename()
+ {
+ // SqlClient has P/invoke that calls "sni.dll" on Windows. This test checks
+ // that native libraries can still be resolved in this case.
+ var path = TestResources.GetTestProjectAssembly("SqlClientApp");
+ var loader = PluginLoader.CreateFromAssemblyFile(path);
+ var assembly = loader.LoadDefaultAssembly();
+
+ var method = assembly
+ .GetType("SqlClientApp.Program", throwOnError: true)!
+ .GetMethod("Run", BindingFlags.Static | BindingFlags.Public);
+ Assert.NotNull(method);
+ Assert.Equal(true, method!.Invoke(null, Array.Empty<object>()));
+ }
+
+ [Fact]
+ public void LoadsNetCoreApp2Project()
+ {
+ var path = TestResources.GetTestProjectAssembly("NetCoreApp2App");
+ var loader = PluginLoader.CreateFromAssemblyFile(path);
+ var assembly = loader.LoadDefaultAssembly();
+
+ var method = assembly
+ .GetType("NetCoreApp2App.Program", throwOnError: true)!
+ .GetMethod("GetGreeting", BindingFlags.Static | BindingFlags.Public);
+ Assert.NotNull(method);
+ Assert.Equal("Hello world!", method!.Invoke(null, Array.Empty<object>()));
+ }
+
+ [Fact]
+ public void LoadsNetStandard20Project()
+ {
+ var path = TestResources.GetTestProjectAssembly("NetStandardClassLib");
+ var loader = PluginLoader.CreateFromAssemblyFile(path);
+ var assembly = loader.LoadDefaultAssembly();
+
+ var type = assembly.GetType("NetStandardClassLib.Class1", throwOnError: true);
+ var method = type!.GetMethod("GetColor", BindingFlags.Instance | BindingFlags.Public);
+ Assert.NotNull(method);
+ Assert.Equal("Red", method!.Invoke(Activator.CreateInstance(type), Array.Empty<object>()));
+ }
+
+ [Fact]
+ public void ItPrefersRuntimeSpecificManagedAssetsOverRidlessOnes()
+ {
+ // System.Drawing.Common is an example of a package which has both rid-specific and ridless versions
+ // The package has lib/netstandard2.0/System.Drawing.Common.dll, but also has runtimes/{win,unix}/lib/netcoreapp2.0/System.Drawing.Common.dll
+ // In this case, the host will pick the rid-specific version
+
+ var path = TestResources.GetTestProjectAssembly("DrawingApp");
+ var loader = PluginLoader.CreateFromAssemblyFile(path);
+ var assembly = loader.LoadDefaultAssembly();
+
+ var type = assembly.GetType("Finder", throwOnError: true)!;
+ var method = type.GetMethod("FindDrawingAssembly", BindingFlags.Static | BindingFlags.Public);
+ Assert.NotNull(method);
+ Assert.Contains("runtimes", (string?)method!.Invoke(null, Array.Empty<object>()));
+ }
+
+ [Fact]
+ [UseCulture("es")]
+ public void ItLoadsSatelliteAssemblies()
+ {
+ var fruit = GetPlátano();
+ Assert.Equal("Plátano", fruit.GetFlavor());
+ }
+
+ [Fact]
+ [UseCulture("en")]
+ public void ItLoadsDefaultCultureAssemblies()
+ {
+ var fruit = GetPlátano();
+ Assert.Equal("Banana", fruit.GetFlavor());
+ }
+
+ private IFruit GetPlátano()
+ {
+ var path = TestResources.GetTestProjectAssembly("Plátano");
+ var loader = PluginLoader.CreateFromAssemblyFile(path,
+#if !NETCOREAPP2_1
+ isUnloadable: true,
+#endif
+ sharedTypes: new[] { typeof(IFruit) });
+
+ var assembly = loader.LoadDefaultAssembly();
+ var type = Assert.Single(assembly.GetTypes(), t => typeof(IFruit).IsAssignableFrom(t));
+ return (IFruit)Activator.CreateInstance(type)!;
+ }
+ }
+}
diff --git a/third-party/DotNetCorePlugins/test/Plugins.Tests/DebouncerTests.cs b/third-party/DotNetCorePlugins/test/Plugins.Tests/DebouncerTests.cs
new file mode 100644
index 0000000..67994f6
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/Plugins.Tests/DebouncerTests.cs
@@ -0,0 +1,60 @@
+// Copyright (c) Nate McMaster.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Threading.Tasks;
+using McMaster.NETCore.Plugins.Internal;
+using Xunit;
+
+namespace McMaster.NETCore.Plugins.Tests
+{
+ public class DebouncerTests
+ {
+ [Fact]
+ public async Task InvocationIsDelayed()
+ {
+ var executionCounter = 0;
+
+ var debouncer = new Debouncer(TimeSpan.FromSeconds(.1));
+ debouncer.Execute(() => executionCounter++);
+
+ Assert.Equal(0, executionCounter);
+
+ await Task.Delay(TimeSpan.FromSeconds(.5));
+
+ Assert.Equal(1, executionCounter);
+ }
+
+ [Fact]
+ public async Task ActionsAreDebounced()
+ {
+ var executionCounter = 0;
+
+ var debouncer = new Debouncer(TimeSpan.FromSeconds(.1));
+ debouncer.Execute(() => executionCounter++);
+ debouncer.Execute(() => executionCounter++);
+ debouncer.Execute(() => executionCounter++);
+
+ await Task.Delay(TimeSpan.FromSeconds(.5));
+
+ Assert.Equal(1, executionCounter);
+ }
+
+ [Fact]
+ public async Task OnlyLastActionIsInvoked()
+ {
+ string? invokedAction = null;
+
+ var debouncer = new Debouncer(TimeSpan.FromSeconds(.1));
+ foreach (var action in new[] { "a", "b", "c" })
+ {
+ debouncer.Execute(() => invokedAction = action);
+ }
+
+ await Task.Delay(TimeSpan.FromSeconds(.5));
+
+ Assert.NotNull(invokedAction);
+ Assert.Equal("c", invokedAction);
+ }
+ }
+}
diff --git a/third-party/DotNetCorePlugins/test/Plugins.Tests/ManageLoadContextTests.cs b/third-party/DotNetCorePlugins/test/Plugins.Tests/ManageLoadContextTests.cs
new file mode 100644
index 0000000..bd40aae
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/Plugins.Tests/ManageLoadContextTests.cs
@@ -0,0 +1,69 @@
+// Copyright (c) Nate McMaster.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.IO;
+using System.Reflection;
+using System.Runtime.Loader;
+using McMaster.NETCore.Plugins.Loader;
+using Xunit;
+
+namespace McMaster.NETCore.Plugins.Tests
+{
+ public class ManagedLoadContextTests
+ {
+ [Fact]
+ public void ItUpgradesTypesInContext()
+ {
+ var samplePath = TestResources.GetTestProjectAssembly("XunitSample");
+ var context = new AssemblyLoadContextBuilder()
+ .SetMainAssemblyPath(samplePath)
+ .AddProbingPath(samplePath)
+ .AddDependencyContext(Path.Combine(Path.GetDirectoryName(samplePath)!, "XunitSample.deps.json"))
+ .PreferDefaultLoadContext(true)
+ .Build();
+
+ Assert.Same(typeof(TheoryData).Assembly, LoadAssembly(context, "xunit.core"));
+ }
+
+ [Fact]
+ public void ContextsHavePrivateVersionsByDefault()
+ {
+ var samplePath = TestResources.GetTestProjectAssembly("XunitSample");
+
+ var context = new AssemblyLoadContextBuilder()
+ .SetMainAssemblyPath(samplePath)
+ .AddProbingPath(samplePath)
+ .AddDependencyContext(Path.Combine(Path.GetDirectoryName(samplePath)!, "XunitSample.deps.json"))
+ .Build();
+
+ Assert.NotSame(typeof(TheoryData).Assembly, LoadAssembly(context, "xunit.core"));
+ }
+
+ [Fact]
+ public void ItCanDowngradeUnifiedTypes()
+ {
+ var samplePath = TestResources.GetTestProjectAssembly("NetCoreApp2App");
+
+ var defaultLoader = new AssemblyLoadContextBuilder()
+ .SetMainAssemblyPath(samplePath)
+ .AddProbingPath(samplePath)
+ .PreferDefaultLoadContext(false)
+ .AddDependencyContext(Path.Combine(Path.GetDirectoryName(samplePath)!, "NetCoreApp2App.deps.json"))
+ .Build();
+
+ var unifedLoader = new AssemblyLoadContextBuilder()
+ .SetMainAssemblyPath(samplePath)
+ .AddProbingPath(samplePath)
+ .PreferDefaultLoadContext(true)
+ .AddDependencyContext(Path.Combine(Path.GetDirectoryName(samplePath)!, "NetCoreApp2App.deps.json"))
+ .Build();
+
+ Assert.Equal(new Version("2.0.0.0"), LoadAssembly(defaultLoader, "Test.Referenced.Library").GetName().Version);
+ Assert.Equal(new Version("1.0.0.0"), LoadAssembly(unifedLoader, "Test.Referenced.Library").GetName().Version);
+ }
+
+ private Assembly LoadAssembly(AssemblyLoadContext context, string name)
+ => context.LoadFromAssemblyName(new AssemblyName(name));
+ }
+}
diff --git a/third-party/DotNetCorePlugins/test/Plugins.Tests/McMaster.NETCore.Plugins.Tests.csproj b/third-party/DotNetCorePlugins/test/Plugins.Tests/McMaster.NETCore.Plugins.Tests.csproj
new file mode 100644
index 0000000..5a0a5f5
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/Plugins.Tests/McMaster.NETCore.Plugins.Tests.csproj
@@ -0,0 +1,41 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFrameworks>net5.0;netcoreapp3.1;netcoreapp2.1</TargetFrameworks>
+ <DefaultItemExcludes>$(DefaultItemExcludes);TestResults\**</DefaultItemExcludes>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="McMaster.Extensions.Xunit" Version="0.1.0" />
+ <PackageReference Include="coverlet.collector" Version="3.0.3" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
+ <PackageReference Include="xunit" Version="2.4.1" />
+ <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\Plugins\McMaster.NETCore.Plugins.csproj" />
+ <ProjectReference Include="..\TestProjects\WithOwnPluginsContract\WithOwnPluginsContract.csproj" />
+ <ProjectReference Include="..\TestProjects\ReferencedLibv1\ReferencedLibv1.csproj" />
+ <ProjectReference Include="..\TestProjects\SharedAbstraction.v2\SharedAbstraction.v2.csproj" />
+ <TestProject Include="..\TestProjects\ReferencedLibv2\ReferencedLibv2.csproj" />
+ <TestProject Include="..\TestProjects\DrawingApp\DrawingApp.csproj" />
+ <TestProject Include="..\TestProjects\NetCoreApp2App\NetCoreApp2App.csproj" />
+ <TestProject Include="..\TestProjects\NetStandardClassLib\NetStandardClassLib.csproj" />
+ <TestProject Include="..\TestProjects\JsonNet9\JsonNet9.csproj" />
+ <TestProject Include="..\TestProjects\JsonNet10\JsonNet10.csproj" />
+ <TestProject Include="..\TestProjects\JsonNet11\JsonNet11.csproj" />
+ <TestProject Include="..\TestProjects\Strawberry\Strawberry.csproj" />
+ <TestProject Include="..\TestProjects\Banana\Banana.csproj" />
+ <TestProject Include="..\TestProjects\Plátano\Plátano.csproj" />
+ <TestProject Include="..\TestProjects\XunitSample\XunitSample.csproj" />
+ <TestProject Include="..\TestProjects\SqlClientApp\SqlClientApp.csproj" />
+ <TestProject Include="..\TestProjects\TransitivePlugin\TransitivePlugin.csproj" />
+ <TestProject Include="..\TestProjects\NativeDependency\NativeDependency.csproj" Condition=" '$(TargetFramework)' != 'netcoreapp2.1' " />
+ <PublishedTestProject Include="..\TestProjects\PowerShellPlugin\PowerShellPlugin.csproj" />
+ <MultitargetTestProject Include="..\TestProjects\WithOwnPlugins\WithOwnPlugins.csproj" />
+ </ItemGroup>
+
+ <Import Project="TestProjectRefs.targets" />
+
+</Project>
diff --git a/third-party/DotNetCorePlugins/test/Plugins.Tests/PrivateDependencyTests.cs b/third-party/DotNetCorePlugins/test/Plugins.Tests/PrivateDependencyTests.cs
new file mode 100644
index 0000000..c9c56f5
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/Plugins.Tests/PrivateDependencyTests.cs
@@ -0,0 +1,40 @@
+// Copyright (c) Nate McMaster.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Reflection;
+using Xunit;
+
+namespace McMaster.NETCore.Plugins.Tests
+{
+ public class PrivateDependencyTests
+ {
+ [Fact]
+ public void EachContextHasPrivateVersions()
+ {
+ var json9context = PluginLoader.CreateFromAssemblyFile(TestResources.GetTestProjectAssembly("JsonNet9"));
+ var json10context = PluginLoader.CreateFromAssemblyFile(TestResources.GetTestProjectAssembly("JsonNet10"));
+ var json11context = PluginLoader.CreateFromAssemblyFile(TestResources.GetTestProjectAssembly("JsonNet11"));
+
+ // Load newest first to prove we can load older assemblies later into the same process
+ var json11 = GetJson(json11context);
+ var json10 = GetJson(json10context);
+ var json9 = GetJson(json9context);
+
+ Assert.Equal(new Version("9.0.0.0"), json9.GetName().Version);
+ Assert.Equal(new Version("10.0.0.0"), json10.GetName().Version);
+ Assert.Equal(new Version("11.0.0.0"), json11.GetName().Version);
+
+ // types from each context have unique identities
+ Assert.NotEqual(
+ json11.GetType("Newtonsoft.Json.JsonConvert", throwOnError: true),
+ json10.GetType("Newtonsoft.Json.JsonConvert", throwOnError: true));
+ Assert.NotEqual(
+ json10.GetType("Newtonsoft.Json.JsonConvert", throwOnError: true),
+ json9.GetType("Newtonsoft.Json.JsonConvert", throwOnError: true));
+ }
+
+ private Assembly GetJson(PluginLoader loader)
+ => loader.LoadAssembly(new AssemblyName("Newtonsoft.Json"));
+ }
+}
diff --git a/third-party/DotNetCorePlugins/test/Plugins.Tests/ShadowCopyTests.cs b/third-party/DotNetCorePlugins/test/Plugins.Tests/ShadowCopyTests.cs
new file mode 100644
index 0000000..07e7afb
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/Plugins.Tests/ShadowCopyTests.cs
@@ -0,0 +1,31 @@
+// Copyright (c) Nate McMaster.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+#if !NETCOREAPP2_1
+
+using Xunit;
+
+namespace McMaster.NETCore.Plugins.Tests
+{
+ public class ShadowCopyTests
+ {
+ [Fact]
+ public void DoesNotThrowWhenLoadingSameNativeDependecyMoreThanOnce()
+ {
+ var samplePath = TestResources.GetTestProjectAssembly("NativeDependency");
+
+ using var loader = PluginLoader
+ .CreateFromAssemblyFile(samplePath, config => config.EnableHotReload = true);
+
+ var nativeDependencyLoadMethod = loader.LoadDefaultAssembly()
+ ?.GetType("NativeDependency.NativeDependencyLoader")
+ ?.GetMethod("Load");
+
+ var exception = Record.Exception(() => nativeDependencyLoadMethod?.Invoke(null, null));
+
+ Assert.Null(exception);
+ }
+ }
+}
+
+#endif
diff --git a/third-party/DotNetCorePlugins/test/Plugins.Tests/SharedTypesTests.cs b/third-party/DotNetCorePlugins/test/Plugins.Tests/SharedTypesTests.cs
new file mode 100644
index 0000000..f03f42c
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/Plugins.Tests/SharedTypesTests.cs
@@ -0,0 +1,107 @@
+// Copyright (c) Nate McMaster.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.Loader;
+using Test.Referenced.Library;
+using Test.Shared.Abstraction;
+using WithOwnPluginsContract;
+using Xunit;
+
+namespace McMaster.NETCore.Plugins.Tests
+{
+ public class SharedTypesTests
+ {
+ [Fact]
+ public void PluginsCanForceSharedTypes()
+ {
+ var pluginsNames = new[] { "Banana", "Strawberry" };
+ var loaders = new List<PluginLoader>();
+ foreach (var name in pluginsNames)
+ {
+ var loader = PluginLoader.CreateFromAssemblyFile(
+ TestResources.GetTestProjectAssembly(name),
+ sharedTypes: new[] { typeof(IFruit) });
+ loaders.Add(loader);
+ }
+
+ foreach (var plugin in loaders.Select(l => l.LoadDefaultAssembly()))
+ {
+ var fruitType = Assert.Single(plugin.GetTypes(), t => typeof(IFruit).IsAssignableFrom(t));
+ var fruit = (IFruit)Activator.CreateInstance(fruitType)!;
+ Assert.NotNull(fruit.GetFlavor());
+ }
+ }
+
+ /// <summary>
+ /// This is a carefully crafted example which tests
+ /// that the assembly dependencies of shared types are
+ /// accounted for. Without this, the order in which code loads
+ /// could cause different assembly versions to be loaded.
+ /// </summary>
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void TransitiveAssembliesOfSharedTypesAreResolved(bool isLazyLoaded)
+ {
+ using var loader = PluginLoader.CreateFromAssemblyFile(TestResources.GetTestProjectAssembly("TransitivePlugin"), sharedTypes: new[] { typeof(SharedType) }, config => config.IsLazyLoaded = isLazyLoaded);
+ var assembly = loader.LoadDefaultAssembly();
+ var configType = assembly.GetType("TransitivePlugin.PluginConfig", throwOnError: true)!;
+ var config = Activator.CreateInstance(configType);
+ var transitiveInstance = configType.GetMethod("GetTransitiveType")?.Invoke(config, null);
+ Assert.IsType<Test.Transitive.TransitiveSharedType>(transitiveInstance);
+ }
+
+ /// <summary>
+ /// This is a carefully crafted example which tests
+ /// whether the library can be used outside of the default load context
+ /// (<see cref="AssemblyLoadContext.Default"/>).
+ ///
+ /// It works by loading a plugin (that gets loaded into another ALC)
+ /// which in turn loads its own plugins using the library. If said plugin
+ /// can successfully share its own types, the test should work.
+ /// </summary>
+ [Fact]
+ public void NonDefaultLoadContextsAreSupported()
+ {
+ /* The loaded plugin here will be in its own ALC.
+ * It will load its own plugins, which are not known to this ALC.
+ * Then this ALC will ask that ALC if it managed to successfully its own plugins.
+ */
+
+ using var loader = PluginLoader.CreateFromAssemblyFile(TestResources.GetTestProjectAssembly("WithOwnPlugins"), new[] { typeof(IWithOwnPlugins) });
+ var assembly = loader.LoadDefaultAssembly();
+ var configType = assembly.GetType("WithOwnPlugins.WithOwnPlugins", throwOnError: true)!;
+ var config = (IWithOwnPlugins?)Activator.CreateInstance(configType);
+
+ /*
+ * Here, we have made sure that neither WithOwnPlugins or its own plugins have any way to be
+ * accidentally unified with the default (current for our tests) ALC. We did this by ensuring they are
+ * not loaded in the default ALC in the first place, hence the use of the `IWithOwnPlugins` interface.
+ *
+ * We are simulating a real use case scenario where the plugin host is 100% unaware of the
+ * plugin's own plugins.
+ *
+ * An important additional note:
+ * - Although the assembly of WithOurPlugins is not directly referenced thanks to the
+ * ReferenceOutputAssembly = false property, its contents will still be copied to the output.
+ * - This is problematic because the test runner seems to load all of the Assemblies present in the same
+ * directory as the test assembly, regardless of whether referenced or not.
+ * - Therefore we store the plugins of `WithOwnPlugins` are output in a `Plugins` directory.
+ * (see csproj of WithOwnPlugins, Link property)
+ *
+ * You can ensure that WithOwnPlugins or its plugins are not loaded by inspecting the following:
+ * AssemblyLoadContext.Default.Assemblies
+ *
+ * Even if it was loaded, there's an extra check on the other side to ensure no unification could happen.
+ * Nothing wrong with being extra careful ;).
+ */
+
+ var callingContext = AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly());
+ Assert.True(config?.TryLoadPluginsInCustomContext(callingContext));
+ }
+ }
+}
diff --git a/third-party/DotNetCorePlugins/test/Plugins.Tests/TestProjectRefs.targets b/third-party/DotNetCorePlugins/test/Plugins.Tests/TestProjectRefs.targets
new file mode 100644
index 0000000..17e1b82
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/Plugins.Tests/TestProjectRefs.targets
@@ -0,0 +1,50 @@
+<Project>
+
+ <ItemDefinitionGroup>
+ <MultitargetTestProject>
+ <ReferenceOutputAssembly>false</ReferenceOutputAssembly>
+ <SkipGetTargetFrameworkProperties>true</SkipGetTargetFrameworkProperties>
+ <OutputItemType>_ResolvedTestProjectReference</OutputItemType>
+ </MultitargetTestProject>
+ <TestProject>
+ <ReferenceOutputAssembly>false</ReferenceOutputAssembly>
+ <SkipGetTargetFrameworkProperties>true</SkipGetTargetFrameworkProperties>
+ <OutputItemType>_ResolvedTestProjectReference</OutputItemType>
+ <UndefineProperties>TargetFramework;TargetFrameworks</UndefineProperties>
+ </TestProject>
+ <PublishedTestProject>
+ <ReferenceOutputAssembly>false</ReferenceOutputAssembly>
+ <SkipGetTargetFrameworkProperties>true</SkipGetTargetFrameworkProperties>
+ <OutputItemType>_ResolvedPublishedTestProjectReference</OutputItemType>
+ <UndefineProperties>TargetFramework;TargetFrameworks</UndefineProperties>
+ </PublishedTestProject>
+ </ItemDefinitionGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="@(TestProject);@(MultitargetTestProject);@(PublishedTestProject)" />
+ </ItemGroup>
+
+ <Target Name="GeneratePathToTestProjects"
+ BeforeTargets="CoreCompile;GetAssemblyAttributes"
+ DependsOnTargets="ResolveProjectReferences">
+ <ItemGroup>
+ <AssemblyAttribute Include="McMaster.NETCore.Plugins.Tests.TestProjectReferenceAttribute">
+ <_Parameter1>%(_ResolvedTestProjectReference.FileName)</_Parameter1>
+ <_Parameter2>%(_ResolvedTestProjectReference.RootDir)%(_ResolvedTestProjectReference.Directory)%(_ResolvedTestProjectReference.FileName).dll</_Parameter2>
+ </AssemblyAttribute>
+ </ItemGroup>
+
+ <MSBuild Projects="@(PublishedTestProject)"
+ Targets="Publish"
+ Properties="NoBuild=true;PublishDir=$(TargetDir)%(PublishedTestProject.FileName)/"
+ RemoveProperties="TargetFramework;TargetFrameworks" />
+
+ <ItemGroup>
+ <AssemblyAttribute Include="McMaster.NETCore.Plugins.Tests.TestProjectReferenceAttribute">
+ <_Parameter1>%(PublishedTestProject.FileName)</_Parameter1>
+ <_Parameter2>$(TargetDir)%(PublishedTestProject.FileName)/%(PublishedTestProject.FileName).dll</_Parameter2>
+ </AssemblyAttribute>
+ </ItemGroup>
+ </Target>
+
+</Project>
diff --git a/third-party/DotNetCorePlugins/test/Plugins.Tests/Utilities/TestProjectReferenceAttribute.cs b/third-party/DotNetCorePlugins/test/Plugins.Tests/Utilities/TestProjectReferenceAttribute.cs
new file mode 100644
index 0000000..4246596
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/Plugins.Tests/Utilities/TestProjectReferenceAttribute.cs
@@ -0,0 +1,20 @@
+// Copyright (c) Nate McMaster.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+
+namespace McMaster.NETCore.Plugins.Tests
+{
+ [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
+ public class TestProjectReferenceAttribute : Attribute
+ {
+ public TestProjectReferenceAttribute(string name, string path)
+ {
+ Name = name;
+ Path = path;
+ }
+
+ public string Name { get; }
+ public string Path { get; }
+ }
+}
diff --git a/third-party/DotNetCorePlugins/test/Plugins.Tests/Utilities/TestResources.cs b/third-party/DotNetCorePlugins/test/Plugins.Tests/Utilities/TestResources.cs
new file mode 100644
index 0000000..43f17d4
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/Plugins.Tests/Utilities/TestResources.cs
@@ -0,0 +1,20 @@
+// Copyright (c) Nate McMaster.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Linq;
+using System.Reflection;
+
+namespace McMaster.NETCore.Plugins.Tests
+{
+ public class TestResources
+ {
+ public static string GetTestProjectAssembly(string name)
+ {
+ return typeof(TestResources)
+ .Assembly
+ .GetCustomAttributes<TestProjectReferenceAttribute>()
+ .First(a => a.Name == name)
+ .Path;
+ }
+ }
+}
diff --git a/third-party/DotNetCorePlugins/test/TestProjects/Banana/Banana.cs b/third-party/DotNetCorePlugins/test/TestProjects/Banana/Banana.cs
new file mode 100644
index 0000000..3f670b7
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/TestProjects/Banana/Banana.cs
@@ -0,0 +1,12 @@
+// Copyright (c) Nate McMaster.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using Test.Referenced.Library;
+
+namespace Test
+{
+ internal class Banana : IFruit
+ {
+ public string GetFlavor() => nameof(Banana);
+ }
+}
diff --git a/third-party/DotNetCorePlugins/test/TestProjects/Banana/Banana.csproj b/third-party/DotNetCorePlugins/test/TestProjects/Banana/Banana.csproj
new file mode 100644
index 0000000..d6377b4
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/TestProjects/Banana/Banana.csproj
@@ -0,0 +1,11 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <ItemGroup>
+ <ProjectReference Include="..\ReferencedLibv1\ReferencedLibv1.csproj" />
+ </ItemGroup>
+
+ <PropertyGroup>
+ <TargetFramework>netcoreapp2.1</TargetFramework>
+ </PropertyGroup>
+
+</Project>
diff --git a/third-party/DotNetCorePlugins/test/TestProjects/Directory.Build.props b/third-party/DotNetCorePlugins/test/TestProjects/Directory.Build.props
new file mode 100644
index 0000000..e4594cb
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/TestProjects/Directory.Build.props
@@ -0,0 +1,7 @@
+<Project>
+ <Import Project="..\..\Directory.Build.props" />
+
+ <PropertyGroup>
+ <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
+ </PropertyGroup>
+</Project>
diff --git a/third-party/DotNetCorePlugins/test/TestProjects/DrawingApp/DrawingApp.csproj b/third-party/DotNetCorePlugins/test/TestProjects/DrawingApp/DrawingApp.csproj
new file mode 100644
index 0000000..d5a260a
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/TestProjects/DrawingApp/DrawingApp.csproj
@@ -0,0 +1,11 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>netcoreapp2.1</TargetFramework>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="System.Drawing.Common" Version="5.0.1" />
+ </ItemGroup>
+
+</Project>
diff --git a/third-party/DotNetCorePlugins/test/TestProjects/DrawingApp/Finder.cs b/third-party/DotNetCorePlugins/test/TestProjects/DrawingApp/Finder.cs
new file mode 100644
index 0000000..727f7ef
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/TestProjects/DrawingApp/Finder.cs
@@ -0,0 +1,14 @@
+// Copyright (c) Nate McMaster.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Drawing.Printing;
+
+public class Finder
+{
+ public static string FindDrawingAssembly()
+ {
+ _ = new PrintDocument();
+ return typeof(PrintDocument).Assembly.CodeBase;
+ }
+}
+
diff --git a/third-party/DotNetCorePlugins/test/TestProjects/JsonNet10/Class1.cs b/third-party/DotNetCorePlugins/test/TestProjects/JsonNet10/Class1.cs
new file mode 100644
index 0000000..a2133d3
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/TestProjects/JsonNet10/Class1.cs
@@ -0,0 +1,9 @@
+// Copyright (c) Nate McMaster.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace JsonNet10
+{
+ public class Class1
+ {
+ }
+}
diff --git a/third-party/DotNetCorePlugins/test/TestProjects/JsonNet10/JsonNet10.csproj b/third-party/DotNetCorePlugins/test/TestProjects/JsonNet10/JsonNet10.csproj
new file mode 100644
index 0000000..636432f
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/TestProjects/JsonNet10/JsonNet10.csproj
@@ -0,0 +1,12 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>netstandard2.0</TargetFramework>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Newtonsoft.Json" Version="10.0.1" />
+ <PrivateDependency Include="Newtonsoft.Json, Version=10.0.0.0" />
+ </ItemGroup>
+
+</Project>
diff --git a/third-party/DotNetCorePlugins/test/TestProjects/JsonNet11/Class1.cs b/third-party/DotNetCorePlugins/test/TestProjects/JsonNet11/Class1.cs
new file mode 100644
index 0000000..889ca1f
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/TestProjects/JsonNet11/Class1.cs
@@ -0,0 +1,9 @@
+// Copyright (c) Nate McMaster.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace JsonNet11
+{
+ public class Class1
+ {
+ }
+}
diff --git a/third-party/DotNetCorePlugins/test/TestProjects/JsonNet11/JsonNet11.csproj b/third-party/DotNetCorePlugins/test/TestProjects/JsonNet11/JsonNet11.csproj
new file mode 100644
index 0000000..a38d36d
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/TestProjects/JsonNet11/JsonNet11.csproj
@@ -0,0 +1,13 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>netstandard2.0</TargetFramework>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
+ <PrivateDependency Include="Newtonsoft.Json, Version=11.0.0.0" />
+
+ </ItemGroup>
+
+</Project>
diff --git a/third-party/DotNetCorePlugins/test/TestProjects/JsonNet9/Class1.cs b/third-party/DotNetCorePlugins/test/TestProjects/JsonNet9/Class1.cs
new file mode 100644
index 0000000..345849f
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/TestProjects/JsonNet9/Class1.cs
@@ -0,0 +1,9 @@
+// Copyright (c) Nate McMaster.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace JsonNet9
+{
+ public class Class1
+ {
+ }
+}
diff --git a/third-party/DotNetCorePlugins/test/TestProjects/JsonNet9/JsonNet9.csproj b/third-party/DotNetCorePlugins/test/TestProjects/JsonNet9/JsonNet9.csproj
new file mode 100644
index 0000000..8ac8da1
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/TestProjects/JsonNet9/JsonNet9.csproj
@@ -0,0 +1,12 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>netstandard2.0</TargetFramework>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Newtonsoft.Json" Version="9.0.1" />
+ <PrivateDependency Include="Newtonsoft.Json, Version=9.0.0.0" />
+ </ItemGroup>
+
+</Project>
diff --git a/third-party/DotNetCorePlugins/test/TestProjects/NativeDependency/NativeDependency.csproj b/third-party/DotNetCorePlugins/test/TestProjects/NativeDependency/NativeDependency.csproj
new file mode 100644
index 0000000..d9867d8
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/TestProjects/NativeDependency/NativeDependency.csproj
@@ -0,0 +1,11 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net5.0</TargetFramework>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="System.Data.SQLite" Version="1.0.114.3" />
+ </ItemGroup>
+
+</Project>
diff --git a/third-party/DotNetCorePlugins/test/TestProjects/NativeDependency/NativeDependencyLoader.cs b/third-party/DotNetCorePlugins/test/TestProjects/NativeDependency/NativeDependencyLoader.cs
new file mode 100644
index 0000000..5473bb1
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/TestProjects/NativeDependency/NativeDependencyLoader.cs
@@ -0,0 +1,40 @@
+// Copyright (c) Nate McMaster.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Data.SQLite;
+using System.IO;
+
+namespace NativeDependency
+{
+ public static class NativeDependencyLoader
+ {
+ public static void Load()
+ {
+ using var tempFile = new TempFile("db.sqlite");
+ using var dbConnection = new SQLiteConnection($"Data Source={tempFile.FilePath}");
+
+ dbConnection.Open();
+ }
+ }
+
+ public class TempFile : IDisposable
+ {
+ public TempFile(string fileName)
+ {
+ FilePath = Path.Combine(Path.GetTempPath(), fileName);
+ }
+
+ public string FilePath { get; }
+
+ public void Dispose()
+ {
+ if (!File.Exists(FilePath))
+ {
+ return;
+ }
+
+ File.Delete(FilePath);
+ }
+ }
+}
diff --git a/third-party/DotNetCorePlugins/test/TestProjects/NetCoreApp2App/NetCoreApp2App.csproj b/third-party/DotNetCorePlugins/test/TestProjects/NetCoreApp2App/NetCoreApp2App.csproj
new file mode 100644
index 0000000..0403b15
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/TestProjects/NetCoreApp2App/NetCoreApp2App.csproj
@@ -0,0 +1,12 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <OutputType>Exe</OutputType>
+ <TargetFramework>netcoreapp2.1</TargetFramework>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\ReferencedLibv2\ReferencedLibv2.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/third-party/DotNetCorePlugins/test/TestProjects/NetCoreApp2App/Program.cs b/third-party/DotNetCorePlugins/test/TestProjects/NetCoreApp2App/Program.cs
new file mode 100644
index 0000000..c818479
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/TestProjects/NetCoreApp2App/Program.cs
@@ -0,0 +1,17 @@
+// Copyright (c) Nate McMaster.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+
+namespace NetCoreApp2App
+{
+ internal class Program
+ {
+ public static void Main(string[] args)
+ {
+ Console.WriteLine(GetGreeting());
+ }
+
+ public static string GetGreeting() => "Hello world!";
+ }
+}
diff --git a/third-party/DotNetCorePlugins/test/TestProjects/NetStandardClassLib/Class1.cs b/third-party/DotNetCorePlugins/test/TestProjects/NetStandardClassLib/Class1.cs
new file mode 100644
index 0000000..24bd016
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/TestProjects/NetStandardClassLib/Class1.cs
@@ -0,0 +1,10 @@
+// Copyright (c) Nate McMaster.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace NetStandardClassLib
+{
+ public class Class1
+ {
+ public string GetColor() => "Red";
+ }
+}
diff --git a/third-party/DotNetCorePlugins/test/TestProjects/NetStandardClassLib/NetStandardClassLib.csproj b/third-party/DotNetCorePlugins/test/TestProjects/NetStandardClassLib/NetStandardClassLib.csproj
new file mode 100644
index 0000000..9f5c4f4
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/TestProjects/NetStandardClassLib/NetStandardClassLib.csproj
@@ -0,0 +1,7 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>netstandard2.0</TargetFramework>
+ </PropertyGroup>
+
+</Project>
diff --git a/third-party/DotNetCorePlugins/test/TestProjects/Plátano/Plátano.cs b/third-party/DotNetCorePlugins/test/TestProjects/Plátano/Plátano.cs
new file mode 100644
index 0000000..c655f44
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/TestProjects/Plátano/Plátano.cs
@@ -0,0 +1,13 @@
+// Copyright (c) Nate McMaster.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using Plátano;
+using Test.Referenced.Library;
+
+namespace Test
+{
+ internal class Plátano : IFruit
+ {
+ public string GetFlavor() => Strings.Flavor;
+ }
+}
diff --git a/third-party/DotNetCorePlugins/test/TestProjects/Plátano/Plátano.csproj b/third-party/DotNetCorePlugins/test/TestProjects/Plátano/Plátano.csproj
new file mode 100644
index 0000000..d6377b4
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/TestProjects/Plátano/Plátano.csproj
@@ -0,0 +1,11 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <ItemGroup>
+ <ProjectReference Include="..\ReferencedLibv1\ReferencedLibv1.csproj" />
+ </ItemGroup>
+
+ <PropertyGroup>
+ <TargetFramework>netcoreapp2.1</TargetFramework>
+ </PropertyGroup>
+
+</Project>
diff --git a/third-party/DotNetCorePlugins/test/TestProjects/Plátano/Strings.Designer.cs b/third-party/DotNetCorePlugins/test/TestProjects/Plátano/Strings.Designer.cs
new file mode 100644
index 0000000..460c16b
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/TestProjects/Plátano/Strings.Designer.cs
@@ -0,0 +1,55 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Plátano {
+ using System;
+
+
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ public class Strings {
+
+ private static System.Resources.ResourceManager resourceMan;
+
+ private static System.Globalization.CultureInfo resourceCulture;
+
+ [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Strings() {
+ }
+
+ [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)]
+ public static System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.Equals(null, resourceMan)) {
+ System.Resources.ResourceManager temp = new System.Resources.ResourceManager("Plátano.Strings", typeof(Strings).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)]
+ public static System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ public static string Flavor {
+ get {
+ return ResourceManager.GetString("Flavor", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/third-party/DotNetCorePlugins/test/TestProjects/Plátano/Strings.es.Designer.cs b/third-party/DotNetCorePlugins/test/TestProjects/Plátano/Strings.es.Designer.cs
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/TestProjects/Plátano/Strings.es.Designer.cs
@@ -0,0 +1 @@
+
diff --git a/third-party/DotNetCorePlugins/test/TestProjects/Plátano/Strings.es.resx b/third-party/DotNetCorePlugins/test/TestProjects/Plátano/Strings.es.resx
new file mode 100644
index 0000000..532a76d
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/TestProjects/Plátano/Strings.es.resx
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<root>
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:element name="root" msdata:IsDataSet="true">
+
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>1.3</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="Flavor">
+ <value>Plátano</value>
+ </data>
+</root>
diff --git a/third-party/DotNetCorePlugins/test/TestProjects/Plátano/Strings.resx b/third-party/DotNetCorePlugins/test/TestProjects/Plátano/Strings.resx
new file mode 100644
index 0000000..1ee8873
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/TestProjects/Plátano/Strings.resx
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<root>
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:element name="root" msdata:IsDataSet="true">
+
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>1.3</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="Flavor">
+ <value>Banana</value>
+ </data>
+</root>
diff --git a/third-party/DotNetCorePlugins/test/TestProjects/PowerShellPlugin/PowerShellPlugin.csproj b/third-party/DotNetCorePlugins/test/TestProjects/PowerShellPlugin/PowerShellPlugin.csproj
new file mode 100644
index 0000000..80924db
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/TestProjects/PowerShellPlugin/PowerShellPlugin.csproj
@@ -0,0 +1,12 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <OutputType>Exe</OutputType>
+ <TargetFramework>netcoreapp2.1</TargetFramework>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.PowerShell.SDK" Version="6.0.4" />
+ </ItemGroup>
+
+</Project>
diff --git a/third-party/DotNetCorePlugins/test/TestProjects/PowerShellPlugin/Program.cs b/third-party/DotNetCorePlugins/test/TestProjects/PowerShellPlugin/Program.cs
new file mode 100644
index 0000000..eec7c42
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/TestProjects/PowerShellPlugin/Program.cs
@@ -0,0 +1,25 @@
+// Copyright (c) Nate McMaster.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Management.Automation;
+
+namespace PowerShellPlugin
+{
+ internal class Program
+ {
+ public static void Main(string[] args)
+ {
+ Console.WriteLine(GetGreeting());
+ }
+
+ public static string GetGreeting()
+ {
+ using var ps = PowerShell.Create();
+ var type = typeof(AliasAttribute);
+ // Console.WriteLine(type.Assembly.Location);
+ var results = ps.AddScript("Write-Output hello").Invoke();
+ return results[0].ToString();
+ }
+ }
+}
diff --git a/third-party/DotNetCorePlugins/test/TestProjects/ReferencedLibv1/Class1.cs b/third-party/DotNetCorePlugins/test/TestProjects/ReferencedLibv1/Class1.cs
new file mode 100644
index 0000000..3ad31b6
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/TestProjects/ReferencedLibv1/Class1.cs
@@ -0,0 +1,10 @@
+// Copyright (c) Nate McMaster.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace Test.Referenced.Library
+{
+ public class Class1
+ {
+ public static string GetVersion() => "v1";
+ }
+}
diff --git a/third-party/DotNetCorePlugins/test/TestProjects/ReferencedLibv1/IFruit.cs b/third-party/DotNetCorePlugins/test/TestProjects/ReferencedLibv1/IFruit.cs
new file mode 100644
index 0000000..5aee614
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/TestProjects/ReferencedLibv1/IFruit.cs
@@ -0,0 +1,10 @@
+// Copyright (c) Nate McMaster.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace Test.Referenced.Library
+{
+ public interface IFruit
+ {
+ string GetFlavor();
+ }
+}
diff --git a/third-party/DotNetCorePlugins/test/TestProjects/ReferencedLibv1/ReferencedLibv1.csproj b/third-party/DotNetCorePlugins/test/TestProjects/ReferencedLibv1/ReferencedLibv1.csproj
new file mode 100644
index 0000000..e6fb2b2
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/TestProjects/ReferencedLibv1/ReferencedLibv1.csproj
@@ -0,0 +1,9 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>netstandard2.0</TargetFramework>
+ <AssemblyName>Test.Referenced.Library</AssemblyName>
+ <AssemblyVersion>1.0.0.0</AssemblyVersion>
+ </PropertyGroup>
+
+</Project>
diff --git a/third-party/DotNetCorePlugins/test/TestProjects/ReferencedLibv2/Class1.cs b/third-party/DotNetCorePlugins/test/TestProjects/ReferencedLibv2/Class1.cs
new file mode 100644
index 0000000..af12c3e
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/TestProjects/ReferencedLibv2/Class1.cs
@@ -0,0 +1,10 @@
+// Copyright (c) Nate McMaster.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace Test.Referenced.Library
+{
+ public class Class1
+ {
+ public static string GetVersion() => "v2";
+ }
+}
diff --git a/third-party/DotNetCorePlugins/test/TestProjects/ReferencedLibv2/ReferencedLibv2.csproj b/third-party/DotNetCorePlugins/test/TestProjects/ReferencedLibv2/ReferencedLibv2.csproj
new file mode 100644
index 0000000..5622b28
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/TestProjects/ReferencedLibv2/ReferencedLibv2.csproj
@@ -0,0 +1,13 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>netstandard2.0</TargetFramework>
+ <AssemblyName>Test.Referenced.Library</AssemblyName>
+ <AssemblyVersion>2.0.0.0</AssemblyVersion>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <Compile Include="..\ReferencedLibv1\IFruit.cs" />
+ </ItemGroup>
+
+</Project>
diff --git a/third-party/DotNetCorePlugins/test/TestProjects/SharedAbstraction.v1/SharedAbstraction.v1.csproj b/third-party/DotNetCorePlugins/test/TestProjects/SharedAbstraction.v1/SharedAbstraction.v1.csproj
new file mode 100644
index 0000000..1b9b58e
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/TestProjects/SharedAbstraction.v1/SharedAbstraction.v1.csproj
@@ -0,0 +1,13 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>netstandard2.0</TargetFramework>
+ <AssemblyName>Test.Shared.Abstraction</AssemblyName>
+ <AssemblyVersion>1.0.0.0</AssemblyVersion>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\TransitiveDep.v1\TransitiveDep.v1.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/third-party/DotNetCorePlugins/test/TestProjects/SharedAbstraction.v1/SharedType.cs b/third-party/DotNetCorePlugins/test/TestProjects/SharedAbstraction.v1/SharedType.cs
new file mode 100644
index 0000000..869345d
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/TestProjects/SharedAbstraction.v1/SharedType.cs
@@ -0,0 +1,13 @@
+// Copyright (c) Nate McMaster.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using Test.Transitive;
+
+namespace Test.Shared.Abstraction
+{
+ public class SharedType
+ {
+ public Type GetTransitive() => typeof(TransitiveSharedType);
+ }
+}
diff --git a/third-party/DotNetCorePlugins/test/TestProjects/SharedAbstraction.v2/SharedAbstraction.v2.csproj b/third-party/DotNetCorePlugins/test/TestProjects/SharedAbstraction.v2/SharedAbstraction.v2.csproj
new file mode 100644
index 0000000..1eddcc9
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/TestProjects/SharedAbstraction.v2/SharedAbstraction.v2.csproj
@@ -0,0 +1,14 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>netstandard2.0</TargetFramework>
+ <AssemblyName>Test.Shared.Abstraction</AssemblyName>
+ <AssemblyVersion>2.0.0.0</AssemblyVersion>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <Compile Include="..\SharedAbstraction.v1\SharedType.cs" />
+ <ProjectReference Include="..\TransitiveDep.v2\TransitiveDep.v2.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/third-party/DotNetCorePlugins/test/TestProjects/SqlClientApp/Program.cs b/third-party/DotNetCorePlugins/test/TestProjects/SqlClientApp/Program.cs
new file mode 100644
index 0000000..79fc076
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/TestProjects/SqlClientApp/Program.cs
@@ -0,0 +1,31 @@
+// Copyright (c) Nate McMaster.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Data.SqlClient;
+
+namespace SqlClientApp
+{
+ public class Program
+ {
+ // required to make the C# compiler happy
+ public static void Main(string[] args)
+ {
+ }
+
+ public static bool Run()
+ {
+ try
+ {
+ using var client = new SqlConnection(@"Data Source=(localdb)\mssqllocaldb;Integrated Security=True");
+ client.Open();
+ return !string.IsNullOrEmpty(client.ServerVersion);
+ }
+ catch (SqlException ex) when (ex.Number == -2) // -2 means SQL timeout
+ {
+ // When running the test in Azure DevOps build pipeline, we'll get a SqlException with "Connection Timeout Expired".
+ // We can ignore this safely in unit tests.
+ return true;
+ }
+ }
+ }
+}
diff --git a/third-party/DotNetCorePlugins/test/TestProjects/SqlClientApp/SqlClientApp.csproj b/third-party/DotNetCorePlugins/test/TestProjects/SqlClientApp/SqlClientApp.csproj
new file mode 100644
index 0000000..c8a711c
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/TestProjects/SqlClientApp/SqlClientApp.csproj
@@ -0,0 +1,12 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>netcoreapp2.1</TargetFramework>
+ <OutputType>Exe</OutputType>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="System.Data.SqlClient" Version="4.6.0" />
+ </ItemGroup>
+
+</Project>
diff --git a/third-party/DotNetCorePlugins/test/TestProjects/Strawberry/Strawberry.cs b/third-party/DotNetCorePlugins/test/TestProjects/Strawberry/Strawberry.cs
new file mode 100644
index 0000000..47a3c87
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/TestProjects/Strawberry/Strawberry.cs
@@ -0,0 +1,12 @@
+// Copyright (c) Nate McMaster.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using Test.Referenced.Library;
+
+namespace Test
+{
+ internal class Strawberry : IFruit
+ {
+ public string GetFlavor() => nameof(Strawberry);
+ }
+}
diff --git a/third-party/DotNetCorePlugins/test/TestProjects/Strawberry/Strawberry.csproj b/third-party/DotNetCorePlugins/test/TestProjects/Strawberry/Strawberry.csproj
new file mode 100644
index 0000000..cbbb6d6
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/TestProjects/Strawberry/Strawberry.csproj
@@ -0,0 +1,11 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <ItemGroup>
+ <ProjectReference Include="..\ReferencedLibv1\ReferencedLibv1.csproj" />
+ </ItemGroup>
+
+ <PropertyGroup>
+ <TargetFramework>netstandard2.0</TargetFramework>
+ </PropertyGroup>
+
+</Project>
diff --git a/third-party/DotNetCorePlugins/test/TestProjects/TransitiveDep.v1/TransitiveDep.v1.csproj b/third-party/DotNetCorePlugins/test/TestProjects/TransitiveDep.v1/TransitiveDep.v1.csproj
new file mode 100644
index 0000000..a133a0c
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/TestProjects/TransitiveDep.v1/TransitiveDep.v1.csproj
@@ -0,0 +1,9 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>netstandard2.0</TargetFramework>
+ <AssemblyName>Test.Transitive</AssemblyName>
+ <AssemblyVersion>1.0.0.0</AssemblyVersion>
+ </PropertyGroup>
+
+</Project>
diff --git a/third-party/DotNetCorePlugins/test/TestProjects/TransitiveDep.v1/TransitiveSharedType.cs b/third-party/DotNetCorePlugins/test/TestProjects/TransitiveDep.v1/TransitiveSharedType.cs
new file mode 100644
index 0000000..5f4ca47
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/TestProjects/TransitiveDep.v1/TransitiveSharedType.cs
@@ -0,0 +1,7 @@
+// Copyright (c) Nate McMaster.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace Test.Transitive
+{
+ public class TransitiveSharedType { }
+}
diff --git a/third-party/DotNetCorePlugins/test/TestProjects/TransitiveDep.v2/TransitiveDep.v2.csproj b/third-party/DotNetCorePlugins/test/TestProjects/TransitiveDep.v2/TransitiveDep.v2.csproj
new file mode 100644
index 0000000..ff98f18
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/TestProjects/TransitiveDep.v2/TransitiveDep.v2.csproj
@@ -0,0 +1,13 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>netstandard2.0</TargetFramework>
+ <AssemblyName>Test.Transitive</AssemblyName>
+ <AssemblyVersion>2.0.0.0</AssemblyVersion>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <Compile Include="..\TransitiveDep.v1\*.cs" />
+ </ItemGroup>
+
+</Project>
diff --git a/third-party/DotNetCorePlugins/test/TestProjects/TransitivePlugin/PluginConfig.cs b/third-party/DotNetCorePlugins/test/TestProjects/TransitivePlugin/PluginConfig.cs
new file mode 100644
index 0000000..7e4b420
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/TestProjects/TransitivePlugin/PluginConfig.cs
@@ -0,0 +1,12 @@
+// Copyright (c) Nate McMaster.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using Test.Transitive;
+
+namespace TransitivePlugin
+{
+ public class PluginConfig
+ {
+ public TransitiveSharedType GetTransitiveType() => new();
+ }
+}
diff --git a/third-party/DotNetCorePlugins/test/TestProjects/TransitivePlugin/TransitivePlugin.csproj b/third-party/DotNetCorePlugins/test/TestProjects/TransitivePlugin/TransitivePlugin.csproj
new file mode 100644
index 0000000..4a917c2
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/TestProjects/TransitivePlugin/TransitivePlugin.csproj
@@ -0,0 +1,11 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>netstandard2.0</TargetFramework>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\SharedAbstraction.v1\SharedAbstraction.v1.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/third-party/DotNetCorePlugins/test/TestProjects/WithOurPluginsPluginA/Class1.cs b/third-party/DotNetCorePlugins/test/TestProjects/WithOurPluginsPluginA/Class1.cs
new file mode 100644
index 0000000..cf15f88
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/TestProjects/WithOurPluginsPluginA/Class1.cs
@@ -0,0 +1,12 @@
+// Copyright (c) Nate McMaster.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using WithOurPluginsPluginContract;
+
+namespace WithOurPluginsPluginA
+{
+ public class Class1 : ISayHello
+ {
+ public string SayHello() => $"Hello from {nameof(WithOurPluginsPluginA)}";
+ }
+}
diff --git a/third-party/DotNetCorePlugins/test/TestProjects/WithOurPluginsPluginA/WithOurPluginsPluginA.csproj b/third-party/DotNetCorePlugins/test/TestProjects/WithOurPluginsPluginA/WithOurPluginsPluginA.csproj
new file mode 100644
index 0000000..d20507a
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/TestProjects/WithOurPluginsPluginA/WithOurPluginsPluginA.csproj
@@ -0,0 +1,12 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>netstandard2.0</TargetFramework>
+ <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\WithOurPluginsPluginContract\WithOurPluginsPluginContract.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/third-party/DotNetCorePlugins/test/TestProjects/WithOurPluginsPluginB/Class1.cs b/third-party/DotNetCorePlugins/test/TestProjects/WithOurPluginsPluginB/Class1.cs
new file mode 100644
index 0000000..837f0e7
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/TestProjects/WithOurPluginsPluginB/Class1.cs
@@ -0,0 +1,12 @@
+// Copyright (c) Nate McMaster.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using WithOurPluginsPluginContract;
+
+namespace WithOurPluginsPluginB
+{
+ public class Class1 : ISayHello
+ {
+ public string SayHello() => $"Hello from {nameof(WithOurPluginsPluginB)}";
+ }
+}
diff --git a/third-party/DotNetCorePlugins/test/TestProjects/WithOurPluginsPluginB/WithOurPluginsPluginB.csproj b/third-party/DotNetCorePlugins/test/TestProjects/WithOurPluginsPluginB/WithOurPluginsPluginB.csproj
new file mode 100644
index 0000000..d20507a
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/TestProjects/WithOurPluginsPluginB/WithOurPluginsPluginB.csproj
@@ -0,0 +1,12 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>netstandard2.0</TargetFramework>
+ <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\WithOurPluginsPluginContract\WithOurPluginsPluginContract.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/third-party/DotNetCorePlugins/test/TestProjects/WithOurPluginsPluginContract/ISayHello.cs b/third-party/DotNetCorePlugins/test/TestProjects/WithOurPluginsPluginContract/ISayHello.cs
new file mode 100644
index 0000000..cc52587
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/TestProjects/WithOurPluginsPluginContract/ISayHello.cs
@@ -0,0 +1,10 @@
+// Copyright (c) Nate McMaster.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace WithOurPluginsPluginContract
+{
+ public interface ISayHello
+ {
+ string SayHello();
+ }
+}
diff --git a/third-party/DotNetCorePlugins/test/TestProjects/WithOurPluginsPluginContract/WithOurPluginsPluginContract.csproj b/third-party/DotNetCorePlugins/test/TestProjects/WithOurPluginsPluginContract/WithOurPluginsPluginContract.csproj
new file mode 100644
index 0000000..9f5c4f4
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/TestProjects/WithOurPluginsPluginContract/WithOurPluginsPluginContract.csproj
@@ -0,0 +1,7 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>netstandard2.0</TargetFramework>
+ </PropertyGroup>
+
+</Project>
diff --git a/third-party/DotNetCorePlugins/test/TestProjects/WithOwnPlugins/WithOwnPlugins.cs b/third-party/DotNetCorePlugins/test/TestProjects/WithOwnPlugins/WithOwnPlugins.cs
new file mode 100644
index 0000000..47cff6f
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/TestProjects/WithOwnPlugins/WithOwnPlugins.cs
@@ -0,0 +1,72 @@
+// Copyright (c) Nate McMaster.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.Loader;
+using McMaster.NETCore.Plugins;
+using WithOurPluginsPluginContract;
+using WithOwnPluginsContract;
+
+namespace WithOwnPlugins
+{
+ public class WithOwnPlugins : IWithOwnPlugins
+ {
+ public bool TryLoadPluginsInCustomContext(AssemblyLoadContext? callingContext)
+ {
+ var currentContext = AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly());
+ if (currentContext == callingContext)
+ {
+ throw new ArgumentException("The context of the caller is the context of this assembly. This invalidates the test.");
+ }
+
+#if !NETCOREAPP2_1
+ /*
+ Ensure the source calling context does not have our plugin's interfaces loaded.
+ This guarantees that the Assembly cannot possibly unify with the default load context.
+
+ Note:
+ The code below this check would fail anyway if the assembly would unify with the default context.
+ This is more of a safety check to ensure "correctness" as opposed to anything else.
+ */
+ var sayHelloAssembly = typeof(ISayHello).Assembly;
+ if (callingContext?.Assemblies.Contains(sayHelloAssembly) == true) // .Assemblies API not available in Core 2.X
+ {
+ throw new ArgumentException("The context of the caller has this plugin's interface to interact with its own plugins loaded. Test is void.");
+ }
+#endif
+
+ // Load our own plugins: Remember, we are in an isolated, non-default ALC.
+ var plugins = new List<ISayHello?>();
+ string[] assemblyNames = { "Plugins/WithOurPluginsPluginA.dll", "Plugins/WithOurPluginsPluginB.dll" };
+
+ foreach (var assemblyName in assemblyNames)
+ {
+ var currentAssemblyFolderPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? throw new Exception("Unable to get folder path for currently executing assembly.");
+ var pluginPath = Path.Combine(currentAssemblyFolderPath, assemblyName);
+
+ using var loader = PluginLoader.CreateFromAssemblyFile(pluginPath, new[] { typeof(ISayHello) });
+ var assembly = loader.LoadDefaultAssembly();
+ var configType = assembly.GetTypes().First(x => typeof(ISayHello).IsAssignableFrom(x) && !x.IsAbstract);
+ var plugin = (ISayHello?)Activator.CreateInstance(configType);
+ if (plugin == null)
+ {
+ throw new Exception($"Failed to load instance of {nameof(ISayHello)} from plugin.");
+ }
+
+ plugins.Add(plugin);
+ }
+
+ // Shouldn't need to check for this but just in case to absolutely make sure.
+ if (plugins.Any(plugin => String.IsNullOrEmpty(plugin?.SayHello())))
+ {
+ throw new Exception("No value returned from plugin.");
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/third-party/DotNetCorePlugins/test/TestProjects/WithOwnPlugins/WithOwnPlugins.csproj b/third-party/DotNetCorePlugins/test/TestProjects/WithOwnPlugins/WithOwnPlugins.csproj
new file mode 100644
index 0000000..4218eda
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/TestProjects/WithOwnPlugins/WithOwnPlugins.csproj
@@ -0,0 +1,15 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFrameworks>net5.0;netcoreapp3.1;netcoreapp2.1</TargetFrameworks>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\..\src\Plugins\McMaster.NETCore.Plugins.csproj" />
+ <ProjectReference Include="..\WithOurPluginsPluginA\WithOurPluginsPluginA.csproj" PrivateAssets="All" ReferenceOutputAssembly="false" OutputItemType="Content" CopyToOutputDirectory="Always" Link="Plugins/%(RecursiveDir)%(Filename).dll" />
+ <ProjectReference Include="..\WithOurPluginsPluginB\WithOurPluginsPluginB.csproj" PrivateAssets="All" ReferenceOutputAssembly="false" OutputItemType="Content" CopyToOutputDirectory="Always" Link="Plugins/%(RecursiveDir)%(Filename).dll" />
+ <ProjectReference Include="..\WithOurPluginsPluginContract\WithOurPluginsPluginContract.csproj" />
+ <ProjectReference Include="..\WithOwnPluginsContract\WithOwnPluginsContract.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/third-party/DotNetCorePlugins/test/TestProjects/WithOwnPluginsContract/IWithOwnPlugins.cs b/third-party/DotNetCorePlugins/test/TestProjects/WithOwnPluginsContract/IWithOwnPlugins.cs
new file mode 100644
index 0000000..ae7563f
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/TestProjects/WithOwnPluginsContract/IWithOwnPlugins.cs
@@ -0,0 +1,12 @@
+// Copyright (c) Nate McMaster.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Runtime.Loader;
+
+namespace WithOwnPluginsContract
+{
+ public interface IWithOwnPlugins
+ {
+ bool TryLoadPluginsInCustomContext(AssemblyLoadContext? callingContext);
+ }
+}
diff --git a/third-party/DotNetCorePlugins/test/TestProjects/WithOwnPluginsContract/WithOwnPluginsContract.csproj b/third-party/DotNetCorePlugins/test/TestProjects/WithOwnPluginsContract/WithOwnPluginsContract.csproj
new file mode 100644
index 0000000..65d6641
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/TestProjects/WithOwnPluginsContract/WithOwnPluginsContract.csproj
@@ -0,0 +1,7 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFrameworks>netcoreapp3.1;netcoreapp2.1</TargetFrameworks>
+ </PropertyGroup>
+
+</Project>
diff --git a/third-party/DotNetCorePlugins/test/TestProjects/XunitSample/Class1.cs b/third-party/DotNetCorePlugins/test/TestProjects/XunitSample/Class1.cs
new file mode 100644
index 0000000..b124389
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/TestProjects/XunitSample/Class1.cs
@@ -0,0 +1,9 @@
+// Copyright (c) Nate McMaster.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace XunitSample
+{
+ public class Class1
+ {
+ }
+}
diff --git a/third-party/DotNetCorePlugins/test/TestProjects/XunitSample/XunitSample.csproj b/third-party/DotNetCorePlugins/test/TestProjects/XunitSample/XunitSample.csproj
new file mode 100644
index 0000000..4620f3d
--- /dev/null
+++ b/third-party/DotNetCorePlugins/test/TestProjects/XunitSample/XunitSample.csproj
@@ -0,0 +1,12 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>netstandard2.0</TargetFramework>
+ <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="xunit.extensibility.execution" Version="2.2.0" />
+ </ItemGroup>
+
+</Project>