├── .editorconfig ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── Bug_report.md │ ├── Feature_request.md │ ├── Question.md │ └── config.yml └── workflows │ ├── ci.yml │ └── stale.yml ├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Directory.Build.props ├── DotNetCorePlugins.sln ├── LICENSE.txt ├── README.md ├── build.ps1 ├── docs ├── design-doc.md └── what-are-shared-types.md ├── samples ├── aspnetcore-mvc │ ├── MvcApp │ │ ├── Controllers │ │ │ └── HomeController.cs │ │ ├── MvcApp.csproj │ │ ├── Program.cs │ │ ├── Startup.cs │ │ └── Views │ │ │ ├── Home │ │ │ └── Index.cshtml │ │ │ └── _ViewImports.cshtml │ ├── MvcAppPlugin1 │ │ ├── MvcAppPlugin1.csproj │ │ ├── MyPluginController.cs │ │ └── Views │ │ │ └── MyPlugin │ │ │ └── Index.cshtml │ ├── README.md │ └── aspnetcore-mvc.sln ├── aspnetcore │ ├── Abstractions │ │ ├── Abstractions.csproj │ │ ├── IPlugin.cs │ │ └── IPluginLink.cs │ ├── MainWebApp │ │ ├── MainWebApp.csproj │ │ ├── Pages │ │ │ ├── Index.cshtml │ │ │ └── Index.cshtml.cs │ │ ├── Program.cs │ │ ├── Properties │ │ │ └── launchSettings.json │ │ └── Startup.cs │ ├── README.md │ ├── WebAppPlugin1 │ │ ├── WebAppPlugin1.csproj │ │ └── WebPlugin1.cs │ ├── WebAppPlugin2 │ │ ├── WebAppPlugin2.csproj │ │ └── WebPlugin2.cs │ └── aspnetcore.sln ├── dependency-injection │ ├── DI.HostApp │ │ ├── DI.HostApp.csproj │ │ └── Program.cs │ ├── DI.SharedAbstractions │ │ ├── DI.SharedAbstractions.csproj │ │ ├── Fruit.cs │ │ ├── IFruitConsumer.cs │ │ ├── IFruitProducer.cs │ │ └── IPluginFactory.cs │ ├── MyPlugin1 │ │ ├── MyFruitProducer.cs │ │ ├── MyPlugin1.csproj │ │ └── PluginConfiguration.cs │ ├── MyPlugin2 │ │ ├── MyFruitConsumer.cs │ │ ├── MyPlugin2.csproj │ │ └── PluginConfiguration.cs │ └── README.md ├── dynamic-implementation │ ├── Contracts │ │ ├── Contracts.csproj │ │ ├── Fruit.cs │ │ ├── IFruitService.cs │ │ ├── IMixerService.cs │ │ └── IPluginFactory.cs │ ├── DynamicImplementation.sln │ ├── Host │ │ ├── Host.csproj │ │ └── Program.cs │ ├── Mixer │ │ ├── Mixer.csproj │ │ ├── MixerPlugin.cs │ │ ├── MixerService.cs │ │ └── StandardFruiteService.cs │ ├── README.md │ └── ServiceImplementation │ │ ├── OverrideFruiteService.cs │ │ ├── OverridePlugin.cs │ │ └── ServiceImplementation.csproj ├── hello-world │ ├── HostApp │ │ ├── HostApp.csproj │ │ └── Program.cs │ ├── MyPlugin │ │ ├── MyPlugin.csproj │ │ └── MyPlugin1.cs │ ├── PluginContract │ │ ├── IPlugin.cs │ │ └── PluginContract.csproj │ ├── README.md │ └── hello-world.sln └── hot-reload │ ├── HotReloadApp │ ├── HotReloadApp.csproj │ └── Program.cs │ ├── README.md │ ├── TimestampedPlugin │ ├── InfoDisplayer.cs │ └── TimestampedPlugin.csproj │ ├── hot-reload.sln │ ├── run.ps1 │ └── run.sh ├── src ├── Directory.Build.targets ├── Plugins.Mvc │ ├── McMaster.NETCore.Plugins.Mvc.csproj │ ├── MvcPluginExtensions.cs │ ├── PublicAPI.Shipped.txt │ ├── PublicAPI.Unshipped.txt │ └── releasenotes.props ├── Plugins │ ├── Internal │ │ ├── Debouncer.cs │ │ ├── PlatformInformation.cs │ │ ├── RuntimeConfig.cs │ │ └── RuntimeOptions.cs │ ├── LibraryModel │ │ ├── ManagedLibrary.cs │ │ └── NativeLibrary.cs │ ├── Loader │ │ ├── AssemblyLoadContextBuilder.cs │ │ ├── ManagedLoadContext.cs │ │ └── RuntimeConfigExtensions.cs │ ├── McMaster.NETCore.Plugins.csproj │ ├── PluginConfig.cs │ ├── PluginLoader.cs │ ├── PluginReloadedEventHandler.cs │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── PublicAPI.Shipped.txt │ ├── PublicAPI.Unshipped.txt │ └── releasenotes.props ├── StrongName.snk └── common.psm1 └── test ├── Plugins.Tests ├── BasicAssemblyLoaderTests.cs ├── DebouncerTests.cs ├── ManageLoadContextTests.cs ├── McMaster.NETCore.Plugins.Tests.csproj ├── PrivateDependencyTests.cs ├── ShadowCopyTests.cs ├── SharedTypesTests.cs ├── TestProjectRefs.targets └── Utilities │ ├── TestProjectReferenceAttribute.cs │ └── TestResources.cs └── TestProjects ├── Banana ├── Banana.cs └── Banana.csproj ├── Directory.Build.props ├── DrawingApp ├── DrawingApp.csproj └── Finder.cs ├── Libv1 ├── Class1.cs └── Libv1.csproj ├── Libv2 ├── Class1.cs └── Libv2.csproj ├── Libv3 ├── Class1.cs └── Libv3.csproj ├── NativeDependency ├── NativeDependency.csproj └── NativeDependencyLoader.cs ├── NetCoreApp2App ├── NetCoreApp2App.csproj └── Program.cs ├── NetStandardClassLib ├── Class1.cs └── NetStandardClassLib.csproj ├── Plátano ├── Plátano.cs ├── Plátano.csproj ├── Strings.Designer.cs ├── Strings.es.Designer.cs ├── Strings.es.resx └── Strings.resx ├── PowerShellPlugin ├── PowerShellPlugin.csproj └── Program.cs ├── PrivateDepv1 ├── Class1.cs └── PrivateDepv1.csproj ├── PrivateDepv2 ├── Class1.cs └── PrivateDepv2.csproj ├── PrivateDepv3 ├── Class1.cs └── PrivateDepv3.csproj ├── ReferencedLibv1 ├── Class1.cs ├── IFruit.cs └── ReferencedLibv1.csproj ├── ReferencedLibv2 ├── Class1.cs └── ReferencedLibv2.csproj ├── SharedAbstraction.v1 ├── SharedAbstraction.v1.csproj └── SharedType.cs ├── SharedAbstraction.v2 └── SharedAbstraction.v2.csproj ├── SqlClientApp ├── Program.cs └── SqlClientApp.csproj ├── Strawberry ├── Strawberry.cs └── Strawberry.csproj ├── TransitiveDep.v1 ├── TransitiveDep.v1.csproj └── TransitiveSharedType.cs ├── TransitiveDep.v2 └── TransitiveDep.v2.csproj ├── TransitivePlugin ├── PluginConfig.cs └── TransitivePlugin.csproj ├── WithOurPluginsPluginA ├── Class1.cs └── WithOurPluginsPluginA.csproj ├── WithOurPluginsPluginB ├── Class1.cs └── WithOurPluginsPluginB.csproj ├── WithOurPluginsPluginContract ├── ISayHello.cs └── WithOurPluginsPluginContract.csproj ├── WithOwnPlugins ├── WithOwnPlugins.cs └── WithOwnPlugins.csproj ├── WithOwnPluginsContract ├── IWithOwnPlugins.cs └── WithOwnPluginsContract.csproj └── XunitSample ├── Class1.cs └── XunitSample.csproj /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | trim_trailing_whitespace = true 6 | insert_final_newline = true 7 | end_of_line = lf 8 | 9 | [*.{xml,csproj,props,targets,config}] 10 | indent_size = 2 11 | 12 | [*.json] 13 | indent_size = 2 14 | 15 | [*.yml] 16 | indent_size = 2 17 | 18 | [*.cs] 19 | indent_size = 4 20 | 21 | # Style I care about 22 | csharp_style_expression_bodied_constructors = false 23 | csharp_prefer_braces = true 24 | dotnet_sort_system_directives_first = true 25 | csharp_using_directive_placement = outside_namespace 26 | 27 | # Stuff that is usually best 28 | csharp_style_inlined_variable_declaration = true 29 | csharp_style_var_elsewhere = true 30 | csharp_space_after_cast = false 31 | csharp_style_pattern_matching_over_as_with_null_check = true 32 | csharp_style_pattern_matching_over_is_with_cast_check = true 33 | csharp_style_var_for_built_in_types = true 34 | csharp_style_var_when_type_is_apparent = true 35 | csharp_new_line_before_catch = true 36 | csharp_new_line_before_else = true 37 | csharp_new_line_before_finally = true 38 | csharp_indent_case_contents = true 39 | csharp_new_line_before_open_brace = all 40 | csharp_indent_switch_labels = true 41 | csharp_indent_labels = one_less_than_current 42 | csharp_prefer_simple_default_expression = true 43 | csharp_preserve_single_line_blocks = true 44 | csharp_preserve_single_line_statements = true 45 | 46 | # Good defaults, but not always 47 | dotnet_style_object_initializer = true 48 | csharp_style_expression_bodied_indexers = true 49 | csharp_style_expression_bodied_accessors = true 50 | csharp_style_throw_expression = true 51 | 52 | # Default severity for analyzer diagnostics with category 'Style' (escalated to build warnings) 53 | # dotnet_analyzer_diagnostic.category-Style.severity = suggestion 54 | 55 | # Required naming style 56 | dotnet_diagnostic.IDE0006.severity = error 57 | 58 | # suppress warning aboud unused methods 59 | dotnet_diagnostic.IDE0051.severity = none 60 | 61 | # Missing required header 62 | dotnet_diagnostic.IDE0040.severity = error 63 | 64 | # Missing accessibility modifier 65 | dotnet_diagnostic.IDE0073.severity = warning 66 | 67 | # Remove unnecessary parenthesis 68 | dotnet_diagnostic.IDE0047.severity = warning 69 | 70 | # Parenthesis added for clarity 71 | dotnet_diagnostic.IDE0048.severity = warning 72 | 73 | # Suppress explicit type instead of var 74 | dotnet_diagnostic.IDE0008.severity = none 75 | 76 | # Suppress unused expression 77 | dotnet_diagnostic.IDE0058.severity = none 78 | 79 | 80 | 81 | # Naming styles 82 | 83 | ## Constants are PascalCase 84 | dotnet_naming_style.pascal_case.capitalization = pascal_case 85 | 86 | dotnet_naming_symbols.constants.applicable_kinds = field, property 87 | dotnet_naming_symbols.constants.applicable_accessibilities = * 88 | dotnet_naming_symbols.constants.required_modifiers = const 89 | 90 | dotnet_naming_rule.constants_should_be_pascal_case.symbols = constants 91 | dotnet_naming_rule.constants_should_be_pascal_case.style = pascal_case 92 | dotnet_naming_rule.constants_should_be_pascal_case.severity = error 93 | 94 | ## Private static fields start with s_ 95 | dotnet_naming_style.s_underscore_camel_case.required_prefix = s_ 96 | dotnet_naming_style.s_underscore_camel_case.capitalization = camel_case 97 | 98 | dotnet_naming_symbols.private_static_fields.applicable_kinds = field 99 | dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private 100 | dotnet_naming_symbols.private_static_fields.required_modifiers = static 101 | 102 | dotnet_naming_rule.private_static_fields_should_be_underscore.symbols = private_static_fields 103 | dotnet_naming_rule.private_static_fields_should_be_underscore.style = s_underscore_camel_case 104 | dotnet_naming_rule.private_static_fields_should_be_underscore.severity = error 105 | 106 | ## Private fields are _camelCase 107 | dotnet_naming_style.underscore_camel_case.required_prefix = _ 108 | dotnet_naming_style.underscore_camel_case.capitalization = camel_case 109 | 110 | dotnet_naming_symbols.private_fields.applicable_kinds = field 111 | dotnet_naming_symbols.private_fields.applicable_accessibilities = private 112 | 113 | dotnet_naming_rule.private_fields_should_be_underscore.symbols = private_fields 114 | dotnet_naming_rule.private_fields_should_be_underscore.style = underscore_camel_case 115 | dotnet_naming_rule.private_fields_should_be_underscore.severity = error 116 | 117 | # File header 118 | file_header_template = Copyright (c) Nate McMaster.\nLicensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 119 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @natemcmaster 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Using this version of the library '...' 16 | 2. Run this code '....' 17 | 3. With these arguments '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. 12 | Example. I'm am trying to do [...] but [...] 13 | 14 | **Describe the solution you'd like** 15 | A clear and concise description of what you want to happen. 16 | 17 | **Describe alternatives you've considered** 18 | A description of any alternative solutions or features you've considered. 19 | 20 | **Additional context** 21 | Add any other context or screenshots about the feature request here. 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask a question 4 | title: "[Question] " 5 | labels: question 6 | assignees: '' 7 | 8 | --- 9 | 10 | If you're not exactly sure what to say, here are some suggestions: https://stackoverflow.com/help/how-to-ask 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - dependabot/* 7 | pull_request: 8 | workflow_dispatch: 9 | inputs: 10 | is_stable_build: 11 | description: Use a version number indicating this is a stable release 12 | required: true 13 | default: "false" 14 | release: 15 | description: Create a release 16 | required: true 17 | default: "false" 18 | 19 | env: 20 | IS_STABLE_BUILD: ${{ github.event.inputs.is_stable_build }} 21 | 22 | jobs: 23 | build: 24 | if: "!contains(github.event.head_commit.message, 'ci skip') || github.event_name == 'workflow_dispatch'" 25 | strategy: 26 | fail-fast: false 27 | matrix: 28 | os: [windows-latest, ubuntu-latest, macos-latest] 29 | 30 | runs-on: ${{ matrix.os }} 31 | 32 | # Windows builds are failing 100% of the time on code that works just fine on Linux 33 | # and works 90% of the time on the macOS build agents GitHub uses and works 100% of the time on my 34 | # my laptop. Life is short. I'm not spending more time trying to figure out why the code that used 35 | # to work create on .NET 5 became flaky on newer versions of .NET but only on some platforms. 36 | continue-on-error: ${{ matrix.os != 'ubuntu-latest' }} 37 | 38 | outputs: 39 | package_version: ${{ steps.build_script.outputs.package_version }} 40 | 41 | steps: 42 | - uses: actions/checkout@v4 43 | - name: Setup .NET 44 | uses: actions/setup-dotnet@v4 45 | with: 46 | dotnet-version: 8 47 | - name: Run build script 48 | id: build_script 49 | run: ./build.ps1 -ci 50 | - uses: actions/upload-artifact@v4 51 | if: ${{ matrix.os == 'ubuntu-latest' }} 52 | with: 53 | name: packages 54 | path: artifacts/ 55 | if-no-files-found: error 56 | - uses: codecov/codecov-action@v5 57 | with: 58 | token: ${{ secrets.CODECOV_TOKEN }} 59 | name: unittests-${{ matrix.os }} 60 | fail_ci_if_error: true 61 | release: 62 | if: "github.event.inputs.release" 63 | needs: build 64 | runs-on: windows-latest 65 | env: 66 | PACKAGE_VERSION: ${{ needs.build.outputs.package_version }} 67 | steps: 68 | - run: echo "Releasing ${{ env.PACKAGE_VERSION }}" 69 | - name: Setup NuGet 70 | uses: NuGet/setup-nuget@v2 71 | with: 72 | nuget-version: latest 73 | - uses: actions/download-artifact@v4 74 | with: 75 | name: packages 76 | path: packages 77 | - name: Configure GitHub NuGet registry 78 | run: nuget sources add -name github -source https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json -username ${{ github.repository_owner }} -password ${{ secrets.GITHUB_TOKEN }} 79 | - name: Push to GitHub package registry 80 | run: nuget push packages\*.nupkg -ApiKey ${{ secrets.GITHUB_TOKEN }} -Source github 81 | - name: Push to NuGet.org 82 | run: nuget push packages\*.nupkg -ApiKey ${{ secrets.NUGET_API_KEY }} -Source https://api.nuget.org/v3/index.json 83 | - name: Create GitHub release 84 | uses: softprops/action-gh-release@v2 85 | env: 86 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 87 | with: 88 | name: ${{ env.PACKAGE_VERSION }} 89 | tag_name: v${{ env.PACKAGE_VERSION }} 90 | body: | 91 | ## Notes 92 | 93 | ## How to get this update 94 | Packages have been posted to these feeds: 95 | 96 | #### NuGet.org 97 | https://nuget.org/packages/McMaster.NETCore.Plugins/${{ env.PACKAGE_VERSION }} 98 | https://nuget.org/packages/McMaster.NETCore.Plugins.Mvc/${{ env.PACKAGE_VERSION }} 99 | 100 | #### GitHub Package Registry 101 | https://github.com/natemcmaster?tab=packages&repo_name=DotNetCorePlugins 102 | 103 | draft: true 104 | prerelease: ${{ env.IS_STABLE_BUILD == 'false' }} # Example: v3.1.0-beta 105 | files: packages/* 106 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues and PRs' 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | debug-only: 6 | description: Run in debug mode 7 | required: false 8 | default: 'false' 9 | schedule: 10 | - cron: '30 1 * * *' 11 | 12 | jobs: 13 | stale: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/stale@v9 17 | with: 18 | debug-only: ${{ github.event.inputs.debug-only == 'true' }} 19 | days-before-stale: 365 20 | days-before-close: 14 21 | stale-issue-label: stale 22 | close-issue-label: closed-stale 23 | close-issue-reason: not_planned 24 | exempt-issue-labels: announcement,planning 25 | exempt-all-milestones: true 26 | exempt-all-assignees: true 27 | stale-issue-message: > 28 | This issue has been automatically marked as stale because it has no recent activity. 29 | It will be closed if no further activity occurs. Please comment if you believe this 30 | should remain open, otherwise it will be closed in 14 days. 31 | Thank you for your contributions to this project. 32 | close-issue-message: > 33 | Closing due to inactivity. 34 | 35 | If you are looking at this issue in the future and think it should be reopened, 36 | please make a commented here and mention natemcmaster so he sees the notification. 37 | stale-pr-message: > 38 | This pull request appears to be stale. Please comment if you believe this should remain 39 | open and reviewed. If there are no updates, it will be closed in 14 days. 40 | close-pr-message: > 41 | Thank you for your contributions to this project. This pull request has been closed due to inactivity. 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | obj/ 3 | .vs/ 4 | artifacts/ 5 | .idea/ 6 | *.user 7 | launchSettings.json 8 | .dotnet/ 9 | TestResults/ 10 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": ".NET Core Attach", 6 | "type": "coreclr", 7 | "request": "attach", 8 | "processId": "${command:pickProcess}" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "command": "dotnet", 6 | "label": "build", 7 | "args": [ 8 | "build", 9 | "--no-restore" 10 | ], 11 | "presentation": { 12 | "echo": true, 13 | "reveal": "silent", 14 | "focus": false, 15 | "panel": "shared", 16 | "showReuseMessage": false, 17 | "clear": true 18 | }, 19 | "problemMatcher": "$msCompile", 20 | "group": { 21 | "kind": "build", 22 | "isDefault": true 23 | } 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at open-source@natemcmaster.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing Guide 2 | ================== 3 | 4 | Contributions are welcome! If you would like to help out, here are some suggestions for how to get involved. 5 | 6 | ## Get involved 7 | [Watch][watchers] this repository to get notifications about all conversations. GitHub issues and pull requests are the authoritative 8 | source of truth for design reviews, release schedules, and bug fixes. 9 | 10 | ## You don't have to contribute code 11 | 12 | There are more ways to help that don't involve writing code. 13 | 14 | * Respond to new issues. Users often open an issue to ask a question. You are welcome to offer your answer on the thread. 15 | * :+1: Up vote features that you think are important. 16 | * Look through issues labeled [closed-stale][closed-stale] to see if there are feature requests worth reviving. 17 | * Review pull requests. 18 | 19 | ## Contributing code 20 | 21 | * Open issues labeled ["help wanted"][help-wanted] are issues that I think are worth doing, but no one has volunteered to do the work yet. 22 | Make a comment on issues you want assigned to yourself. 23 | * Pull requests are more likely to be accepted if I have first agreed to accept a feature or bug fix. Open an issue first if you aren't sure. 24 | 25 | ## Questions? 26 | 27 | Open a GitHub issue if you'd like to help and don't know where to begin. 28 | 29 | [watchers]: https://github.com/natemcmaster/DotNetCorePlugins/watchers 30 | [closed-stale]: https://github.com/natemcmaster/DotNetCorePlugins/labels/closed-stale 31 | [help-wanted]: https://github.com/natemcmaster/DotNetCorePlugins/labels/help%20wanted 32 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Nate McMaster 5 | DotNetCorePlugins 6 | Copyright © Nate McMaster 7 | en-US 8 | false 9 | $(MSBuildThisFileDirectory) 10 | Apache-2.0 11 | https://github.com/natemcmaster/DotNetCorePlugins 12 | https://github.com/natemcmaster/DotNetCorePlugins.git 13 | README.md 14 | git 15 | snupkg 16 | true 17 | true 18 | portable 19 | false 20 | false 21 | 12 22 | true 23 | true 24 | enable 25 | $(MSBuildThisFileDirectory)src\StrongName.snk 26 | true 27 | $(NoWarn);NU5105 28 | $(WarningsNotAsErrors);1591;0618 29 | 30 | true 31 | true 32 | true 33 | $(MSBuildThisFileDirectory).build\obj\$(MSBuildProjectName)\ 34 | $(MSBuildThisFileDirectory).build\bin\$(MSBuildProjectName)\ 35 | 36 | 37 | 38 | 2.0.0 39 | beta 40 | true 41 | $([MSBuild]::ValueOrDefault($(GITHUB_RUN_NUMBER), 0)) 42 | $(VersionSuffix).$(BuildNumber) 43 | $(GITHUB_SHA) 44 | $(VersionPrefix) 45 | $(PackageVersion)-$(VersionSuffix) 46 | $(PackageVersion)+$(SourceRevisionId) 47 | 48 | 49 | 50 | 51 | 52 | 53 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /build.ps1: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env pwsh 2 | [CmdletBinding(PositionalBinding = $false)] 3 | param( 4 | [ValidateSet('Debug', 'Release')] 5 | $Configuration = $null, 6 | [switch] 7 | $ci 8 | ) 9 | 10 | Set-StrictMode -Version 1 11 | $ErrorActionPreference = 'Stop' 12 | 13 | Import-Module -Force -Scope Local "$PSScriptRoot/src/common.psm1" 14 | 15 | if (!$Configuration) { 16 | $Configuration = if ($ci) { 'Release' } else { 'Debug' } 17 | } 18 | 19 | $artifacts = "$PSScriptRoot/artifacts/" 20 | 21 | Remove-Item -Recurse $artifacts -ErrorAction Ignore 22 | 23 | exec dotnet format 24 | exec dotnet build --configuration $Configuration '-warnaserror:CS1591' 25 | exec dotnet pack --no-restore --no-build --configuration $Configuration -o $artifacts 26 | exec dotnet test --no-restore --no-build --configuration $Configuration ` 27 | "$PSScriptRoot/test/Plugins.Tests/McMaster.NETCore.Plugins.Tests.csproj" ` 28 | --collect:"XPlat Code Coverage" 29 | 30 | write-host -f green 'BUILD SUCCEEDED' 31 | -------------------------------------------------------------------------------- /docs/design-doc.md: -------------------------------------------------------------------------------- 1 | .NET Core Plugins - design doc 2 | ============================== 3 | 4 | This repository contains a library code to support a .NET Core plugin model. 5 | 6 | ## Synopsis 7 | 8 | This library is designed to provide support to .NET Core applications that wish to load assemblies on the fly and execute them as extensions to the main application. 9 | It defines a plugin model that allows these applications to control, to some level, isolation between the extension (plugin) loaded and the 10 | host (main application). 11 | 12 | ## Background 13 | 14 | Today, implementing a plugin model requires a high level of knowledge about how assemblies are resolved and loaded. Various features in .NET Core exist to assist in this, but none of them provide an API and experience consistent with how corehost handles and loads assemblies. 15 | 16 | Existing features: 17 | - System.Runtime.Loader.AssemblyLoadContext - users can implement custom assembly loading behaviors 18 | - additionalDeps - users can craft a deps.json file which the host will load into all processes as if the dependencies were in the application's .deps.json file 19 | 20 | Problems with existing features: 21 | - AssemblyLoadContext - users must implement all load behavior themselves. If users want support for deps.json files, additional probing paths, RID graphs, and more, they must implement it themselves. As corehost continually changes, these behaviors must be updated. 22 | - additionalDeps - this feature is currently too inflexible. Applciations can control when or how additional dependencies are loaded, and also, it forces unification to a single version of a dependency. 23 | 24 | ## What this library does 25 | 26 | This library provides API for .NET Core app developers to load assemblies as plugins. This API should: 27 | 28 | - reduce the complexity of loading assemblies 29 | - allow plugins to define additional dependencies 30 | - allow controlling the behavior of how types are unified between the host and plugin 31 | - allow the host to customize how and when to load applications 32 | - (stretch goal) allow unloading plugins (depends on unloadable AssemblyLoadContext's) 33 | 34 | ## Implementation 35 | 36 | This API implements these features using: 37 | - AssemblyLoadContext to manage assembly loading and assembly version isolation 38 | - Microsoft.Extensions.DependencyModel to use the .deps.json and .runtimeconfig.json files to express additional dependencies 39 | and search paths for dependencies 40 | 41 | ## Sample usage 42 | 43 | A host and plugin have a shared abstraction 44 | ```c# 45 | public interface IFruit 46 | { 47 | string GetColor() 48 | } 49 | ``` 50 | 51 | A host application could load plugins like this: 52 | 53 | ```c# 54 | // (pseudocode) 55 | public class Program 56 | { 57 | public static void Main(string[] args) 58 | { 59 | foreach (var pluginDirectory in Directory.GetDirectories("plugins/")) 60 | { 61 | var pluginDirName = Path.GetFileName(pluginDir); 62 | var assemblyFile = Path.Combine(pluginDir, pluginDirName + ".dll"); 63 | var loader = PluginLoader.CreateFromAssemblyFile( 64 | assemblyFile: pluginFile, 65 | sharedTypes: new [] { typeof(IFruit) }); 66 | 67 | var plugin = loader.LoadDefaultAssembly(); 68 | foreach (var fruitType in plugin.GetTypes().Where(t => typeof(IFruit).IsAssignableFrom(t) && !t.IsAbstract)) 69 | { 70 | var fruit = (IFruit)Activator.CreateInstance(fruitType, new object[0]); 71 | Console.WriteLine(fruit.GetColor()); 72 | } 73 | 74 | loader.Dispose(); 75 | } 76 | } 77 | } 78 | ``` 79 | 80 | A plugin author could implement the shared abstraction and distribute a plugin without knowing how the host will behave: 81 | 82 | ```xml 83 | 84 | 85 | netstandard2.0 86 | 87 | 88 | ``` 89 | ```c# 90 | internal class MyApple : IFruit 91 | { 92 | public string GetColor() => "Red"; 93 | } 94 | ``` 95 | -------------------------------------------------------------------------------- /docs/what-are-shared-types.md: -------------------------------------------------------------------------------- 1 | # An Explanation of Shared Types 2 | 3 | The PluginLoader API uses the term "shared types". This document explains what it means. 4 | 5 | ```csharp 6 | PluginLoader.CreateFromAssemblyFile("./plugins/MyPlugin/MyPlugin1.dll", 7 | sharedTypes: new [] { typeof(ILogger) }); 8 | 9 | // versus 10 | 11 | PluginLoader.CreateFromAssemblyFile("./plugins/MyPlugin/MyPlugin1.dll", 12 | config => config.PreferSharedTypes = true); 13 | ``` 14 | 15 | ## Concepts 16 | 17 | First, a quick overview of essential concepts. 18 | 19 | #### Type identity 20 | 21 | Type identity what makes a class/struct/enum unique. It is defined by the combination of type name (which includes its namespace), assembly name, 22 | assembly public key token, and assembly version. You can inspect a type's identity in .NET by looking at System.Type.AssemblyQualifiedName. 23 | 24 | For example, 25 | 26 | ```csharp 27 | typeof(ILogger).AssemblyQualifiedName 28 | => "Microsoft.Extensions.Logging.ILogger, Microsoft.Extensions.Logging.Abstractions, Version=2.2.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60" 29 | ``` 30 | 31 | Element | Value 32 | ---------------------------|------------------ 33 | Type name | Microsoft.Extensions.Logging.ILogger 34 | Assembly name | Microsoft.Extensions.Logging.Abstractions 35 | Assembly version | 2.2.0.0 36 | Assembly public key token | adb9793829ddae60 37 | 38 | > Simplification: we're going to ignore the "Culture" part of the fully qualified name for now. 39 | 40 | #### Diamond dependencies 41 | 42 | The [diamond dependency problem][dep-hell] is when a library A depends on libraries B and C, both B and C depend on library D, 43 | but B requires version D.1 and C requires version D.2. 44 | 45 | [dep-hell]: https://en.wikipedia.org/wiki/Dependency_hell 46 | 47 | 48 | 49 | You could solve this problem by 50 | 51 | 1. Choosing D.1 52 | 1. Choosing D.2 53 | 1. Choosing both 54 | 55 | #### Type unification (option 2) 56 | 57 | Type unification is .NET's solution for the diamond dependency problem. In the simple example above, 58 | .NET's build system picks the higher version (D.2) and writes this into the application manifest 59 | (the .deps.json or .config file in build output.) Then, when the application is running and encounters usages of D.1, .NET binds 60 | the usage to D.2 instead. 61 | 62 | In other words, .NET will ignore assembly version when evaluating type identity. 63 | 64 | * Type name 65 | * Assembly name 66 | * ~~Assembly version~~ _this part gets ignored_ 67 | * Assembly public key token 68 | 69 | Why is this done? It allows **type exchange** so code can share instances of types even if the code was originally compiled 70 | with different dependency versions. 71 | 72 | ```csharp 73 | var instanceOfD = new D(); // D.2 74 | new B().DoSomethingWith(instanceOfD); // Library B was compiled to expect D.1, but type unification makes it work with D.2 75 | new C().DoSomethingWith(instanceOfD); 76 | ``` 77 | 78 | ## But what if... 79 | 80 | There are two common problems with type unification. 81 | 82 | 1. Breaking changes: what if library B depends on a behavior of D.1 that changed in D.2 and breaks B? Conversely, what library C uses a new API added in D.2, 83 | but we force the app to use D.1 instead? 84 | 2. Static vs dynamic: what if my app has a plugin system with dynamic dependencies? 85 | 86 | ## This library's answer... 87 | 88 | By default, this `PluginLoader` does not unify any types. This means you can have **multiple versions of the same assembly** 89 | loaded in separate plugins. 90 | 91 | ```csharp 92 | PluginLoader.CreateFromAssemblyFile("./plugins/MyPlugin/MyPlugin1.dll") 93 | ``` 94 | 95 | This can make working with a plugin difficult because it breaks **type exchange**, so the `sharedTypes` list API 96 | is provided to allow you to select which types you want to make sure are unified between the plugin and the 97 | application loading the plugin (aka the host). 98 | ```csharp 99 | PluginLoader.CreateFromAssemblyFile("./plugins/MyPlugin/MyPlugin1.dll", 100 | sharedTypes: new [] { typeof(ILogger) }); 101 | ``` 102 | 103 | Finally, you can invert the default completely to **always attempt to unify** by setting `PreferSharedTypes`. In this mode, 104 | the assembly version provided by the host uses is always used. 105 | ```csharp 106 | PluginLoader.CreateFromAssemblyFile("./plugins/MyPlugin/MyPlugin1.dll", 107 | config => config.PreferSharedTypes = true); 108 | 109 | // In older versions of the library, this API was found on PluginLoaderOptions 110 | PluginLoader.CreateFromAssemblyFile("./plugins/MyPlugin/MyPlugin1.dll", 111 | PluginLoaderOptions.PreferSharedTypes); 112 | ``` 113 | -------------------------------------------------------------------------------- /samples/aspnetcore-mvc/MvcApp/Controllers/HomeController.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using Microsoft.AspNetCore.Mvc; 5 | 6 | namespace MvcWebApp.Controllers 7 | { 8 | public class HomeController : Controller 9 | { 10 | public IActionResult Index() 11 | { 12 | return View(); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /samples/aspnetcore-mvc/MvcApp/MvcApp.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | 6 | 7 | 8 | 12 | 13 | 14 | 15 | 16 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /samples/aspnetcore-mvc/MvcApp/Program.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using Microsoft.AspNetCore; 5 | using Microsoft.AspNetCore.Hosting; 6 | 7 | namespace MvcWebApp 8 | { 9 | public class Program 10 | { 11 | public static void Main(string[] args) 12 | { 13 | CreateWebHostBuilder(args).Build().Run(); 14 | } 15 | 16 | public static IWebHostBuilder CreateWebHostBuilder(string[] args) => 17 | WebHost.CreateDefaultBuilder(args) 18 | .UseStartup(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /samples/aspnetcore-mvc/MvcApp/Startup.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.IO; 6 | using Microsoft.AspNetCore.Builder; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.Extensions.DependencyInjection; 9 | 10 | namespace MvcWebApp 11 | { 12 | public class Startup 13 | { 14 | public void ConfigureServices(IServiceCollection services) 15 | { 16 | var mvcBuilder = services.AddMvc(); 17 | 18 | foreach (var dir in Directory.GetDirectories(Path.Combine(AppContext.BaseDirectory, "plugins"))) 19 | { 20 | var pluginFile = Path.Combine(dir, Path.GetFileName(dir) + ".dll"); 21 | // The AddPluginFromAssemblyFile method comes from McMaster.NETCore.Plugins.Mvc 22 | mvcBuilder.AddPluginFromAssemblyFile(pluginFile); 23 | } 24 | } 25 | 26 | public void Configure(IApplicationBuilder app) 27 | { 28 | app.UseDeveloperExceptionPage(); 29 | app 30 | .UseRouting() 31 | .UseEndpoints(r => 32 | { 33 | r.MapDefaultControllerRoute(); 34 | }); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /samples/aspnetcore-mvc/MvcApp/Views/Home/Index.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "Home Page"; 3 | } 4 | 5 |

Hello from the web app.

6 | 7 | 10 | -------------------------------------------------------------------------------- /samples/aspnetcore-mvc/MvcApp/Views/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using MvcWebApp 2 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 3 | -------------------------------------------------------------------------------- /samples/aspnetcore-mvc/MvcAppPlugin1/MvcAppPlugin1.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | true 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /samples/aspnetcore-mvc/MvcAppPlugin1/MyPluginController.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using Microsoft.AspNetCore.Mvc; 5 | 6 | namespace MvcAppPlugin1 7 | { 8 | public class MyPluginController : Controller 9 | { 10 | public IActionResult Index() => View(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /samples/aspnetcore-mvc/MvcAppPlugin1/Views/MyPlugin/Index.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "Plugin1"; 3 | } 4 | 5 |

Hello world from Plugin1!

6 | -------------------------------------------------------------------------------- /samples/aspnetcore-mvc/README.md: -------------------------------------------------------------------------------- 1 | ASP.NET Core MVC Sample 2 | ======================= 3 | 4 | This sample contains 2 projects which demonstrate a simple plugin scenario. 5 | 6 | 1. 'MvcWebApp' is an ASP.NET Core application which scans for a 'plugins' folder in its base directory and attempts to load any plugins it finds 7 | 2. 'MvcAppPlugin1' which implements MVC controllers. 8 | 9 | Normally, an ASP.NET Core MVC application must have a direct dependency on any assemblies 10 | which provide controllers. However, as this sample demonstrates, an MVC application 11 | can load controllers from a list of assemblies which is not known ahead of time when the 12 | host app is built. 13 | 14 | ## Running the sample 15 | 16 | Open a command line to this folder and run: 17 | 18 | ``` 19 | dotnet restore 20 | dotnet run --project MvcApp/ 21 | ``` 22 | 23 | Then open 24 | -------------------------------------------------------------------------------- /samples/aspnetcore-mvc/aspnetcore-mvc.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26124.0 5 | MinimumVisualStudioVersion = 15.0.26124.0 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MvcApp", "MvcApp\MvcApp.csproj", "{4F363469-7783-4A40-B1B5-455187AF1C76}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MvcAppPlugin1", "MvcAppPlugin1\MvcAppPlugin1.csproj", "{B6316996-A470-470F-9CF1-475096A0510A}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Debug|x64 = Debug|x64 14 | Debug|x86 = Debug|x86 15 | Release|Any CPU = Release|Any CPU 16 | Release|x64 = Release|x64 17 | Release|x86 = Release|x86 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 23 | {4F363469-7783-4A40-B1B5-455187AF1C76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {4F363469-7783-4A40-B1B5-455187AF1C76}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {4F363469-7783-4A40-B1B5-455187AF1C76}.Debug|x64.ActiveCfg = Debug|Any CPU 26 | {4F363469-7783-4A40-B1B5-455187AF1C76}.Debug|x64.Build.0 = Debug|Any CPU 27 | {4F363469-7783-4A40-B1B5-455187AF1C76}.Debug|x86.ActiveCfg = Debug|Any CPU 28 | {4F363469-7783-4A40-B1B5-455187AF1C76}.Debug|x86.Build.0 = Debug|Any CPU 29 | {4F363469-7783-4A40-B1B5-455187AF1C76}.Release|Any CPU.ActiveCfg = Release|Any CPU 30 | {4F363469-7783-4A40-B1B5-455187AF1C76}.Release|Any CPU.Build.0 = Release|Any CPU 31 | {4F363469-7783-4A40-B1B5-455187AF1C76}.Release|x64.ActiveCfg = Release|Any CPU 32 | {4F363469-7783-4A40-B1B5-455187AF1C76}.Release|x64.Build.0 = Release|Any CPU 33 | {4F363469-7783-4A40-B1B5-455187AF1C76}.Release|x86.ActiveCfg = Release|Any CPU 34 | {4F363469-7783-4A40-B1B5-455187AF1C76}.Release|x86.Build.0 = Release|Any CPU 35 | {B6316996-A470-470F-9CF1-475096A0510A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 36 | {B6316996-A470-470F-9CF1-475096A0510A}.Debug|Any CPU.Build.0 = Debug|Any CPU 37 | {B6316996-A470-470F-9CF1-475096A0510A}.Debug|x64.ActiveCfg = Debug|Any CPU 38 | {B6316996-A470-470F-9CF1-475096A0510A}.Debug|x64.Build.0 = Debug|Any CPU 39 | {B6316996-A470-470F-9CF1-475096A0510A}.Debug|x86.ActiveCfg = Debug|Any CPU 40 | {B6316996-A470-470F-9CF1-475096A0510A}.Debug|x86.Build.0 = Debug|Any CPU 41 | {B6316996-A470-470F-9CF1-475096A0510A}.Release|Any CPU.ActiveCfg = Release|Any CPU 42 | {B6316996-A470-470F-9CF1-475096A0510A}.Release|Any CPU.Build.0 = Release|Any CPU 43 | {B6316996-A470-470F-9CF1-475096A0510A}.Release|x64.ActiveCfg = Release|Any CPU 44 | {B6316996-A470-470F-9CF1-475096A0510A}.Release|x64.Build.0 = Release|Any CPU 45 | {B6316996-A470-470F-9CF1-475096A0510A}.Release|x86.ActiveCfg = Release|Any CPU 46 | {B6316996-A470-470F-9CF1-475096A0510A}.Release|x86.Build.0 = Release|Any CPU 47 | EndGlobalSection 48 | EndGlobal 49 | -------------------------------------------------------------------------------- /samples/aspnetcore/Abstractions/Abstractions.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /samples/aspnetcore/Abstractions/IPlugin.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using Microsoft.AspNetCore.Builder; 5 | using Microsoft.Extensions.DependencyInjection; 6 | 7 | namespace Plugin.Abstractions 8 | { 9 | public interface IWebPlugin 10 | { 11 | void Configure(IApplicationBuilder appBuilder); 12 | void ConfigureServices(IServiceCollection services); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /samples/aspnetcore/Abstractions/IPluginLink.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | namespace Plugin.Abstractions 5 | { 6 | public interface IPluginLink 7 | { 8 | string GetHref(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /samples/aspnetcore/MainWebApp/MainWebApp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | 16 | 17 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /samples/aspnetcore/MainWebApp/Pages/Index.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @using MainWebApp 3 | @model IndexModel 4 | 12 | -------------------------------------------------------------------------------- /samples/aspnetcore/MainWebApp/Pages/Index.cshtml.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using Microsoft.AspNetCore.Mvc.RazorPages; 7 | using Plugin.Abstractions; 8 | 9 | namespace MainWebApp 10 | { 11 | public class IndexModel : PageModel 12 | { 13 | public IndexModel(IEnumerable pluginLinks) 14 | { 15 | Links = pluginLinks.Select(p => p.GetHref()).ToArray(); 16 | } 17 | 18 | public string[] Links { get; } 19 | 20 | public void OnGet() 21 | { 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /samples/aspnetcore/MainWebApp/Program.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using Microsoft.AspNetCore; 5 | using Microsoft.AspNetCore.Hosting; 6 | 7 | namespace MainWebApp 8 | { 9 | public class Program 10 | { 11 | public static void Main(string[] args) 12 | { 13 | CreateWebHostBuilder(args).Build().Run(); 14 | } 15 | 16 | public static IWebHostBuilder CreateWebHostBuilder(string[] args) => 17 | WebHost.CreateDefaultBuilder(args) 18 | .UseStartup(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /samples/aspnetcore/MainWebApp/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:35566", 7 | "sslPort": 44362 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "WebAppWithPlugins": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 22 | "environmentVariables": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | } 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /samples/aspnetcore/MainWebApp/Startup.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Linq; 8 | using McMaster.NETCore.Plugins; 9 | using Microsoft.AspNetCore.Builder; 10 | using Microsoft.AspNetCore.Hosting; 11 | using Microsoft.Extensions.DependencyInjection; 12 | using Plugin.Abstractions; 13 | 14 | namespace MainWebApp 15 | { 16 | public class Startup 17 | { 18 | private readonly List _plugins = new(); 19 | 20 | public Startup() 21 | { 22 | foreach (var pluginDir in Directory.GetDirectories(Path.Combine(AppContext.BaseDirectory, "plugins"))) 23 | { 24 | var dirName = Path.GetFileName(pluginDir); 25 | var pluginFile = Path.Combine(pluginDir, dirName + ".dll"); 26 | var loader = PluginLoader.CreateFromAssemblyFile(pluginFile, 27 | // this ensures that the plugin resolves to the same version of DependencyInjection 28 | // and ASP.NET Core that the current app uses 29 | sharedTypes: new[] 30 | { 31 | typeof(IApplicationBuilder), 32 | typeof(IWebPlugin), 33 | typeof(IServiceCollection), 34 | }); 35 | foreach (var type in loader.LoadDefaultAssembly() 36 | .GetTypes() 37 | .Where(t => typeof(IWebPlugin).IsAssignableFrom(t) && !t.IsAbstract)) 38 | { 39 | Console.WriteLine("Found plugin " + type.Name); 40 | var plugin = (IWebPlugin)Activator.CreateInstance(type)!; 41 | _plugins.Add(plugin); 42 | } 43 | } 44 | } 45 | 46 | public void ConfigureServices(IServiceCollection services) 47 | { 48 | services.AddMvc().AddMvcOptions(o => o.EnableEndpointRouting = false); 49 | 50 | foreach (var plugin in _plugins) 51 | { 52 | plugin.ConfigureServices(services); 53 | } 54 | } 55 | 56 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 57 | { 58 | app.UseDeveloperExceptionPage(); 59 | 60 | foreach (var plugin in _plugins) 61 | { 62 | plugin.Configure(app); 63 | } 64 | 65 | app.UseMvcWithDefaultRoute(); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /samples/aspnetcore/README.md: -------------------------------------------------------------------------------- 1 | ASP.NET Core Sample 2 | =================== 3 | 4 | This sample contains 4 projects which demonstrate a simple plugin scenario. 5 | 6 | 1. 'Abstractions' defines common interfaces shared by the web application (host) and plugins 7 | 2. 'MainWebApp' is an ASP.NET Core application which scans for a 'plugins' folder in its base directory and attempts to load any plugins it finds 8 | 3. 'WebAppPlugin1' references 'Abstractions' and implements `IWebPlugin`. This plugin has a dependency on [AutoMapper](https://www.nuget.org/packages/AutoMapper/) version 6. 9 | 4. 'WebAppPlugin2' is the same as plugin1, but it uses AutoMapper version 7. 10 | 11 | Normally, in .NET Core applications you cannot reference two different versions of the same assembly. 12 | However, as this sample demonstrates, using .NET Core plugins you can load and use two different versions. 13 | 14 | * http://localhost:5000/plugin/v1 responds with 15 | ``` 16 | This plugin uses AutoMapper, Version=6.2.2.0, Culture=neutral, PublicKeyToken=be96cd2c38ef1005 17 | ``` 18 | 19 | * http://localhost:5000/plugin/v2 responds with 20 | ``` 21 | This plugin uses AutoMapper, Version=7.0.1.0, Culture=neutral, PublicKeyToken=be96cd2c38ef1005 22 | ``` 23 | 24 | There are some important types, however, which must share the same identity between the plugins and the host. 25 | To ensure type exchange works between the host and the plugins, the MainWebApp project uses the `sharedTypes` 26 | parameter on `PluginLoader.CreateFromAssemblyFile`. 27 | 28 | ```csharp 29 | var loader = PluginLoader.CreateFromAssemblyFile( 30 | pluginAssembly, 31 | sharedTypes: new[] 32 | { 33 | typeof(IApplicationBuilder), 34 | typeof(IWebPlugin), 35 | typeof(IServiceCollection), 36 | }); 37 | ``` 38 | 39 | This is important because the plugins in this sample are compiled for ASP.NET Core 2.0 interfaces, 40 | but the MainWebApp uses ASP.NET Core 2.1. If not for this parameter, the plugins would also attempt to use 41 | a private copy of the ASP.NET Core implementations and type exchange between the plugin and the web app 42 | would fail to resolve `IApplicationBuilder` and `IServiceCollection` as the same type. 43 | -------------------------------------------------------------------------------- /samples/aspnetcore/WebAppPlugin1/WebAppPlugin1.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /samples/aspnetcore/WebAppPlugin1/WebPlugin1.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using Microsoft.AspNetCore.Builder; 5 | using Microsoft.AspNetCore.Http; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Plugin.Abstractions; 8 | 9 | namespace Plugin1 10 | { 11 | internal class WebPlugin1 : IWebPlugin, IPluginLink 12 | { 13 | public string GetHref() => "/plugin/v1"; 14 | 15 | public void ConfigureServices(IServiceCollection services) 16 | { 17 | services.AddScoped(); 18 | } 19 | 20 | public void Configure(IApplicationBuilder appBuilder) 21 | { 22 | appBuilder.Map("/plugin/v1", c => 23 | { 24 | var autoMapperType = typeof(AutoMapper.IMapper).Assembly; 25 | c.Run(async (ctx) => 26 | { 27 | await ctx.Response.WriteAsync("This plugin uses " + autoMapperType.GetName().ToString()); 28 | }); 29 | }); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /samples/aspnetcore/WebAppPlugin2/WebAppPlugin2.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /samples/aspnetcore/WebAppPlugin2/WebPlugin2.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using Microsoft.AspNetCore.Builder; 5 | using Microsoft.AspNetCore.Http; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Plugin.Abstractions; 8 | 9 | namespace Plugin2 10 | { 11 | internal class WebPlugin2 : IWebPlugin, IPluginLink 12 | { 13 | public string GetHref() => "/plugin/v2"; 14 | 15 | public void ConfigureServices(IServiceCollection services) 16 | { 17 | services.AddScoped(); 18 | } 19 | 20 | public void Configure(IApplicationBuilder appBuilder) 21 | { 22 | appBuilder.Map("/plugin/v2", c => 23 | { 24 | var autoMapperType = typeof(AutoMapper.IMapper).Assembly; 25 | c.Run(async (ctx) => 26 | { 27 | await ctx.Response.WriteAsync("This plugin uses " + autoMapperType.GetName().ToString()); 28 | }); 29 | }); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /samples/aspnetcore/aspnetcore.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26124.0 5 | MinimumVisualStudioVersion = 15.0.26124.0 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Abstractions", "Abstractions\Abstractions.csproj", "{7CDF7B07-F103-4C22-9BB3-26B7C11716FF}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MainWebApp", "MainWebApp\MainWebApp.csproj", "{781A86B1-C278-44CD-997B-1AA26D6BFB38}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebAppPlugin1", "WebAppPlugin1\WebAppPlugin1.csproj", "{B17BE4CB-C382-4C7D-8B11-FD21E0E1A7CB}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebAppPlugin2", "WebAppPlugin2\WebAppPlugin2.csproj", "{ECAB37EC-1E82-4B1F-8C51-983E46A557A4}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Debug|x64 = Debug|x64 18 | Debug|x86 = Debug|x86 19 | Release|Any CPU = Release|Any CPU 20 | Release|x64 = Release|x64 21 | Release|x86 = Release|x86 22 | EndGlobalSection 23 | GlobalSection(SolutionProperties) = preSolution 24 | HideSolutionNode = FALSE 25 | EndGlobalSection 26 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 27 | {7CDF7B07-F103-4C22-9BB3-26B7C11716FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 28 | {7CDF7B07-F103-4C22-9BB3-26B7C11716FF}.Debug|Any CPU.Build.0 = Debug|Any CPU 29 | {7CDF7B07-F103-4C22-9BB3-26B7C11716FF}.Debug|x64.ActiveCfg = Debug|Any CPU 30 | {7CDF7B07-F103-4C22-9BB3-26B7C11716FF}.Debug|x64.Build.0 = Debug|Any CPU 31 | {7CDF7B07-F103-4C22-9BB3-26B7C11716FF}.Debug|x86.ActiveCfg = Debug|Any CPU 32 | {7CDF7B07-F103-4C22-9BB3-26B7C11716FF}.Debug|x86.Build.0 = Debug|Any CPU 33 | {7CDF7B07-F103-4C22-9BB3-26B7C11716FF}.Release|Any CPU.ActiveCfg = Release|Any CPU 34 | {7CDF7B07-F103-4C22-9BB3-26B7C11716FF}.Release|Any CPU.Build.0 = Release|Any CPU 35 | {7CDF7B07-F103-4C22-9BB3-26B7C11716FF}.Release|x64.ActiveCfg = Release|Any CPU 36 | {7CDF7B07-F103-4C22-9BB3-26B7C11716FF}.Release|x64.Build.0 = Release|Any CPU 37 | {7CDF7B07-F103-4C22-9BB3-26B7C11716FF}.Release|x86.ActiveCfg = Release|Any CPU 38 | {7CDF7B07-F103-4C22-9BB3-26B7C11716FF}.Release|x86.Build.0 = Release|Any CPU 39 | {781A86B1-C278-44CD-997B-1AA26D6BFB38}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 40 | {781A86B1-C278-44CD-997B-1AA26D6BFB38}.Debug|Any CPU.Build.0 = Debug|Any CPU 41 | {781A86B1-C278-44CD-997B-1AA26D6BFB38}.Debug|x64.ActiveCfg = Debug|Any CPU 42 | {781A86B1-C278-44CD-997B-1AA26D6BFB38}.Debug|x64.Build.0 = Debug|Any CPU 43 | {781A86B1-C278-44CD-997B-1AA26D6BFB38}.Debug|x86.ActiveCfg = Debug|Any CPU 44 | {781A86B1-C278-44CD-997B-1AA26D6BFB38}.Debug|x86.Build.0 = Debug|Any CPU 45 | {781A86B1-C278-44CD-997B-1AA26D6BFB38}.Release|Any CPU.ActiveCfg = Release|Any CPU 46 | {781A86B1-C278-44CD-997B-1AA26D6BFB38}.Release|Any CPU.Build.0 = Release|Any CPU 47 | {781A86B1-C278-44CD-997B-1AA26D6BFB38}.Release|x64.ActiveCfg = Release|Any CPU 48 | {781A86B1-C278-44CD-997B-1AA26D6BFB38}.Release|x64.Build.0 = Release|Any CPU 49 | {781A86B1-C278-44CD-997B-1AA26D6BFB38}.Release|x86.ActiveCfg = Release|Any CPU 50 | {781A86B1-C278-44CD-997B-1AA26D6BFB38}.Release|x86.Build.0 = Release|Any CPU 51 | {B17BE4CB-C382-4C7D-8B11-FD21E0E1A7CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 52 | {B17BE4CB-C382-4C7D-8B11-FD21E0E1A7CB}.Debug|Any CPU.Build.0 = Debug|Any CPU 53 | {B17BE4CB-C382-4C7D-8B11-FD21E0E1A7CB}.Debug|x64.ActiveCfg = Debug|Any CPU 54 | {B17BE4CB-C382-4C7D-8B11-FD21E0E1A7CB}.Debug|x64.Build.0 = Debug|Any CPU 55 | {B17BE4CB-C382-4C7D-8B11-FD21E0E1A7CB}.Debug|x86.ActiveCfg = Debug|Any CPU 56 | {B17BE4CB-C382-4C7D-8B11-FD21E0E1A7CB}.Debug|x86.Build.0 = Debug|Any CPU 57 | {B17BE4CB-C382-4C7D-8B11-FD21E0E1A7CB}.Release|Any CPU.ActiveCfg = Release|Any CPU 58 | {B17BE4CB-C382-4C7D-8B11-FD21E0E1A7CB}.Release|Any CPU.Build.0 = Release|Any CPU 59 | {B17BE4CB-C382-4C7D-8B11-FD21E0E1A7CB}.Release|x64.ActiveCfg = Release|Any CPU 60 | {B17BE4CB-C382-4C7D-8B11-FD21E0E1A7CB}.Release|x64.Build.0 = Release|Any CPU 61 | {B17BE4CB-C382-4C7D-8B11-FD21E0E1A7CB}.Release|x86.ActiveCfg = Release|Any CPU 62 | {B17BE4CB-C382-4C7D-8B11-FD21E0E1A7CB}.Release|x86.Build.0 = Release|Any CPU 63 | {ECAB37EC-1E82-4B1F-8C51-983E46A557A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 64 | {ECAB37EC-1E82-4B1F-8C51-983E46A557A4}.Debug|Any CPU.Build.0 = Debug|Any CPU 65 | {ECAB37EC-1E82-4B1F-8C51-983E46A557A4}.Debug|x64.ActiveCfg = Debug|Any CPU 66 | {ECAB37EC-1E82-4B1F-8C51-983E46A557A4}.Debug|x64.Build.0 = Debug|Any CPU 67 | {ECAB37EC-1E82-4B1F-8C51-983E46A557A4}.Debug|x86.ActiveCfg = Debug|Any CPU 68 | {ECAB37EC-1E82-4B1F-8C51-983E46A557A4}.Debug|x86.Build.0 = Debug|Any CPU 69 | {ECAB37EC-1E82-4B1F-8C51-983E46A557A4}.Release|Any CPU.ActiveCfg = Release|Any CPU 70 | {ECAB37EC-1E82-4B1F-8C51-983E46A557A4}.Release|Any CPU.Build.0 = Release|Any CPU 71 | {ECAB37EC-1E82-4B1F-8C51-983E46A557A4}.Release|x64.ActiveCfg = Release|Any CPU 72 | {ECAB37EC-1E82-4B1F-8C51-983E46A557A4}.Release|x64.Build.0 = Release|Any CPU 73 | {ECAB37EC-1E82-4B1F-8C51-983E46A557A4}.Release|x86.ActiveCfg = Release|Any CPU 74 | {ECAB37EC-1E82-4B1F-8C51-983E46A557A4}.Release|x86.Build.0 = Release|Any CPU 75 | EndGlobalSection 76 | EndGlobal 77 | -------------------------------------------------------------------------------- /samples/dependency-injection/DI.HostApp/DI.HostApp.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net8.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /samples/dependency-injection/DI.HostApp/Program.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Linq; 8 | using McMaster.NETCore.Plugins; 9 | using Microsoft.Extensions.DependencyInjection; 10 | 11 | namespace DependencyInjection 12 | { 13 | public class Program 14 | { 15 | public static void Main(string[] args) 16 | { 17 | var services = new ServiceCollection(); 18 | var loaders = GetPluginLoaders(); 19 | 20 | ConfigureServices(services, loaders); 21 | 22 | using var serviceProvider = services.BuildServiceProvider(); 23 | 24 | var consumer = serviceProvider.GetRequiredService(); 25 | consumer.Consume(); 26 | } 27 | 28 | private static List GetPluginLoaders() 29 | { 30 | var loaders = new List(); 31 | 32 | // create plugin loaders 33 | var pluginsDir = Path.Combine(AppContext.BaseDirectory, "plugins"); 34 | foreach (var dir in Directory.GetDirectories(pluginsDir)) 35 | { 36 | var dirName = Path.GetFileName(dir); 37 | var pluginDll = Path.Combine(dir, dirName + ".dll"); 38 | if (File.Exists(pluginDll)) 39 | { 40 | var loader = PluginLoader.CreateFromAssemblyFile( 41 | pluginDll, 42 | sharedTypes: new[] { typeof(IPluginFactory), typeof(IServiceCollection) }); 43 | loaders.Add(loader); 44 | } 45 | } 46 | 47 | return loaders; 48 | } 49 | 50 | private static void ConfigureServices(ServiceCollection services, List loaders) 51 | { 52 | // Create an instance of plugin types 53 | foreach (var loader in loaders) 54 | { 55 | foreach (var pluginType in loader 56 | .LoadDefaultAssembly() 57 | .GetTypes() 58 | .Where(t => typeof(IPluginFactory).IsAssignableFrom(t) && !t.IsAbstract)) 59 | { 60 | // This assumes the implementation of IPluginFactory has a parameterless constructor 61 | var plugin = Activator.CreateInstance(pluginType) as IPluginFactory; 62 | 63 | plugin?.Configure(services); 64 | } 65 | } 66 | } 67 | 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /samples/dependency-injection/DI.SharedAbstractions/DI.SharedAbstractions.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /samples/dependency-injection/DI.SharedAbstractions/Fruit.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | namespace DependencyInjection 5 | { 6 | public class Fruit 7 | { 8 | public Fruit(string name) 9 | { 10 | Name = name; 11 | } 12 | 13 | public string Name { get; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /samples/dependency-injection/DI.SharedAbstractions/IFruitConsumer.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | namespace DependencyInjection 5 | { 6 | public interface IFruitConsumer 7 | { 8 | void Consume(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /samples/dependency-injection/DI.SharedAbstractions/IFruitProducer.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System.Collections.Generic; 5 | 6 | namespace DependencyInjection 7 | { 8 | public interface IFruitProducer 9 | { 10 | IEnumerable Produce(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /samples/dependency-injection/DI.SharedAbstractions/IPluginFactory.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using Microsoft.Extensions.DependencyInjection; 5 | 6 | namespace DependencyInjection 7 | { 8 | public interface IPluginFactory 9 | { 10 | void Configure(IServiceCollection services); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /samples/dependency-injection/MyPlugin1/MyFruitProducer.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System.Collections.Generic; 5 | using DependencyInjection; 6 | 7 | namespace MyPlugin1 8 | { 9 | internal class MyFruitProducer : IFruitProducer 10 | { 11 | public IEnumerable Produce() 12 | { 13 | yield return new Fruit("banana"); 14 | yield return new Fruit("orange"); 15 | yield return new Fruit("strawberry"); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /samples/dependency-injection/MyPlugin1/MyPlugin1.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /samples/dependency-injection/MyPlugin1/PluginConfiguration.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using DependencyInjection; 5 | using Microsoft.Extensions.DependencyInjection; 6 | 7 | namespace MyPlugin1 8 | { 9 | public class PluginConfiguration : IPluginFactory 10 | { 11 | public void Configure(IServiceCollection services) 12 | { 13 | services.AddSingleton(); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /samples/dependency-injection/MyPlugin2/MyFruitConsumer.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using DependencyInjection; 7 | 8 | namespace MyPlugin2 9 | { 10 | internal class MyFruitConsumer : IFruitConsumer 11 | { 12 | private readonly IEnumerable _producers; 13 | 14 | public MyFruitConsumer(IEnumerable producers) 15 | { 16 | _producers = producers; 17 | } 18 | 19 | public void Consume() 20 | { 21 | foreach (var producer in _producers) 22 | { 23 | foreach (var fruit in producer.Produce()) 24 | { 25 | Console.WriteLine($"Consumed {fruit.Name}"); 26 | } 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /samples/dependency-injection/MyPlugin2/MyPlugin2.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /samples/dependency-injection/MyPlugin2/PluginConfiguration.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using DependencyInjection; 5 | using Microsoft.Extensions.DependencyInjection; 6 | 7 | namespace MyPlugin2 8 | { 9 | public class PluginConfiguration : IPluginFactory 10 | { 11 | public void Configure(IServiceCollection services) 12 | { 13 | services.AddSingleton(); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /samples/dependency-injection/README.md: -------------------------------------------------------------------------------- 1 | Dependency Injection Sample 2 | =========================== 3 | 4 | This sample contains 4 projects which demonstrate a plugin scenario which coordinates types between 5 | plugins using a dependency injection container. 6 | 7 | * 'DI.HostApp' is a console application which scans for a 'plugins' folder in its base directory and attempts to load any plugins it finds. It then configures plugins in a dependency injection collection. 8 | * 'DI.SharedAbstractions' which contains an interface shared by plugins and the host. 9 | * 'MyPlugin1' and 'MyPlugin2' implement shared abstractions and register them with the host. 10 | 11 | ## Running the sample 12 | 13 | Open a command line to this folder and run: 14 | 15 | ``` 16 | dotnet restore 17 | dotnet run --project DI.HostApp/ 18 | ``` 19 | -------------------------------------------------------------------------------- /samples/dynamic-implementation/Contracts/Contracts.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Library 5 | net8.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /samples/dynamic-implementation/Contracts/Fruit.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | namespace Contracts 5 | { 6 | public class Fruit 7 | { 8 | public string? Name { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /samples/dynamic-implementation/Contracts/IFruitService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System.Collections.Generic; 5 | 6 | namespace Contracts 7 | { 8 | public interface IFruitService 9 | { 10 | List GetFruits(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /samples/dynamic-implementation/Contracts/IMixerService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | namespace Contracts 5 | { 6 | public interface IMixerService 7 | { 8 | string MixIt(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /samples/dynamic-implementation/Contracts/IPluginFactory.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using Microsoft.Extensions.DependencyInjection; 5 | 6 | namespace Contracts 7 | { 8 | public interface IPluginFactory 9 | { 10 | void Configure(IServiceCollection services); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /samples/dynamic-implementation/DynamicImplementation.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.28307.757 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Host", "Host\Host.csproj", "{52016A46-23E0-48BB-AB79-CA13BECB55DF}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Contracts", "Contracts\Contracts.csproj", "{138FD514-7F88-43A5-A3CD-6084ABA316CD}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServiceImplementation", "ServiceImplementation\ServiceImplementation.csproj", "{A8104C41-1224-4626-A812-ED561BD35432}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mixer", "Mixer\Mixer.csproj", "{DB9CE54F-D2A1-454A-BF58-19F41D4F3E9B}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {52016A46-23E0-48BB-AB79-CA13BECB55DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {52016A46-23E0-48BB-AB79-CA13BECB55DF}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {52016A46-23E0-48BB-AB79-CA13BECB55DF}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {52016A46-23E0-48BB-AB79-CA13BECB55DF}.Release|Any CPU.Build.0 = Release|Any CPU 24 | {138FD514-7F88-43A5-A3CD-6084ABA316CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {138FD514-7F88-43A5-A3CD-6084ABA316CD}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {138FD514-7F88-43A5-A3CD-6084ABA316CD}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {138FD514-7F88-43A5-A3CD-6084ABA316CD}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {A8104C41-1224-4626-A812-ED561BD35432}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {A8104C41-1224-4626-A812-ED561BD35432}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {A8104C41-1224-4626-A812-ED561BD35432}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {A8104C41-1224-4626-A812-ED561BD35432}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {DB9CE54F-D2A1-454A-BF58-19F41D4F3E9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {DB9CE54F-D2A1-454A-BF58-19F41D4F3E9B}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {DB9CE54F-D2A1-454A-BF58-19F41D4F3E9B}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {DB9CE54F-D2A1-454A-BF58-19F41D4F3E9B}.Release|Any CPU.Build.0 = Release|Any CPU 36 | EndGlobalSection 37 | GlobalSection(SolutionProperties) = preSolution 38 | HideSolutionNode = FALSE 39 | EndGlobalSection 40 | GlobalSection(ExtensibilityGlobals) = postSolution 41 | SolutionGuid = {98A28D5B-F00C-44F0-A5DA-80A16E551E57} 42 | EndGlobalSection 43 | EndGlobal 44 | -------------------------------------------------------------------------------- /samples/dynamic-implementation/Host/Host.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net8.0 6 | 7 | 8 | 9 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /samples/dynamic-implementation/Host/Program.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Linq; 8 | using Contracts; 9 | using McMaster.NETCore.Plugins; 10 | using Microsoft.Extensions.DependencyInjection; 11 | 12 | namespace Host 13 | { 14 | internal class Program 15 | { 16 | public static void Main(string[] args) 17 | { 18 | var services = new ServiceCollection(); 19 | var loaders = GetPluginLoaders(); 20 | 21 | ConfigureServices(services, loaders); 22 | 23 | var serviceProvider = services.BuildServiceProvider(); 24 | 25 | var mixer = serviceProvider.GetRequiredService(); 26 | mixer.MixIt(); 27 | } 28 | 29 | private static List GetPluginLoaders() 30 | { 31 | var loaders = new List(); 32 | 33 | // create plugin loaders 34 | var pluginsDir = Path.Combine(AppContext.BaseDirectory, "plugins"); 35 | foreach (var dir in Directory.GetDirectories(pluginsDir)) 36 | { 37 | var dirName = Path.GetFileName(dir); 38 | var pluginDll = Path.Combine(dir, dirName + ".dll"); 39 | if (File.Exists(pluginDll)) 40 | { 41 | var loader = PluginLoader.CreateFromAssemblyFile( 42 | pluginDll, 43 | sharedTypes: new[] { typeof(IPluginFactory), typeof(IServiceCollection) }); 44 | loaders.Add(loader); 45 | } 46 | } 47 | 48 | return loaders; 49 | } 50 | 51 | private static void ConfigureServices(ServiceCollection services, List loaders) 52 | { 53 | // Create an instance of plugin types 54 | foreach (var loader in loaders) 55 | { 56 | foreach (var pluginType in loader 57 | .LoadDefaultAssembly() 58 | .GetTypes() 59 | .Where(t => typeof(IPluginFactory).IsAssignableFrom(t) && !t.IsAbstract)) 60 | { 61 | // This assumes the implementation of IPluginFactory has a parameterless constructor 62 | var plugin = Activator.CreateInstance(pluginType) as IPluginFactory; 63 | 64 | plugin?.Configure(services); 65 | } 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /samples/dynamic-implementation/Mixer/Mixer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | Library 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /samples/dynamic-implementation/Mixer/MixerPlugin.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using Contracts; 5 | using Microsoft.Extensions.DependencyInjection; 6 | 7 | namespace Mixer 8 | { 9 | public class MixerPluginConfiguration : IPluginFactory 10 | { 11 | public void Configure(IServiceCollection services) 12 | { 13 | services.AddSingleton(); 14 | services.AddSingleton(); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /samples/dynamic-implementation/Mixer/MixerService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System.Linq; 5 | using Contracts; 6 | 7 | namespace Mixer 8 | { 9 | public class MixerService : IMixerService 10 | { 11 | protected IFruitService fruit; 12 | public MixerService(IFruitService fruit) 13 | { 14 | this.fruit = fruit; 15 | } 16 | 17 | public string MixIt() 18 | { 19 | return string.Join(",", fruit.GetFruits().Select(x => x.Name).ToArray()); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /samples/dynamic-implementation/Mixer/StandardFruiteService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System.Collections.Generic; 5 | using Contracts; 6 | 7 | namespace Mixer 8 | { 9 | public class StandardFruiteService : IFruitService 10 | { 11 | public List GetFruits() 12 | { 13 | return new List() 14 | { 15 | new Fruit { Name="Banana" }, 16 | new Fruit { Name="Apple" }, 17 | new Fruit { Name="Carrot" }, 18 | }; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /samples/dynamic-implementation/README.md: -------------------------------------------------------------------------------- 1 | Dynamic implementation 2 | =========================== 3 | 4 | This sample contains 4 projects which demonstrate a plugin scenario that coordinates types between 5 | plugins using a dependency injection container. In this scenario, one implements a default service definition (Mixer), but adding a plugin the normal behavior is changed. 6 | 7 | This sample can be useful to create a pluggable application that can be extended or altered just by adding new modules. 8 | 9 | * 'Host': console app that contains the sample 10 | * 'Contracts': which contains an interface shared by plugins and the host. 11 | * 'Mixer': this plugin contains the default mixer implementation. It contains the FruitService, which gives 3 fruits and the Mixer service that returns the fruit shake. 12 | * 'ServiceImplementation': this plugin contains an alternative version of the FruitService. By adding this to the plugin set, the default behavior is altered and the shake is composed by only Banana. 13 | 14 | ## Running the sample 15 | 16 | Open a command line to this folder and run, otherwise open the .sln file: 17 | 18 | ``` 19 | dotnet restore 20 | dotnet run --project Host/ 21 | ``` 22 | -------------------------------------------------------------------------------- /samples/dynamic-implementation/ServiceImplementation/OverrideFruiteService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System.Collections.Generic; 5 | using Contracts; 6 | 7 | namespace ServiceImplementation 8 | { 9 | public class OverrideFruiteService : IFruitService 10 | { 11 | public List GetFruits() 12 | { 13 | return new List() 14 | { 15 | new Fruit { Name="Banana" } 16 | }; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /samples/dynamic-implementation/ServiceImplementation/OverridePlugin.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using Contracts; 5 | using Microsoft.Extensions.DependencyInjection; 6 | 7 | namespace ServiceImplementation 8 | { 9 | public class OverridePluginConfiguration : IPluginFactory 10 | { 11 | public void Configure(IServiceCollection services) 12 | { 13 | //this service override the standard one. unload this plugin or comment this to use the basic service 14 | services.AddSingleton(); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /samples/dynamic-implementation/ServiceImplementation/ServiceImplementation.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Library 5 | net8.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /samples/hello-world/HostApp/HostApp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | 7 | 8 | 9 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /samples/hello-world/HostApp/Program.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Linq; 8 | using HelloWorld; 9 | using McMaster.NETCore.Plugins; 10 | 11 | var loaders = new List(); 12 | 13 | // create plugin loaders 14 | var pluginsDir = Path.Combine(AppContext.BaseDirectory, "plugins"); 15 | foreach (var dir in Directory.GetDirectories(pluginsDir)) 16 | { 17 | var dirName = Path.GetFileName(dir); 18 | var pluginDll = Path.Combine(dir, dirName + ".dll"); 19 | if (File.Exists(pluginDll)) 20 | { 21 | var loader = PluginLoader.CreateFromAssemblyFile( 22 | pluginDll, 23 | sharedTypes: new[] { typeof(IPlugin) }); 24 | loaders.Add(loader); 25 | } 26 | } 27 | 28 | // Create an instance of plugin types 29 | foreach (var loader in loaders) 30 | { 31 | foreach (var pluginType in loader 32 | .LoadDefaultAssembly() 33 | .GetTypes() 34 | .Where(t => typeof(IPlugin).IsAssignableFrom(t) && !t.IsAbstract)) 35 | { 36 | // This assumes the implementation of IPlugin has a parameterless constructor 37 | var plugin = Activator.CreateInstance(pluginType) as IPlugin; 38 | 39 | Console.WriteLine($"Created plugin instance '{plugin?.GetName()}'."); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /samples/hello-world/MyPlugin/MyPlugin.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /samples/hello-world/MyPlugin/MyPlugin1.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using HelloWorld; 5 | 6 | namespace MyPlugin 7 | { 8 | internal class MyPlugin1 : IPlugin 9 | { 10 | public string GetName() => "My plugin v1"; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /samples/hello-world/PluginContract/IPlugin.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | namespace HelloWorld 5 | { 6 | /// 7 | /// This interface is an example of one way to define the interactions between the host and plugins. 8 | /// There is nothing special about the name "IPlugin"; it's used here to illustrate a concept. 9 | /// Look at https://github.com/natemcmaster/DotNetCorePlugins/tree/main/samples for additional examples 10 | /// of ways you could define the interaction between host and plugins. 11 | /// 12 | public interface IPlugin 13 | { 14 | string GetName(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /samples/hello-world/PluginContract/PluginContract.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /samples/hello-world/README.md: -------------------------------------------------------------------------------- 1 | "Hello World" Sample 2 | ==================== 3 | 4 | This sample contains 3 projects which demonstrate a simple plugin scenario. 5 | 6 | 1. 'HostApp' is a console application which scans for a 'plugins' folder in its base directory and attempts to load any plugins it finds 7 | 2. 'MyPlugin' which implements an implementation of this plugin 8 | 3. 'PluginContract' which contains an interface shared by plugins and the host. 9 | 10 | ## Running the sample 11 | 12 | Open a command line to this folder and run: 13 | 14 | ``` 15 | dotnet restore 16 | dotnet run --project HostApp/ 17 | ``` 18 | -------------------------------------------------------------------------------- /samples/hello-world/hello-world.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26124.0 5 | MinimumVisualStudioVersion = 15.0.26124.0 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HostApp", "HostApp\HostApp.csproj", "{FAA91115-37CE-4A74-8EC1-C58E15E5D683}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyPlugin", "MyPlugin\MyPlugin.csproj", "{EF895D6F-B174-46AF-936C-F4E3FE2B96E0}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PluginContract", "PluginContract\PluginContract.csproj", "{5F3E90BD-7D9E-4469-A89A-63B0A8F77465}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Debug|x64 = Debug|x64 16 | Debug|x86 = Debug|x86 17 | Release|Any CPU = Release|Any CPU 18 | Release|x64 = Release|x64 19 | Release|x86 = Release|x86 20 | EndGlobalSection 21 | GlobalSection(SolutionProperties) = preSolution 22 | HideSolutionNode = FALSE 23 | EndGlobalSection 24 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 25 | {FAA91115-37CE-4A74-8EC1-C58E15E5D683}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {FAA91115-37CE-4A74-8EC1-C58E15E5D683}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {FAA91115-37CE-4A74-8EC1-C58E15E5D683}.Debug|x64.ActiveCfg = Debug|Any CPU 28 | {FAA91115-37CE-4A74-8EC1-C58E15E5D683}.Debug|x64.Build.0 = Debug|Any CPU 29 | {FAA91115-37CE-4A74-8EC1-C58E15E5D683}.Debug|x86.ActiveCfg = Debug|Any CPU 30 | {FAA91115-37CE-4A74-8EC1-C58E15E5D683}.Debug|x86.Build.0 = Debug|Any CPU 31 | {FAA91115-37CE-4A74-8EC1-C58E15E5D683}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {FAA91115-37CE-4A74-8EC1-C58E15E5D683}.Release|Any CPU.Build.0 = Release|Any CPU 33 | {FAA91115-37CE-4A74-8EC1-C58E15E5D683}.Release|x64.ActiveCfg = Release|Any CPU 34 | {FAA91115-37CE-4A74-8EC1-C58E15E5D683}.Release|x64.Build.0 = Release|Any CPU 35 | {FAA91115-37CE-4A74-8EC1-C58E15E5D683}.Release|x86.ActiveCfg = Release|Any CPU 36 | {FAA91115-37CE-4A74-8EC1-C58E15E5D683}.Release|x86.Build.0 = Release|Any CPU 37 | {EF895D6F-B174-46AF-936C-F4E3FE2B96E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 38 | {EF895D6F-B174-46AF-936C-F4E3FE2B96E0}.Debug|Any CPU.Build.0 = Debug|Any CPU 39 | {EF895D6F-B174-46AF-936C-F4E3FE2B96E0}.Debug|x64.ActiveCfg = Debug|Any CPU 40 | {EF895D6F-B174-46AF-936C-F4E3FE2B96E0}.Debug|x64.Build.0 = Debug|Any CPU 41 | {EF895D6F-B174-46AF-936C-F4E3FE2B96E0}.Debug|x86.ActiveCfg = Debug|Any CPU 42 | {EF895D6F-B174-46AF-936C-F4E3FE2B96E0}.Debug|x86.Build.0 = Debug|Any CPU 43 | {EF895D6F-B174-46AF-936C-F4E3FE2B96E0}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {EF895D6F-B174-46AF-936C-F4E3FE2B96E0}.Release|Any CPU.Build.0 = Release|Any CPU 45 | {EF895D6F-B174-46AF-936C-F4E3FE2B96E0}.Release|x64.ActiveCfg = Release|Any CPU 46 | {EF895D6F-B174-46AF-936C-F4E3FE2B96E0}.Release|x64.Build.0 = Release|Any CPU 47 | {EF895D6F-B174-46AF-936C-F4E3FE2B96E0}.Release|x86.ActiveCfg = Release|Any CPU 48 | {EF895D6F-B174-46AF-936C-F4E3FE2B96E0}.Release|x86.Build.0 = Release|Any CPU 49 | {5F3E90BD-7D9E-4469-A89A-63B0A8F77465}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 50 | {5F3E90BD-7D9E-4469-A89A-63B0A8F77465}.Debug|Any CPU.Build.0 = Debug|Any CPU 51 | {5F3E90BD-7D9E-4469-A89A-63B0A8F77465}.Debug|x64.ActiveCfg = Debug|Any CPU 52 | {5F3E90BD-7D9E-4469-A89A-63B0A8F77465}.Debug|x64.Build.0 = Debug|Any CPU 53 | {5F3E90BD-7D9E-4469-A89A-63B0A8F77465}.Debug|x86.ActiveCfg = Debug|Any CPU 54 | {5F3E90BD-7D9E-4469-A89A-63B0A8F77465}.Debug|x86.Build.0 = Debug|Any CPU 55 | {5F3E90BD-7D9E-4469-A89A-63B0A8F77465}.Release|Any CPU.ActiveCfg = Release|Any CPU 56 | {5F3E90BD-7D9E-4469-A89A-63B0A8F77465}.Release|Any CPU.Build.0 = Release|Any CPU 57 | {5F3E90BD-7D9E-4469-A89A-63B0A8F77465}.Release|x64.ActiveCfg = Release|Any CPU 58 | {5F3E90BD-7D9E-4469-A89A-63B0A8F77465}.Release|x64.Build.0 = Release|Any CPU 59 | {5F3E90BD-7D9E-4469-A89A-63B0A8F77465}.Release|x86.ActiveCfg = Release|Any CPU 60 | {5F3E90BD-7D9E-4469-A89A-63B0A8F77465}.Release|x86.Build.0 = Release|Any CPU 61 | EndGlobalSection 62 | EndGlobal 63 | -------------------------------------------------------------------------------- /samples/hot-reload/HotReloadApp/HotReloadApp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | 7 | 8 | 9 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /samples/hot-reload/HotReloadApp/Program.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using McMaster.NETCore.Plugins; 8 | 9 | var pluginPath = args[0]; 10 | var loader = PluginLoader.CreateFromAssemblyFile(pluginPath, 11 | config => config.EnableHotReload = true); 12 | 13 | loader.Reloaded += ShowPluginInfo; 14 | 15 | var cts = new CancellationTokenSource(); 16 | Console.CancelKeyPress += (_, __) => cts.Cancel(); 17 | 18 | // Show info on first load 19 | InvokePlugin(loader); 20 | 21 | await Task.Delay(-1, cts.Token); 22 | 23 | static void ShowPluginInfo(object sender, PluginReloadedEventArgs eventArgs) 24 | { 25 | Console.ForegroundColor = ConsoleColor.Blue; 26 | Console.Write("HotReloadApp: "); 27 | Console.ResetColor(); 28 | Console.WriteLine("plugin was reloaded"); 29 | InvokePlugin(eventArgs.Loader); 30 | } 31 | 32 | static void InvokePlugin(PluginLoader loader) 33 | { 34 | var assembly = loader.LoadDefaultAssembly(); 35 | assembly 36 | .GetType("TimestampedPlugin.InfoDisplayer", throwOnError: true) 37 | !.GetMethod("Print") 38 | !.Invoke(null, null); 39 | } 40 | -------------------------------------------------------------------------------- /samples/hot-reload/README.md: -------------------------------------------------------------------------------- 1 | # Hot Reload Sample 2 | 3 | This is a minimal sample of how to take advantage of hot reloading support. 4 | 5 | To run this sample, execute the `run.sh` script. This will: 6 | 7 | * Compile the projects once 8 | * Start the HotReloadApp console application 9 | * This creates a single loader with hot reloading enabled 10 | * It subscribes to the `PluginLoader.Reloaded` event to be notified when a new version of the assemblies 11 | are availabled. 12 | * It invokes the new version of the assembly 13 | * Rebuilds the TimestampedPlugin every 5 seconds (until you press CTRL+C to exit) 14 | -------------------------------------------------------------------------------- /samples/hot-reload/TimestampedPlugin/InfoDisplayer.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Linq; 6 | using System.Reflection; 7 | using Microsoft.Data.Sqlite; 8 | 9 | namespace TimestampedPlugin 10 | { 11 | public class InfoDisplayer 12 | { 13 | public static void Print() 14 | { 15 | // Use something from Microsoft.Data.Sqlite to trigger loading of native dependency 16 | var connectionString = new SqliteConnectionStringBuilder 17 | { 18 | DataSource = "HELLO" 19 | }; 20 | 21 | var compileTimestamp = typeof(InfoDisplayer) 22 | .Assembly 23 | .GetCustomAttributes() 24 | .First(a => a.Key == "CompileTimestamp") 25 | .Value; 26 | Console.ForegroundColor = ConsoleColor.Green; 27 | Console.Write("TimestampedPlugin: "); 28 | Console.ResetColor(); 29 | Console.WriteLine($"this plugin was compiled at {compileTimestamp}. {connectionString.DataSource}!"); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /samples/hot-reload/TimestampedPlugin/TimestampedPlugin.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | 6 | 7 | 8 | 9 | <_Parameter1>CompileTimestamp 10 | <_Parameter2>$([System.DateTime]::Now.ToString('h:mm:ss tt')) 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /samples/hot-reload/hot-reload.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29209.152 5 | MinimumVisualStudioVersion = 15.0.26124.0 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotReloadApp", "HotReloadApp\HotReloadApp.csproj", "{421F2562-390A-45C3-B028-32A0027AD8BD}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TimestampedPlugin", "TimestampedPlugin\TimestampedPlugin.csproj", "{92DCE5FD-1A77-4A77-AE04-2820004A446B}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Debug|x64 = Debug|x64 14 | Debug|x86 = Debug|x86 15 | Release|Any CPU = Release|Any CPU 16 | Release|x64 = Release|x64 17 | Release|x86 = Release|x86 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 23 | {421F2562-390A-45C3-B028-32A0027AD8BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {421F2562-390A-45C3-B028-32A0027AD8BD}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {421F2562-390A-45C3-B028-32A0027AD8BD}.Debug|x64.ActiveCfg = Debug|Any CPU 26 | {421F2562-390A-45C3-B028-32A0027AD8BD}.Debug|x64.Build.0 = Debug|Any CPU 27 | {421F2562-390A-45C3-B028-32A0027AD8BD}.Debug|x86.ActiveCfg = Debug|Any CPU 28 | {421F2562-390A-45C3-B028-32A0027AD8BD}.Debug|x86.Build.0 = Debug|Any CPU 29 | {421F2562-390A-45C3-B028-32A0027AD8BD}.Release|Any CPU.ActiveCfg = Release|Any CPU 30 | {421F2562-390A-45C3-B028-32A0027AD8BD}.Release|Any CPU.Build.0 = Release|Any CPU 31 | {421F2562-390A-45C3-B028-32A0027AD8BD}.Release|x64.ActiveCfg = Release|Any CPU 32 | {421F2562-390A-45C3-B028-32A0027AD8BD}.Release|x64.Build.0 = Release|Any CPU 33 | {421F2562-390A-45C3-B028-32A0027AD8BD}.Release|x86.ActiveCfg = Release|Any CPU 34 | {421F2562-390A-45C3-B028-32A0027AD8BD}.Release|x86.Build.0 = Release|Any CPU 35 | {92DCE5FD-1A77-4A77-AE04-2820004A446B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 36 | {92DCE5FD-1A77-4A77-AE04-2820004A446B}.Debug|Any CPU.Build.0 = Debug|Any CPU 37 | {92DCE5FD-1A77-4A77-AE04-2820004A446B}.Debug|x64.ActiveCfg = Debug|Any CPU 38 | {92DCE5FD-1A77-4A77-AE04-2820004A446B}.Debug|x64.Build.0 = Debug|Any CPU 39 | {92DCE5FD-1A77-4A77-AE04-2820004A446B}.Debug|x86.ActiveCfg = Debug|Any CPU 40 | {92DCE5FD-1A77-4A77-AE04-2820004A446B}.Debug|x86.Build.0 = Debug|Any CPU 41 | {92DCE5FD-1A77-4A77-AE04-2820004A446B}.Release|Any CPU.ActiveCfg = Release|Any CPU 42 | {92DCE5FD-1A77-4A77-AE04-2820004A446B}.Release|Any CPU.Build.0 = Release|Any CPU 43 | {92DCE5FD-1A77-4A77-AE04-2820004A446B}.Release|x64.ActiveCfg = Release|Any CPU 44 | {92DCE5FD-1A77-4A77-AE04-2820004A446B}.Release|x64.Build.0 = Release|Any CPU 45 | {92DCE5FD-1A77-4A77-AE04-2820004A446B}.Release|x86.ActiveCfg = Release|Any CPU 46 | {92DCE5FD-1A77-4A77-AE04-2820004A446B}.Release|x86.Build.0 = Release|Any CPU 47 | EndGlobalSection 48 | EndGlobal 49 | -------------------------------------------------------------------------------- /samples/hot-reload/run.ps1: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env pwsh 2 | 3 | $ErrorActionPreference = 'Stop' 4 | 5 | Push-Location $PSScriptRoot 6 | try { 7 | $publish_dir = "$PSScriptRoot/bin/plugins/TimestampedPlugin/" 8 | 9 | function log { 10 | Write-Host -NoNewline -ForegroundColor Yellow "run.ps1: " 11 | Write-Host $args 12 | } 13 | 14 | function publish { 15 | Write-Host "" 16 | dotnet publish --no-restore TimestampedPlugin/ -o $publish_dir -nologo 17 | Write-Host "" 18 | } 19 | 20 | log "Compiling apps" 21 | 22 | & dotnet build HotReloadApp -nologo -clp:NoSummary 23 | & publish 24 | 25 | log "Use CTRL+C to exit" 26 | 27 | $bg_args = @("run", "--no-build", "--project", "HotReloadApp", "$publish_dir/TimestampedPlugin.dll") 28 | $host_process = Start-Process -NoNewWindow -FilePath dotnet -ArgumentList $bg_args 29 | try { 30 | while ($true) { 31 | Start-Sleep 5 32 | log "Rebuilding plugin..." 33 | publish 34 | } 35 | } 36 | finally { 37 | $host_process.Kill() 38 | } 39 | } 40 | finally { 41 | Pop-Location 42 | } 43 | -------------------------------------------------------------------------------- /samples/hot-reload/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | RESET="\033[0m" 6 | YELLOW="\033[0;33m" 7 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 8 | pushd $DIR >/dev/null 9 | 10 | publish_dir="$DIR/bin/plugins/TimestampedPlugin/" 11 | 12 | publish() { 13 | echo "" 14 | dotnet publish --no-restore TimestampedPlugin/ -o $publish_dir -nologo 15 | echo "" 16 | } 17 | 18 | echo -e "${YELLOW}run.sh:${RESET} Compiling apps" 19 | dotnet build HotReloadApp -nologo -clp:NoSummary 20 | publish 21 | 22 | trap "kill 0" EXIT 23 | 24 | echo -e "${YELLOW}run.sh:${RESET} Use CTRL+C to exit" 25 | 26 | dotnet run --no-build --project HotReloadApp -- "$publish_dir/TimestampedPlugin.dll" & 27 | 28 | while true 29 | do 30 | sleep 5 31 | echo -e "${YELLOW}run.sh:${RESET} Rebuilding plugin..." 32 | publish 33 | done 34 | -------------------------------------------------------------------------------- /src/Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/Plugins.Mvc/McMaster.NETCore.Plugins.Mvc.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | library 6 | true 7 | true 8 | Provides API for dynamically loading MVC controllers into an ASP.NET Core web application. 9 | 10 | This package should be used by the host application which needs to load plugins. 11 | See https://github.com/natemcmaster/DotNetCorePlugins/blob/main/README.md for more samples and documentation. 12 | 13 | .NET Core;plugins;aspnetcore 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/Plugins.Mvc/MvcPluginExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System.Reflection; 5 | using McMaster.NETCore.Plugins; 6 | using Microsoft.AspNetCore.Mvc.ApplicationParts; 7 | 8 | namespace Microsoft.Extensions.DependencyInjection 9 | { 10 | /// 11 | /// Extends the MVC builder. 12 | /// 13 | public static class MvcPluginExtensions 14 | { 15 | /// 16 | /// Loads controllers and razor pages from a plugin assembly. 17 | /// 18 | /// This creates a loader with set to true. 19 | /// If you need more control over shared types, use instead. 20 | /// 21 | /// 22 | /// The MVC builder 23 | /// Full path the main .dll file for the plugin. 24 | /// The builder 25 | public static IMvcBuilder AddPluginFromAssemblyFile(this IMvcBuilder mvcBuilder, string assemblyFile) 26 | { 27 | var plugin = PluginLoader.CreateFromAssemblyFile( 28 | assemblyFile, // create a plugin from for the .dll file 29 | config => 30 | // this ensures that the version of MVC is shared between this app and the plugin 31 | config.PreferSharedTypes = true); 32 | 33 | return mvcBuilder.AddPluginLoader(plugin); 34 | } 35 | 36 | /// 37 | /// Loads controllers and razor pages from a plugin loader. 38 | /// 39 | /// In order for this to work, the PluginLoader instance must be configured to share the types 40 | /// and 41 | /// (comes from Microsoft.AspNetCore.Mvc.Core.dll). The easiest way to ensure that is done correctly 42 | /// is to set to true. 43 | /// 44 | /// 45 | /// The MVC builder 46 | /// An instance of PluginLoader. 47 | /// The builder 48 | public static IMvcBuilder AddPluginLoader(this IMvcBuilder mvcBuilder, PluginLoader pluginLoader) 49 | { 50 | var pluginAssembly = pluginLoader.LoadDefaultAssembly(); 51 | 52 | // This loads MVC application parts from plugin assemblies 53 | var partFactory = ApplicationPartFactory.GetApplicationPartFactory(pluginAssembly); 54 | foreach (var part in partFactory.GetApplicationParts(pluginAssembly)) 55 | { 56 | mvcBuilder.PartManager.ApplicationParts.Add(part); 57 | } 58 | 59 | // This piece finds and loads related parts, such as MvcAppPlugin1.Views.dll. 60 | var relatedAssembliesAttrs = pluginAssembly.GetCustomAttributes(); 61 | foreach (var attr in relatedAssembliesAttrs) 62 | { 63 | var assembly = pluginLoader.LoadAssembly(attr.AssemblyFileName); 64 | partFactory = ApplicationPartFactory.GetApplicationPartFactory(assembly); 65 | foreach (var part in partFactory.GetApplicationParts(assembly)) 66 | { 67 | mvcBuilder.PartManager.ApplicationParts.Add(part); 68 | } 69 | } 70 | 71 | return mvcBuilder; 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Plugins.Mvc/PublicAPI.Shipped.txt: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | Microsoft.Extensions.DependencyInjection.MvcPluginExtensions 3 | static Microsoft.Extensions.DependencyInjection.MvcPluginExtensions.AddPluginFromAssemblyFile(this Microsoft.Extensions.DependencyInjection.IMvcBuilder! mvcBuilder, string! assemblyFile) -> Microsoft.Extensions.DependencyInjection.IMvcBuilder! 4 | static Microsoft.Extensions.DependencyInjection.MvcPluginExtensions.AddPluginLoader(this Microsoft.Extensions.DependencyInjection.IMvcBuilder! mvcBuilder, McMaster.NETCore.Plugins.PluginLoader! pluginLoader) -> Microsoft.Extensions.DependencyInjection.IMvcBuilder! 5 | -------------------------------------------------------------------------------- /src/Plugins.Mvc/PublicAPI.Unshipped.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/natemcmaster/DotNetCorePlugins/a6094157bfd1ace9d6b13df108c488526cfe850f/src/Plugins.Mvc/PublicAPI.Unshipped.txt -------------------------------------------------------------------------------- /src/Plugins.Mvc/releasenotes.props: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Breaking change: require .NET >= 8.0 5 | 6 | See also https://nuget.org/packages/McMaster.NETCore.Plugins/$(VersionPrefix) for its release notes. 7 | 8 | 9 | No changes to this library, but updated to use 1.1.0 of Microsoft.NETCore.Plugins. 10 | See https://nuget.org/packages/McMaster.NETCore.Plugins/$(VersionPrefix) for its release notes. 11 | 12 | 13 | Changes: 14 | * Add new API: IMvcBuilder.AddPluginLoader(PluginLoader pluginLoader) 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/Plugins/Internal/Debouncer.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace McMaster.NETCore.Plugins.Internal 9 | { 10 | internal class Debouncer : IDisposable 11 | { 12 | private readonly CancellationTokenSource _cts = new(); 13 | private readonly TimeSpan _waitTime; 14 | private int _counter; 15 | 16 | public Debouncer(TimeSpan waitTime) 17 | { 18 | _waitTime = waitTime; 19 | } 20 | 21 | public void Execute(Action action) 22 | { 23 | var current = Interlocked.Increment(ref _counter); 24 | 25 | Task.Delay(_waitTime).ContinueWith(task => 26 | { 27 | // Is this the last task that was queued? 28 | if (current == _counter && !_cts.IsCancellationRequested) 29 | { 30 | action(); 31 | } 32 | 33 | task.Dispose(); 34 | }, _cts.Token); 35 | } 36 | 37 | public void Dispose() 38 | { 39 | _cts.Cancel(); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Plugins/Internal/PlatformInformation.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Diagnostics; 6 | using System.Runtime.InteropServices; 7 | 8 | namespace McMaster.NETCore.Plugins 9 | { 10 | internal class PlatformInformation 11 | { 12 | public static readonly string[] NativeLibraryExtensions; 13 | public static readonly string[] NativeLibraryPrefixes; 14 | public static readonly string[] ManagedAssemblyExtensions = new[] 15 | { 16 | ".dll", 17 | ".ni.dll", 18 | ".exe", 19 | ".ni.exe" 20 | }; 21 | 22 | static PlatformInformation() 23 | { 24 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 25 | { 26 | NativeLibraryPrefixes = new[] { "" }; 27 | NativeLibraryExtensions = new[] { ".dll" }; 28 | } 29 | else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) 30 | { 31 | NativeLibraryPrefixes = new[] { "", "lib", }; 32 | NativeLibraryExtensions = new[] { ".dylib" }; 33 | } 34 | else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) 35 | { 36 | NativeLibraryPrefixes = new[] { "", "lib" }; 37 | NativeLibraryExtensions = new[] { ".so", ".so.1" }; 38 | } 39 | else 40 | { 41 | Debug.Fail("Unknown OS type"); 42 | NativeLibraryPrefixes = Array.Empty(); 43 | NativeLibraryExtensions = Array.Empty(); 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Plugins/Internal/RuntimeConfig.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | 6 | namespace McMaster.NETCore.Plugins 7 | { 8 | internal class RuntimeConfig 9 | { 10 | public RuntimeOptions? RuntimeOptions { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Plugins/Internal/RuntimeOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | namespace McMaster.NETCore.Plugins 5 | { 6 | internal class RuntimeOptions 7 | { 8 | public string? Tfm { get; set; } 9 | 10 | public string[]? AdditionalProbingPaths { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Plugins/LibraryModel/ManagedLibrary.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Diagnostics; 6 | using System.IO; 7 | using System.Reflection; 8 | 9 | namespace McMaster.NETCore.Plugins.LibraryModel 10 | { 11 | /// 12 | /// Represents a managed, .NET assembly. 13 | /// 14 | [DebuggerDisplay("{Name} = {AdditionalProbingPath}")] 15 | public class ManagedLibrary 16 | { 17 | private ManagedLibrary(AssemblyName name, string additionalProbingPath, string appLocalPath) 18 | { 19 | Name = name ?? throw new ArgumentNullException(nameof(name)); 20 | AdditionalProbingPath = additionalProbingPath ?? throw new ArgumentNullException(nameof(additionalProbingPath)); 21 | AppLocalPath = appLocalPath ?? throw new ArgumentNullException(nameof(appLocalPath)); 22 | } 23 | 24 | /// 25 | /// Name of the managed library 26 | /// 27 | public AssemblyName Name { get; private set; } 28 | 29 | /// 30 | /// Contains path to file within an additional probing path root. This is typically a combination 31 | /// of the NuGet package ID (lowercased), version, and path within the package. 32 | /// 33 | /// For example, microsoft.data.sqlite/1.0.0/lib/netstandard1.3/Microsoft.Data.Sqlite.dll 34 | /// 35 | /// 36 | public string AdditionalProbingPath { get; private set; } 37 | 38 | /// 39 | /// Contains path to file within a deployed, framework-dependent application. 40 | /// 41 | /// For most managed libraries, this will be the file name. 42 | /// For example, MyPlugin1.dll. 43 | /// 44 | /// 45 | /// For runtime-specific managed implementations, this may include a sub folder path. 46 | /// For example, runtimes/win/lib/netcoreapp2.0/System.Diagnostics.EventLog.dll 47 | /// 48 | /// 49 | public string AppLocalPath { get; private set; } 50 | 51 | /// 52 | /// Create an instance of from a NuGet package. 53 | /// 54 | /// The name of the package. 55 | /// The version of the package. 56 | /// The path within the NuGet package. 57 | /// 58 | public static ManagedLibrary CreateFromPackage(string packageId, string packageVersion, string assetPath) 59 | { 60 | // When the asset comes from "lib/$tfm/", Microsoft.NET.Sdk will flatten this during publish based on the most compatible TFM. 61 | // The SDK will not flatten managed libraries found under runtimes/ 62 | var appLocalPath = assetPath.StartsWith("lib/") 63 | ? Path.GetFileName(assetPath) 64 | : assetPath; 65 | 66 | return new ManagedLibrary( 67 | new AssemblyName(Path.GetFileNameWithoutExtension(assetPath)), 68 | Path.Combine(packageId.ToLowerInvariant(), packageVersion, assetPath), 69 | appLocalPath 70 | ); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Plugins/LibraryModel/NativeLibrary.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Diagnostics; 6 | using System.IO; 7 | 8 | namespace McMaster.NETCore.Plugins.LibraryModel 9 | { 10 | /// 11 | /// Represents an unmanaged library, such as `libsqlite3`, which may need to be loaded 12 | /// for P/Invoke to work. 13 | /// 14 | [DebuggerDisplay("{Name} = {AdditionalProbingPath}")] 15 | public class NativeLibrary 16 | { 17 | private NativeLibrary(string name, string appLocalPath, string additionalProbingPath) 18 | { 19 | Name = name ?? throw new ArgumentNullException(nameof(name)); 20 | AppLocalPath = appLocalPath ?? throw new ArgumentNullException(nameof(appLocalPath)); 21 | AdditionalProbingPath = additionalProbingPath ?? throw new ArgumentNullException(nameof(additionalProbingPath)); 22 | } 23 | 24 | /// 25 | /// Name of the native library. This should match the name of the P/Invoke call. 26 | /// 27 | /// For example, if specifying `[DllImport("sqlite3")]`, should be sqlite3. 28 | /// This may not match the exact file name as loading will attempt variations on the name according 29 | /// to OS convention. On Windows, P/Invoke will attempt to load `sqlite3.dll`. On macOS, it will 30 | /// attempt to find `sqlite3.dylib` and `libsqlite3.dylib`. On Linux, it will attempt to find 31 | /// `sqlite3.so` and `libsqlite3.so`. 32 | /// 33 | /// 34 | public string Name { get; private set; } 35 | 36 | /// 37 | /// Contains path to file within a deployed, framework-dependent application 38 | /// 39 | /// For example, runtimes/linux-x64/native/libsqlite.so 40 | /// 41 | /// 42 | public string AppLocalPath { get; private set; } 43 | 44 | /// 45 | /// Contains path to file within an additional probing path root. This is typically a combination 46 | /// of the NuGet package ID (lowercased), version, and path within the package. 47 | /// 48 | /// For example, sqlite/3.13.3/runtimes/linux-x64/native/libsqlite.so 49 | /// 50 | /// 51 | public string AdditionalProbingPath { get; private set; } 52 | 53 | /// 54 | /// Create an instance of from a NuGet package. 55 | /// 56 | /// The name of the package. 57 | /// The version of the package. 58 | /// The path within the NuGet package. 59 | /// 60 | public static NativeLibrary CreateFromPackage(string packageId, string packageVersion, string assetPath) 61 | { 62 | return new NativeLibrary( 63 | Path.GetFileNameWithoutExtension(assetPath), 64 | assetPath, 65 | Path.Combine(packageId.ToLowerInvariant(), packageVersion, assetPath) 66 | ); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Plugins/Loader/RuntimeConfigExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Diagnostics; 6 | using System.IO; 7 | using System.Runtime.InteropServices; 8 | using System.Text.Json; 9 | 10 | namespace McMaster.NETCore.Plugins.Loader 11 | { 12 | /// 13 | /// Extensions for creating a load context using settings from a runtimeconfig.json file 14 | /// 15 | public static class RuntimeConfigExtensions 16 | { 17 | private const string JsonExt = ".json"; 18 | private static readonly JsonSerializerOptions s_serializerOptions = new() 19 | { 20 | PropertyNamingPolicy = JsonNamingPolicy.CamelCase 21 | }; 22 | 23 | /// 24 | /// Adds additional probing paths to a managed load context using settings found in the runtimeconfig.json 25 | /// and runtimeconfig.dev.json files. 26 | /// 27 | /// The context builder 28 | /// The path to the runtimeconfig.json file 29 | /// Also read runtimeconfig.dev.json file, if present. 30 | /// The error, if one occurs while parsing runtimeconfig.json 31 | /// The builder. 32 | public static AssemblyLoadContextBuilder TryAddAdditionalProbingPathFromRuntimeConfig( 33 | this AssemblyLoadContextBuilder builder, 34 | string runtimeConfigPath, 35 | bool includeDevConfig, 36 | out Exception? error) 37 | { 38 | error = null; 39 | try 40 | { 41 | var config = TryReadConfig(runtimeConfigPath); 42 | if (config == null) 43 | { 44 | return builder; 45 | } 46 | 47 | RuntimeConfig? devConfig = null; 48 | if (includeDevConfig) 49 | { 50 | var configDevPath = runtimeConfigPath.Substring(0, runtimeConfigPath.Length - JsonExt.Length) + ".dev.json"; 51 | devConfig = TryReadConfig(configDevPath); 52 | } 53 | 54 | var tfm = config.RuntimeOptions?.Tfm ?? devConfig?.RuntimeOptions?.Tfm; 55 | 56 | if (config.RuntimeOptions != null) 57 | { 58 | AddProbingPaths(builder, config.RuntimeOptions, tfm); 59 | } 60 | 61 | if (devConfig?.RuntimeOptions != null) 62 | { 63 | AddProbingPaths(builder, devConfig.RuntimeOptions, tfm); 64 | } 65 | 66 | if (tfm != null) 67 | { 68 | var dotnet = Process.GetCurrentProcess().MainModule?.FileName; 69 | if (dotnet != null && string.Equals(Path.GetFileNameWithoutExtension(dotnet), "dotnet", StringComparison.OrdinalIgnoreCase)) 70 | { 71 | var dotnetHome = Path.GetDirectoryName(dotnet); 72 | if (dotnetHome != null) 73 | { 74 | builder.AddProbingPath(Path.Combine(dotnetHome, "store", RuntimeInformation.OSArchitecture.ToString().ToLowerInvariant(), tfm)); 75 | } 76 | } 77 | } 78 | } 79 | catch (Exception ex) 80 | { 81 | error = ex; 82 | } 83 | return builder; 84 | } 85 | 86 | private static void AddProbingPaths(AssemblyLoadContextBuilder builder, RuntimeOptions options, string? tfm) 87 | { 88 | if (options.AdditionalProbingPaths == null) 89 | { 90 | return; 91 | } 92 | 93 | foreach (var item in options.AdditionalProbingPaths) 94 | { 95 | var path = item; 96 | if (path.Contains("|arch|")) 97 | { 98 | path = path.Replace("|arch|", RuntimeInformation.OSArchitecture.ToString().ToLowerInvariant()); 99 | } 100 | 101 | if (path.Contains("|tfm|")) 102 | { 103 | if (tfm == null) 104 | { 105 | // We don't have enough information to parse this 106 | continue; 107 | } 108 | 109 | path = path.Replace("|tfm|", tfm); 110 | } 111 | 112 | builder.AddProbingPath(path); 113 | } 114 | } 115 | 116 | private static RuntimeConfig? TryReadConfig(string path) 117 | { 118 | try 119 | { 120 | var file = File.ReadAllBytes(path); 121 | return JsonSerializer.Deserialize(file, s_serializerOptions); 122 | } 123 | catch 124 | { 125 | return null; 126 | } 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/Plugins/McMaster.NETCore.Plugins.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | library 6 | true 7 | true 8 | Provides API for dynamically loading assemblies into a .NET application. 9 | 10 | This package should be used by the host application which needs to load plugins. 11 | See https://github.com/natemcmaster/DotNetCorePlugins/blob/main/README.md for more samples and documentation. 12 | 13 | .NET Core;plugins 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Plugins/PluginConfig.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Reflection; 8 | using System.Runtime.Loader; 9 | 10 | namespace McMaster.NETCore.Plugins 11 | { 12 | /// 13 | /// Represents the configuration for a .NET Core plugin. 14 | /// 15 | public class PluginConfig 16 | { 17 | /// 18 | /// Initializes a new instance of 19 | /// 20 | /// The full file path to the main assembly for the plugin. 21 | public PluginConfig(string mainAssemblyPath) 22 | { 23 | if (string.IsNullOrEmpty(mainAssemblyPath)) 24 | { 25 | throw new ArgumentException("Value must be null or not empty", nameof(mainAssemblyPath)); 26 | } 27 | 28 | if (!Path.IsPathRooted(mainAssemblyPath)) 29 | { 30 | throw new ArgumentException("Value must be an absolute file path", nameof(mainAssemblyPath)); 31 | } 32 | 33 | MainAssemblyPath = mainAssemblyPath; 34 | } 35 | 36 | /// 37 | /// The file path to the main assembly. 38 | /// 39 | public string MainAssemblyPath { get; } 40 | 41 | /// 42 | /// A list of assemblies which should be treated as private. 43 | /// 44 | public ICollection PrivateAssemblies { get; protected set; } = new List(); 45 | 46 | /// 47 | /// A list of assemblies which should be unified between the host and the plugin. 48 | /// 49 | /// 50 | /// https://github.com/natemcmaster/DotNetCorePlugins/blob/main/docs/what-are-shared-types.md 51 | /// 52 | public ICollection SharedAssemblies { get; protected set; } = new List(); 53 | 54 | /// 55 | /// Attempt to unify all types from a plugin with the host. 56 | /// 57 | /// This does not guarantee types will unify. 58 | /// 59 | /// 60 | /// https://github.com/natemcmaster/DotNetCorePlugins/blob/main/docs/what-are-shared-types.md 61 | /// 62 | /// 63 | public bool PreferSharedTypes { get; set; } 64 | 65 | /// 66 | /// If enabled, will lazy load dependencies of all shared assemblies. 67 | /// Reduces plugin load time at the expense of non-determinism in how transitive dependencies are loaded 68 | /// between the plugin and the host. 69 | /// 70 | /// Please be aware of the danger of using this option: 71 | /// 72 | /// https://github.com/natemcmaster/DotNetCorePlugins/pull/164#issuecomment-751557873 73 | /// 74 | /// 75 | public bool IsLazyLoaded { get; set; } = false; 76 | 77 | /// 78 | /// If set, replaces the default used by the . 79 | /// Use this feature if the of the is not the Runtime's default load context. 80 | /// i.e. (AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly) != 81 | /// 82 | public AssemblyLoadContext DefaultContext { get; set; } = AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly()) ?? AssemblyLoadContext.Default; 83 | 84 | private bool _isUnloadable; 85 | 86 | /// 87 | /// The plugin can be unloaded from memory. 88 | /// 89 | public bool IsUnloadable 90 | { 91 | get => _isUnloadable || EnableHotReload; 92 | set => _isUnloadable = value; 93 | } 94 | 95 | private bool _loadInMemory; 96 | 97 | /// 98 | /// Loads assemblies into memory in order to not lock files. 99 | /// As example use case here would be: no hot reloading but able to 100 | /// replace files and reload manually at later time 101 | /// 102 | public bool LoadInMemory 103 | { 104 | get => _loadInMemory || EnableHotReload; 105 | set => _loadInMemory = value; 106 | } 107 | 108 | /// 109 | /// When any of the loaded files changes on disk, the plugin will be reloaded. 110 | /// Use the event to be notified of changes. 111 | /// 112 | /// 113 | /// It will load assemblies into memory in order to not lock files 114 | /// 115 | /// 116 | public bool EnableHotReload { get; set; } 117 | 118 | /// 119 | /// Specifies the delay to reload a plugin, after file changes have been detected. 120 | /// Default value is 200 milliseconds. 121 | /// 122 | public TimeSpan ReloadDelay { get; set; } = TimeSpan.FromMilliseconds(200); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/Plugins/PluginReloadedEventHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | 6 | namespace McMaster.NETCore.Plugins 7 | { 8 | /// 9 | /// Represents the method that will handle the event. 10 | /// 11 | /// The object sending the event 12 | /// Data about the event. 13 | public delegate void PluginReloadedEventHandler(object sender, PluginReloadedEventArgs eventArgs); 14 | 15 | /// 16 | /// Provides data for the event. 17 | /// 18 | public class PluginReloadedEventArgs : EventArgs 19 | { 20 | /// 21 | /// Initializes . 22 | /// 23 | /// 24 | public PluginReloadedEventArgs(PluginLoader loader) 25 | { 26 | Loader = loader; 27 | } 28 | 29 | /// 30 | /// The plugin loader 31 | /// 32 | public PluginLoader Loader { get; } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Plugins/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System.Runtime.CompilerServices; 5 | 6 | [assembly: InternalsVisibleTo("McMaster.NETCore.Plugins.Tests, PublicKey=00240000048000009400000006020000002400005253413100040000010001001df0eba4297c8ffdf114a13714ad787744619dfb18e29191703f6f782d6a09e4a4cac35b8c768cbbd9ade8197bc0f66ec66fabc9071a206c8060af8b7a332236968d3ee44b90bd2f30d0edcb6150555c6f8d988e48234debaf2d427a08d7c06ba1343411142dc8ac996f7f7dbe0e93d13f17a7624db5400510e6144b0fd683b9")] 7 | -------------------------------------------------------------------------------- /src/Plugins/PublicAPI.Shipped.txt: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | McMaster.NETCore.Plugins.LibraryModel.ManagedLibrary 3 | McMaster.NETCore.Plugins.LibraryModel.ManagedLibrary.AdditionalProbingPath.get -> string! 4 | McMaster.NETCore.Plugins.LibraryModel.ManagedLibrary.AppLocalPath.get -> string! 5 | McMaster.NETCore.Plugins.LibraryModel.ManagedLibrary.Name.get -> System.Reflection.AssemblyName! 6 | McMaster.NETCore.Plugins.LibraryModel.NativeLibrary 7 | McMaster.NETCore.Plugins.LibraryModel.NativeLibrary.AdditionalProbingPath.get -> string! 8 | McMaster.NETCore.Plugins.LibraryModel.NativeLibrary.AppLocalPath.get -> string! 9 | McMaster.NETCore.Plugins.LibraryModel.NativeLibrary.Name.get -> string! 10 | McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder 11 | McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder.AddManagedLibrary(McMaster.NETCore.Plugins.LibraryModel.ManagedLibrary! library) -> McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder! 12 | McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder.AddNativeLibrary(McMaster.NETCore.Plugins.LibraryModel.NativeLibrary! library) -> McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder! 13 | McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder.AddProbingPath(string! path) -> McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder! 14 | McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder.AddResourceProbingPath(string! path) -> McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder! 15 | McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder.AssemblyLoadContextBuilder() -> void 16 | McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder.Build() -> System.Runtime.Loader.AssemblyLoadContext! 17 | McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder.EnableUnloading() -> McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder! 18 | McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder.IsLazyLoaded(bool isLazyLoaded) -> McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder! 19 | McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder.PreferDefaultLoadContext(bool preferDefaultLoadContext) -> McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder! 20 | McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder.PreferDefaultLoadContextAssembly(System.Reflection.AssemblyName! assemblyName) -> McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder! 21 | McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder.PreferLoadContextAssembly(System.Reflection.AssemblyName! assemblyName) -> McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder! 22 | McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder.PreloadAssembliesIntoMemory() -> McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder! 23 | McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder.SetDefaultContext(System.Runtime.Loader.AssemblyLoadContext! context) -> McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder! 24 | McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder.SetMainAssemblyPath(string! path) -> McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder! 25 | McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder.ShadowCopyNativeLibraries() -> McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder! 26 | McMaster.NETCore.Plugins.Loader.RuntimeConfigExtensions 27 | McMaster.NETCore.Plugins.PluginConfig 28 | McMaster.NETCore.Plugins.PluginConfig.DefaultContext.get -> System.Runtime.Loader.AssemblyLoadContext! 29 | McMaster.NETCore.Plugins.PluginConfig.DefaultContext.set -> void 30 | McMaster.NETCore.Plugins.PluginConfig.EnableHotReload.get -> bool 31 | McMaster.NETCore.Plugins.PluginConfig.EnableHotReload.set -> void 32 | McMaster.NETCore.Plugins.PluginConfig.IsLazyLoaded.get -> bool 33 | McMaster.NETCore.Plugins.PluginConfig.IsLazyLoaded.set -> void 34 | McMaster.NETCore.Plugins.PluginConfig.IsUnloadable.get -> bool 35 | McMaster.NETCore.Plugins.PluginConfig.IsUnloadable.set -> void 36 | McMaster.NETCore.Plugins.PluginConfig.LoadInMemory.get -> bool 37 | McMaster.NETCore.Plugins.PluginConfig.LoadInMemory.set -> void 38 | McMaster.NETCore.Plugins.PluginConfig.MainAssemblyPath.get -> string! 39 | McMaster.NETCore.Plugins.PluginConfig.PluginConfig(string! mainAssemblyPath) -> void 40 | McMaster.NETCore.Plugins.PluginConfig.PreferSharedTypes.get -> bool 41 | McMaster.NETCore.Plugins.PluginConfig.PreferSharedTypes.set -> void 42 | McMaster.NETCore.Plugins.PluginConfig.PrivateAssemblies.get -> System.Collections.Generic.ICollection! 43 | McMaster.NETCore.Plugins.PluginConfig.PrivateAssemblies.set -> void 44 | McMaster.NETCore.Plugins.PluginConfig.ReloadDelay.get -> System.TimeSpan 45 | McMaster.NETCore.Plugins.PluginConfig.ReloadDelay.set -> void 46 | McMaster.NETCore.Plugins.PluginConfig.SharedAssemblies.get -> System.Collections.Generic.ICollection! 47 | McMaster.NETCore.Plugins.PluginConfig.SharedAssemblies.set -> void 48 | McMaster.NETCore.Plugins.PluginLoader 49 | McMaster.NETCore.Plugins.PluginLoader.Dispose() -> void 50 | McMaster.NETCore.Plugins.PluginLoader.EnterContextualReflection() -> System.Runtime.Loader.AssemblyLoadContext.ContextualReflectionScope 51 | McMaster.NETCore.Plugins.PluginLoader.IsUnloadable.get -> bool 52 | McMaster.NETCore.Plugins.PluginLoader.LoadAssembly(string! assemblyName) -> System.Reflection.Assembly! 53 | McMaster.NETCore.Plugins.PluginLoader.LoadAssembly(System.Reflection.AssemblyName! assemblyName) -> System.Reflection.Assembly! 54 | McMaster.NETCore.Plugins.PluginLoader.LoadAssemblyFromPath(string! assemblyPath) -> System.Reflection.Assembly! 55 | McMaster.NETCore.Plugins.PluginLoader.LoadContext.get -> System.Runtime.Loader.AssemblyLoadContext! 56 | McMaster.NETCore.Plugins.PluginLoader.LoadDefaultAssembly() -> System.Reflection.Assembly! 57 | McMaster.NETCore.Plugins.PluginLoader.PluginLoader(McMaster.NETCore.Plugins.PluginConfig! config) -> void 58 | McMaster.NETCore.Plugins.PluginLoader.Reload() -> void 59 | McMaster.NETCore.Plugins.PluginLoader.Reloaded -> McMaster.NETCore.Plugins.PluginReloadedEventHandler? 60 | McMaster.NETCore.Plugins.PluginReloadedEventArgs 61 | McMaster.NETCore.Plugins.PluginReloadedEventArgs.Loader.get -> McMaster.NETCore.Plugins.PluginLoader! 62 | McMaster.NETCore.Plugins.PluginReloadedEventArgs.PluginReloadedEventArgs(McMaster.NETCore.Plugins.PluginLoader! loader) -> void 63 | McMaster.NETCore.Plugins.PluginReloadedEventHandler 64 | static McMaster.NETCore.Plugins.LibraryModel.ManagedLibrary.CreateFromPackage(string! packageId, string! packageVersion, string! assetPath) -> McMaster.NETCore.Plugins.LibraryModel.ManagedLibrary! 65 | static McMaster.NETCore.Plugins.LibraryModel.NativeLibrary.CreateFromPackage(string! packageId, string! packageVersion, string! assetPath) -> McMaster.NETCore.Plugins.LibraryModel.NativeLibrary! 66 | static McMaster.NETCore.Plugins.Loader.RuntimeConfigExtensions.TryAddAdditionalProbingPathFromRuntimeConfig(this McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder! builder, string! runtimeConfigPath, bool includeDevConfig, out System.Exception? error) -> McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder! 67 | static McMaster.NETCore.Plugins.PluginLoader.CreateFromAssemblyFile(string! assemblyFile) -> McMaster.NETCore.Plugins.PluginLoader! 68 | static McMaster.NETCore.Plugins.PluginLoader.CreateFromAssemblyFile(string! assemblyFile, bool isUnloadable, System.Type![]! sharedTypes) -> McMaster.NETCore.Plugins.PluginLoader! 69 | static McMaster.NETCore.Plugins.PluginLoader.CreateFromAssemblyFile(string! assemblyFile, bool isUnloadable, System.Type![]! sharedTypes, System.Action! configure) -> McMaster.NETCore.Plugins.PluginLoader! 70 | static McMaster.NETCore.Plugins.PluginLoader.CreateFromAssemblyFile(string! assemblyFile, System.Action! configure) -> McMaster.NETCore.Plugins.PluginLoader! 71 | static McMaster.NETCore.Plugins.PluginLoader.CreateFromAssemblyFile(string! assemblyFile, System.Type![]! sharedTypes) -> McMaster.NETCore.Plugins.PluginLoader! 72 | static McMaster.NETCore.Plugins.PluginLoader.CreateFromAssemblyFile(string! assemblyFile, System.Type![]! sharedTypes, System.Action! configure) -> McMaster.NETCore.Plugins.PluginLoader! 73 | -------------------------------------------------------------------------------- /src/Plugins/PublicAPI.Unshipped.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/natemcmaster/DotNetCorePlugins/a6094157bfd1ace9d6b13df108c488526cfe850f/src/Plugins/PublicAPI.Unshipped.txt -------------------------------------------------------------------------------- /src/Plugins/releasenotes.props: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Breaking changes: 5 | * Require .NET >= 8.0 6 | * Drop dependencies on .NET Core 2 libraries for manually parsing .deps.json. Use the built-in .NET 8 API for this. 7 | 8 | 9 | Changes: 10 | * @Sewer56 - feature: add option to support lazy loading of transitive dependencies to increase performance (PR #164) 11 | * @Sewer56 - bugfix: search in additional probing paths (PR #172) 12 | 13 | 14 | Changes: 15 | * @KatoStoelen - don't create shadow copy that already exists (PR #147) 16 | 17 | 18 | Changes: 19 | * @bergi9 - add 'LoadInMemory' option so you can manually trigger reloads (alternative to HotReload) (PR #133) 20 | 21 | 22 | Changes: 23 | * @thoemmi - Debounce file system events when using hot reload and add API to control the debounce delay (default = 200 ms) (PR #129) 24 | 25 | 26 | Changes: 27 | * Add an API to enable shadow copying native library dependencies (PR #119) 28 | * Fix issue with native libraries being locked on disk during hot reload (Issue #118) 29 | 30 | 31 | Changes: 32 | * Add an API to enable contextual reflection in .NET Core 3+ (see https://github.com/natemcmaster/DotNetCorePlugins/blob/v1.0.0/README.md#Reflection for details) 33 | * @Sewer56 - Add support for non-default AssemblyLoadContext's (useful for plugins which load more plugins and native .NET Core hosting) (#111) 34 | * Remove API that was made obsolete in 0.3.0 35 | 36 | 37 | Fixes: 38 | * Fix issue preventing hot reload from working on Windows (#108) 39 | 40 | 41 | Fixes: 42 | * @CopaDataPM: Support CoreCLR in native apps (#80) 43 | 44 | 45 | Changes: 46 | * .NET Core 3.0 support 47 | * Support unloading plugins from memory (only .NET Core >3.0) 48 | * Support hot reloading (only .NET Core >3.0) 49 | 50 | Fixes: 51 | * Fix errors in loading the transitive dependencies of shared types 52 | 53 | Breaking changes: 54 | * Support for loading plugin config from an XML file has been dropped. The APIs for PluginLoader.CreateFromConfigFile were removed 55 | in favor of PluginLoader.CreateFromAssemblyFile 56 | * Merged PluginLoaderOptions with the PluginConfig class 57 | 58 | 59 | 60 | Bug fix: 61 | * Fix the MSBuild targets which generate plugin.config to put it into the correct output directory. 62 | 63 | 64 | Bug fix: 65 | * Fix config file generation when using the SDK package 66 | 67 | 68 | 74 | 75 | 76 | 82 | 83 | 84 | 89 | 90 | 91 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /src/StrongName.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/natemcmaster/DotNetCorePlugins/a6094157bfd1ace9d6b13df108c488526cfe850f/src/StrongName.snk -------------------------------------------------------------------------------- /src/common.psm1: -------------------------------------------------------------------------------- 1 | function exec([string]$_cmd) { 2 | write-host -ForegroundColor DarkGray ">>> $_cmd $args" 3 | $ErrorActionPreference = 'Continue' 4 | & $_cmd @args 5 | $ErrorActionPreference = 'Stop' 6 | if ($LASTEXITCODE -ne 0) { 7 | write-error "Failed with exit code $LASTEXITCODE" 8 | exit 1 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/Plugins.Tests/BasicAssemblyLoaderTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Reflection; 6 | using System.Runtime.CompilerServices; 7 | using McMaster.Extensions.Xunit; 8 | using Test.Referenced.Library; 9 | using Xunit; 10 | using Xunit.Abstractions; 11 | 12 | namespace McMaster.NETCore.Plugins.Tests 13 | { 14 | public class BasicAssemblyLoaderTests 15 | { 16 | private readonly ITestOutputHelper output; 17 | 18 | public BasicAssemblyLoaderTests(ITestOutputHelper output) 19 | { 20 | this.output = output; 21 | } 22 | 23 | [Fact] 24 | public void PluginLoaderCanUnload() 25 | { 26 | var path = TestResources.GetTestProjectAssembly("NetCoreApp2App"); 27 | 28 | // See https://github.com/dotnet/coreclr/pull/22221 29 | 30 | ExecuteAndUnload(path, out var weakRef); 31 | 32 | // Force a GC collect to ensure unloaded has completed 33 | for (var i = 0; weakRef.IsAlive && (i < 10); i++) 34 | { 35 | GC.Collect(); 36 | GC.WaitForPendingFinalizers(); 37 | } 38 | 39 | Assert.False(weakRef.IsAlive); 40 | } 41 | 42 | [MethodImpl(MethodImplOptions.NoInlining)] // ensure no local vars are create 43 | private void ExecuteAndUnload(string path, out WeakReference weakRef) 44 | { 45 | var loader = PluginLoader.CreateFromAssemblyFile(path, c => { c.IsUnloadable = true; }); 46 | var assembly = loader.LoadDefaultAssembly(); 47 | 48 | var method = assembly 49 | .GetType("NetCoreApp2App.Program", throwOnError: true)! 50 | .GetMethod("GetGreeting", BindingFlags.Static | BindingFlags.Public); 51 | 52 | Assert.True(loader.IsUnloadable); 53 | Assert.NotNull(method); 54 | Assert.Equal("Hello world!", method!.Invoke(null, Array.Empty())); 55 | loader.Dispose(); 56 | Assert.Throws(() => loader.LoadDefaultAssembly()); 57 | 58 | weakRef = new WeakReference(loader.LoadContext, trackResurrection: true); 59 | } 60 | 61 | [Fact] 62 | public void LoadsNetCoreProjectWithNativeDeps() 63 | { 64 | var path = TestResources.GetTestProjectAssembly("PowerShellPlugin"); 65 | var loader = PluginLoader.CreateFromAssemblyFile(path); 66 | var assembly = loader.LoadDefaultAssembly(); 67 | 68 | var method = assembly 69 | .GetType("PowerShellPlugin.Program", throwOnError: true)! 70 | .GetMethod("GetGreeting", BindingFlags.Static | BindingFlags.Public); 71 | Assert.NotNull(method); 72 | Assert.Equal("hello", method!.Invoke(null, Array.Empty())); 73 | } 74 | 75 | [SkippableFact] 76 | [SkipOnOS(OS.Linux | OS.MacOS)] 77 | public void LoadsNativeDependenciesWhenDllImportUsesFilename() 78 | { 79 | // SqlClient has P/invoke that calls "sni.dll" on Windows. This test checks 80 | // that native libraries can still be resolved in this case. 81 | var path = TestResources.GetTestProjectAssembly("SqlClientApp"); 82 | var loader = PluginLoader.CreateFromAssemblyFile(path); 83 | var assembly = loader.LoadDefaultAssembly(); 84 | 85 | var method = assembly 86 | .GetType("SqlClientApp.Program", throwOnError: true)! 87 | .GetMethod("Run", BindingFlags.Static | BindingFlags.Public); 88 | Assert.NotNull(method); 89 | Assert.Equal(true, method!.Invoke(null, Array.Empty())); 90 | } 91 | 92 | [Fact] 93 | public void LoadsNetCoreApp2Project() 94 | { 95 | var path = TestResources.GetTestProjectAssembly("NetCoreApp2App"); 96 | var loader = PluginLoader.CreateFromAssemblyFile(path); 97 | var assembly = loader.LoadDefaultAssembly(); 98 | 99 | var method = assembly 100 | .GetType("NetCoreApp2App.Program", throwOnError: true)! 101 | .GetMethod("GetGreeting", BindingFlags.Static | BindingFlags.Public); 102 | Assert.NotNull(method); 103 | Assert.Equal("Hello world!", method!.Invoke(null, Array.Empty())); 104 | } 105 | 106 | [Fact] 107 | public void LoadsNetStandard20Project() 108 | { 109 | var path = TestResources.GetTestProjectAssembly("NetStandardClassLib"); 110 | var loader = PluginLoader.CreateFromAssemblyFile(path); 111 | var assembly = loader.LoadDefaultAssembly(); 112 | 113 | var type = assembly.GetType("NetStandardClassLib.Class1", throwOnError: true); 114 | var method = type!.GetMethod("GetColor", BindingFlags.Instance | BindingFlags.Public); 115 | Assert.NotNull(method); 116 | Assert.Equal("Red", method!.Invoke(Activator.CreateInstance(type), Array.Empty())); 117 | } 118 | 119 | [Fact] 120 | [UseCulture("es")] 121 | public void ItLoadsSatelliteAssemblies() 122 | { 123 | var fruit = GetPlátano(); 124 | Assert.Equal("Plátano", fruit.GetFlavor()); 125 | } 126 | 127 | [Fact] 128 | [UseCulture("en")] 129 | public void ItLoadsDefaultCultureAssemblies() 130 | { 131 | var fruit = GetPlátano(); 132 | Assert.Equal("Banana", fruit.GetFlavor()); 133 | } 134 | 135 | private IFruit GetPlátano() 136 | { 137 | var path = TestResources.GetTestProjectAssembly("Plátano"); 138 | var loader = PluginLoader.CreateFromAssemblyFile(path, 139 | isUnloadable: true, 140 | sharedTypes: new[] { typeof(IFruit) }); 141 | 142 | var assembly = loader.LoadDefaultAssembly(); 143 | var type = Assert.Single(assembly.GetTypes(), t => typeof(IFruit).IsAssignableFrom(t)); 144 | return (IFruit)Activator.CreateInstance(type)!; 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /test/Plugins.Tests/DebouncerTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Threading.Tasks; 6 | using McMaster.NETCore.Plugins.Internal; 7 | using Xunit; 8 | 9 | namespace McMaster.NETCore.Plugins.Tests 10 | { 11 | public class DebouncerTests 12 | { 13 | [Fact] 14 | public async Task InvocationIsDelayed() 15 | { 16 | var executionCounter = 0; 17 | 18 | var debouncer = new Debouncer(TimeSpan.FromSeconds(.1)); 19 | debouncer.Execute(() => executionCounter++); 20 | 21 | Assert.Equal(0, executionCounter); 22 | 23 | await Task.Delay(TimeSpan.FromSeconds(.5)); 24 | 25 | Assert.Equal(1, executionCounter); 26 | } 27 | 28 | [Fact] 29 | public async Task ActionsAreDebounced() 30 | { 31 | var executionCounter = 0; 32 | 33 | var debouncer = new Debouncer(TimeSpan.FromSeconds(.1)); 34 | debouncer.Execute(() => executionCounter++); 35 | debouncer.Execute(() => executionCounter++); 36 | debouncer.Execute(() => executionCounter++); 37 | 38 | await Task.Delay(TimeSpan.FromSeconds(.5)); 39 | 40 | Assert.Equal(1, executionCounter); 41 | } 42 | 43 | [Fact] 44 | public async Task OnlyLastActionIsInvoked() 45 | { 46 | string? invokedAction = null; 47 | 48 | var debouncer = new Debouncer(TimeSpan.FromSeconds(.1)); 49 | foreach (var action in new[] { "a", "b", "c" }) 50 | { 51 | debouncer.Execute(() => invokedAction = action); 52 | } 53 | 54 | await Task.Delay(TimeSpan.FromSeconds(.5)); 55 | 56 | Assert.NotNull(invokedAction); 57 | Assert.Equal("c", invokedAction); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /test/Plugins.Tests/ManageLoadContextTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.IO; 6 | using System.Reflection; 7 | using System.Runtime.Loader; 8 | using McMaster.NETCore.Plugins.Loader; 9 | using Xunit; 10 | 11 | namespace McMaster.NETCore.Plugins.Tests 12 | { 13 | public class ManagedLoadContextTests 14 | { 15 | [Fact] 16 | public void ItUpgradesTypesInContext() 17 | { 18 | var samplePath = TestResources.GetTestProjectAssembly("XunitSample"); 19 | var context = new AssemblyLoadContextBuilder() 20 | .SetMainAssemblyPath(samplePath) 21 | .AddProbingPath(samplePath) 22 | .PreferDefaultLoadContext(true) 23 | .Build(); 24 | 25 | Assert.Same(typeof(TheoryData).Assembly, LoadAssembly(context, "xunit.core")); 26 | } 27 | 28 | [Fact] 29 | public void ContextsHavePrivateVersionsByDefault() 30 | { 31 | var samplePath = TestResources.GetTestProjectAssembly("XunitSample"); 32 | 33 | var context = new AssemblyLoadContextBuilder() 34 | .SetMainAssemblyPath(samplePath) 35 | .AddProbingPath(samplePath) 36 | .Build(); 37 | 38 | Assert.NotSame(typeof(TheoryData).Assembly, LoadAssembly(context, "xunit.core")); 39 | } 40 | 41 | [Fact] 42 | public void ItCanDowngradeUnifiedTypes() 43 | { 44 | var samplePath = TestResources.GetTestProjectAssembly("NetCoreApp2App"); 45 | 46 | var defaultLoader = new AssemblyLoadContextBuilder() 47 | .SetMainAssemblyPath(samplePath) 48 | .AddProbingPath(samplePath) 49 | .PreferDefaultLoadContext(false) 50 | .Build(); 51 | 52 | var unifedLoader = new AssemblyLoadContextBuilder() 53 | .SetMainAssemblyPath(samplePath) 54 | .AddProbingPath(samplePath) 55 | .PreferDefaultLoadContext(true) 56 | .Build(); 57 | 58 | Assert.Equal(new Version("2.0.0.0"), LoadAssembly(defaultLoader, "Test.Referenced.Library").GetName().Version); 59 | Assert.Equal(new Version("1.0.0.0"), LoadAssembly(unifedLoader, "Test.Referenced.Library").GetName().Version); 60 | } 61 | 62 | private Assembly LoadAssembly(AssemblyLoadContext context, string name) 63 | => context.LoadFromAssemblyName(new AssemblyName(name)); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /test/Plugins.Tests/McMaster.NETCore.Plugins.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | $(DefaultItemExcludes);TestResults\** 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /test/Plugins.Tests/PrivateDependencyTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Reflection; 6 | using Xunit; 7 | 8 | namespace McMaster.NETCore.Plugins.Tests 9 | { 10 | public class PrivateDependencyTests 11 | { 12 | [Fact] 13 | public void EachContextHasPrivateVersions() 14 | { 15 | var lib1context = PluginLoader.CreateFromAssemblyFile(TestResources.GetTestProjectAssembly("PrivateDepv1")); 16 | var lib2context = PluginLoader.CreateFromAssemblyFile(TestResources.GetTestProjectAssembly("PrivateDepv2")); 17 | var lib3context = PluginLoader.CreateFromAssemblyFile(TestResources.GetTestProjectAssembly("PrivateDepv3")); 18 | 19 | // Load newest first to prove we can load older assemblies later into the same process 20 | var lib3 = GetAssembly(lib3context); 21 | var lib2 = GetAssembly(lib2context); 22 | var lib1 = GetAssembly(lib1context); 23 | 24 | Assert.Equal(new Version("1.0.0.0"), lib1.GetName().Version); 25 | Assert.Equal(new Version("2.0.0.0"), lib2.GetName().Version); 26 | Assert.Equal(new Version("3.0.0.0"), lib3.GetName().Version); 27 | 28 | // types from each context have unique identities 29 | Assert.NotEqual( 30 | lib1.GetType("Mylib.Class1", throwOnError: true), 31 | lib2.GetType("Mylib.Class1", throwOnError: true)); 32 | Assert.NotEqual( 33 | lib2.GetType("Mylib.Class1", throwOnError: true), 34 | lib3.GetType("Mylib.Class1", throwOnError: true)); 35 | } 36 | 37 | private Assembly GetAssembly(PluginLoader loader) 38 | => loader.LoadAssembly(new AssemblyName("Mylib")); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/Plugins.Tests/ShadowCopyTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using Xunit; 5 | 6 | namespace McMaster.NETCore.Plugins.Tests 7 | { 8 | public class ShadowCopyTests 9 | { 10 | [Fact] 11 | public void DoesNotThrowWhenLoadingSameNativeDependecyMoreThanOnce() 12 | { 13 | var samplePath = TestResources.GetTestProjectAssembly("NativeDependency"); 14 | 15 | using var loader = PluginLoader 16 | .CreateFromAssemblyFile(samplePath, config => config.EnableHotReload = true); 17 | 18 | var nativeDependencyLoadMethod = loader.LoadDefaultAssembly() 19 | ?.GetType("NativeDependency.NativeDependencyLoader") 20 | ?.GetMethod("Load"); 21 | 22 | var exception = Record.Exception(() => nativeDependencyLoadMethod?.Invoke(null, null)); 23 | 24 | Assert.Null(exception); 25 | } 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /test/Plugins.Tests/SharedTypesTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Reflection; 8 | using System.Runtime.Loader; 9 | using Test.Referenced.Library; 10 | using Test.Shared.Abstraction; 11 | using WithOwnPluginsContract; 12 | using Xunit; 13 | 14 | namespace McMaster.NETCore.Plugins.Tests 15 | { 16 | public class SharedTypesTests 17 | { 18 | [Fact] 19 | public void PluginsCanForceSharedTypes() 20 | { 21 | var pluginsNames = new[] { "Banana", "Strawberry" }; 22 | var loaders = new List(); 23 | foreach (var name in pluginsNames) 24 | { 25 | var loader = PluginLoader.CreateFromAssemblyFile( 26 | TestResources.GetTestProjectAssembly(name), 27 | sharedTypes: new[] { typeof(IFruit) }); 28 | loaders.Add(loader); 29 | } 30 | 31 | foreach (var plugin in loaders.Select(l => l.LoadDefaultAssembly())) 32 | { 33 | var fruitType = Assert.Single(plugin.GetTypes(), t => typeof(IFruit).IsAssignableFrom(t)); 34 | var fruit = (IFruit)Activator.CreateInstance(fruitType)!; 35 | Assert.NotNull(fruit.GetFlavor()); 36 | } 37 | } 38 | 39 | /// 40 | /// This is a carefully crafted example which tests 41 | /// that the assembly dependencies of shared types are 42 | /// accounted for. Without this, the order in which code loads 43 | /// could cause different assembly versions to be loaded. 44 | /// 45 | [Theory] 46 | [InlineData(true)] 47 | [InlineData(false)] 48 | public void TransitiveAssembliesOfSharedTypesAreResolved(bool isLazyLoaded) 49 | { 50 | using var loader = PluginLoader.CreateFromAssemblyFile(TestResources.GetTestProjectAssembly("TransitivePlugin"), sharedTypes: new[] { typeof(SharedType) }, config => config.IsLazyLoaded = isLazyLoaded); 51 | var assembly = loader.LoadDefaultAssembly(); 52 | var configType = assembly.GetType("TransitivePlugin.PluginConfig", throwOnError: true)!; 53 | var config = Activator.CreateInstance(configType); 54 | var transitiveInstance = configType.GetMethod("GetTransitiveType")?.Invoke(config, null); 55 | Assert.IsType(transitiveInstance); 56 | } 57 | 58 | /// 59 | /// This is a carefully crafted example which tests 60 | /// whether the library can be used outside of the default load context 61 | /// (). 62 | /// 63 | /// It works by loading a plugin (that gets loaded into another ALC) 64 | /// which in turn loads its own plugins using the library. If said plugin 65 | /// can successfully share its own types, the test should work. 66 | /// 67 | [Fact] 68 | public void NonDefaultLoadContextsAreSupported() 69 | { 70 | /* The loaded plugin here will be in its own ALC. 71 | * It will load its own plugins, which are not known to this ALC. 72 | * Then this ALC will ask that ALC if it managed to successfully its own plugins. 73 | */ 74 | 75 | using var loader = PluginLoader.CreateFromAssemblyFile(TestResources.GetTestProjectAssembly("WithOwnPlugins"), new[] { typeof(IWithOwnPlugins) }); 76 | var assembly = loader.LoadDefaultAssembly(); 77 | var configType = assembly.GetType("WithOwnPlugins.WithOwnPlugins", throwOnError: true)!; 78 | var config = (IWithOwnPlugins?)Activator.CreateInstance(configType); 79 | 80 | /* 81 | * Here, we have made sure that neither WithOwnPlugins or its own plugins have any way to be 82 | * accidentally unified with the default (current for our tests) ALC. We did this by ensuring they are 83 | * not loaded in the default ALC in the first place, hence the use of the `IWithOwnPlugins` interface. 84 | * 85 | * We are simulating a real use case scenario where the plugin host is 100% unaware of the 86 | * plugin's own plugins. 87 | * 88 | * An important additional note: 89 | * - Although the assembly of WithOurPlugins is not directly referenced thanks to the 90 | * ReferenceOutputAssembly = false property, its contents will still be copied to the output. 91 | * - This is problematic because the test runner seems to load all of the Assemblies present in the same 92 | * directory as the test assembly, regardless of whether referenced or not. 93 | * - Therefore we store the plugins of `WithOwnPlugins` are output in a `Plugins` directory. 94 | * (see csproj of WithOwnPlugins, Link property) 95 | * 96 | * You can ensure that WithOwnPlugins or its plugins are not loaded by inspecting the following: 97 | * AssemblyLoadContext.Default.Assemblies 98 | * 99 | * Even if it was loaded, there's an extra check on the other side to ensure no unification could happen. 100 | * Nothing wrong with being extra careful ;). 101 | */ 102 | 103 | var callingContext = AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly()); 104 | Assert.True(config?.TryLoadPluginsInCustomContext(callingContext)); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /test/Plugins.Tests/TestProjectRefs.targets: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | false 6 | true 7 | _ResolvedTestProjectReference 8 | 9 | 10 | false 11 | true 12 | _ResolvedTestProjectReference 13 | TargetFramework;TargetFrameworks 14 | 15 | 16 | false 17 | true 18 | _ResolvedPublishedTestProjectReference 19 | TargetFramework;TargetFrameworks 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 30 | 31 | 32 | <_Parameter1>%(_ResolvedTestProjectReference.FileName) 33 | <_Parameter2>%(_ResolvedTestProjectReference.RootDir)%(_ResolvedTestProjectReference.Directory)%(_ResolvedTestProjectReference.FileName).dll 34 | 35 | 36 | 37 | 41 | 42 | 43 | 44 | <_Parameter1>%(PublishedTestProject.FileName) 45 | <_Parameter2>$(TargetDir)%(PublishedTestProject.FileName)/%(PublishedTestProject.FileName).dll 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /test/Plugins.Tests/Utilities/TestProjectReferenceAttribute.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | 6 | namespace McMaster.NETCore.Plugins.Tests 7 | { 8 | [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] 9 | public class TestProjectReferenceAttribute : Attribute 10 | { 11 | public TestProjectReferenceAttribute(string name, string path) 12 | { 13 | Name = name; 14 | Path = path; 15 | } 16 | 17 | public string Name { get; } 18 | public string Path { get; } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/Plugins.Tests/Utilities/TestResources.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System.Linq; 5 | using System.Reflection; 6 | 7 | namespace McMaster.NETCore.Plugins.Tests 8 | { 9 | public class TestResources 10 | { 11 | public static string GetTestProjectAssembly(string name) 12 | { 13 | return typeof(TestResources) 14 | .Assembly 15 | .GetCustomAttributes() 16 | .First(a => a.Name == name) 17 | .Path; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/TestProjects/Banana/Banana.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using Test.Referenced.Library; 5 | 6 | namespace Test 7 | { 8 | internal class Banana : IFruit 9 | { 10 | public string GetFlavor() => nameof(Banana); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/TestProjects/Banana/Banana.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | net8.0 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/TestProjects/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | true 6 | 7 | 8 | -------------------------------------------------------------------------------- /test/TestProjects/DrawingApp/DrawingApp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/TestProjects/DrawingApp/Finder.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System.Drawing.Printing; 5 | 6 | public class Finder 7 | { 8 | public static string FindDrawingAssembly() 9 | { 10 | return typeof(PrinterUnit).Assembly.Location; 11 | } 12 | } 13 | 14 | -------------------------------------------------------------------------------- /test/TestProjects/Libv1/Class1.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | namespace Mylib; 5 | 6 | public class Class1 7 | { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /test/TestProjects/Libv1/Libv1.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | 1.0.0.0 6 | Mylib 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/TestProjects/Libv2/Class1.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | namespace Mylib; 5 | 6 | public class Class1 7 | { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /test/TestProjects/Libv2/Libv2.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | 2.0.0.0 6 | Mylib 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/TestProjects/Libv3/Class1.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | namespace Mylib; 5 | 6 | public class Class1 7 | { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /test/TestProjects/Libv3/Libv3.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | 3.0.0.0 6 | Mylib 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/TestProjects/NativeDependency/NativeDependency.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/TestProjects/NativeDependency/NativeDependencyLoader.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.IO; 6 | 7 | using Microsoft.Data.Sqlite; 8 | 9 | namespace NativeDependency 10 | { 11 | public static class NativeDependencyLoader 12 | { 13 | public static void Load() 14 | { 15 | using var tempFile = new TempFile("db.sqlite"); 16 | using var dbConnection = new SqliteConnection($"Data Source={tempFile.FilePath}"); 17 | 18 | dbConnection.Open(); 19 | } 20 | } 21 | 22 | public class TempFile : IDisposable 23 | { 24 | public TempFile(string fileName) 25 | { 26 | FilePath = Path.Combine(Path.GetTempPath(), fileName); 27 | } 28 | 29 | public string FilePath { get; } 30 | 31 | public void Dispose() 32 | { 33 | if (!File.Exists(FilePath)) 34 | { 35 | return; 36 | } 37 | 38 | File.Delete(FilePath); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/TestProjects/NetCoreApp2App/NetCoreApp2App.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/TestProjects/NetCoreApp2App/Program.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | 6 | namespace NetCoreApp2App 7 | { 8 | internal class Program 9 | { 10 | public static void Main(string[] args) 11 | { 12 | Console.WriteLine(GetGreeting()); 13 | } 14 | 15 | public static string GetGreeting() => "Hello world!"; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/TestProjects/NetStandardClassLib/Class1.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | namespace NetStandardClassLib 5 | { 6 | public class Class1 7 | { 8 | public string GetColor() => "Red"; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/TestProjects/NetStandardClassLib/NetStandardClassLib.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /test/TestProjects/Plátano/Plátano.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using Plátano; 5 | using Test.Referenced.Library; 6 | 7 | namespace Test 8 | { 9 | internal class Plátano : IFruit 10 | { 11 | public string GetFlavor() => Strings.Flavor; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/TestProjects/Plátano/Plátano.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | net8.0 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/TestProjects/Plátano/Strings.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace Plátano { 12 | using System; 13 | 14 | 15 | [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] 16 | [System.Diagnostics.DebuggerNonUserCodeAttribute()] 17 | [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 18 | public class Strings { 19 | 20 | private static System.Resources.ResourceManager resourceMan; 21 | 22 | private static System.Globalization.CultureInfo resourceCulture; 23 | 24 | [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 25 | internal Strings() { 26 | } 27 | 28 | [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] 29 | public static System.Resources.ResourceManager ResourceManager { 30 | get { 31 | if (object.Equals(null, resourceMan)) { 32 | System.Resources.ResourceManager temp = new System.Resources.ResourceManager("Plátano.Strings", typeof(Strings).Assembly); 33 | resourceMan = temp; 34 | } 35 | return resourceMan; 36 | } 37 | } 38 | 39 | [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] 40 | public static System.Globalization.CultureInfo Culture { 41 | get { 42 | return resourceCulture; 43 | } 44 | set { 45 | resourceCulture = value; 46 | } 47 | } 48 | 49 | public static string Flavor { 50 | get { 51 | return ResourceManager.GetString("Flavor", resourceCulture); 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test/TestProjects/Plátano/Strings.es.Designer.cs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/TestProjects/Plátano/Strings.es.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | text/microsoft-resx 11 | 12 | 13 | 1.3 14 | 15 | 16 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 17 | 18 | 19 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 20 | 21 | 22 | Plátano 23 | 24 | 25 | -------------------------------------------------------------------------------- /test/TestProjects/Plátano/Strings.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | text/microsoft-resx 11 | 12 | 13 | 1.3 14 | 15 | 16 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 17 | 18 | 19 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 20 | 21 | 22 | Banana 23 | 24 | 25 | -------------------------------------------------------------------------------- /test/TestProjects/PowerShellPlugin/PowerShellPlugin.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/TestProjects/PowerShellPlugin/Program.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Management.Automation; 6 | 7 | namespace PowerShellPlugin 8 | { 9 | internal class Program 10 | { 11 | public static void Main(string[] args) 12 | { 13 | Console.WriteLine(GetGreeting()); 14 | } 15 | 16 | public static string GetGreeting() 17 | { 18 | using var ps = PowerShell.Create(); 19 | var type = typeof(AliasAttribute); 20 | // Console.WriteLine(type.Assembly.Location); 21 | var results = ps.AddScript("Write-Output hello").Invoke(); 22 | return results[0].ToString(); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/TestProjects/PrivateDepv1/Class1.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | namespace PrivateDepv1; 5 | 6 | public class Class1 7 | { 8 | } 9 | -------------------------------------------------------------------------------- /test/TestProjects/PrivateDepv1/PrivateDepv1.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/TestProjects/PrivateDepv2/Class1.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | namespace PrivateDepv2; 5 | 6 | public class Class1 7 | { 8 | } 9 | -------------------------------------------------------------------------------- /test/TestProjects/PrivateDepv2/PrivateDepv2.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/TestProjects/PrivateDepv3/Class1.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | namespace PrivateDepv3; 5 | 6 | public class Class1 7 | { 8 | } 9 | -------------------------------------------------------------------------------- /test/TestProjects/PrivateDepv3/PrivateDepv3.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/TestProjects/ReferencedLibv1/Class1.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | namespace Test.Referenced.Library 5 | { 6 | public class Class1 7 | { 8 | public static string GetVersion() => "v1"; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/TestProjects/ReferencedLibv1/IFruit.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | namespace Test.Referenced.Library 5 | { 6 | public interface IFruit 7 | { 8 | string GetFlavor(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/TestProjects/ReferencedLibv1/ReferencedLibv1.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | Test.Referenced.Library 6 | 1.0.0.0 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/TestProjects/ReferencedLibv2/Class1.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | namespace Test.Referenced.Library 5 | { 6 | public class Class1 7 | { 8 | public static string GetVersion() => "v2"; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/TestProjects/ReferencedLibv2/ReferencedLibv2.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | Test.Referenced.Library 6 | 2.0.0.0 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /test/TestProjects/SharedAbstraction.v1/SharedAbstraction.v1.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | Test.Shared.Abstraction 6 | 1.0.0.0 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /test/TestProjects/SharedAbstraction.v1/SharedType.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using Test.Transitive; 6 | 7 | namespace Test.Shared.Abstraction 8 | { 9 | public class SharedType 10 | { 11 | public Type GetTransitive() => typeof(TransitiveSharedType); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/TestProjects/SharedAbstraction.v2/SharedAbstraction.v2.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | Test.Shared.Abstraction 6 | 2.0.0.0 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /test/TestProjects/SqlClientApp/Program.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System.Data.SqlClient; 5 | 6 | namespace SqlClientApp 7 | { 8 | public class Program 9 | { 10 | // required to make the C# compiler happy 11 | public static void Main(string[] args) 12 | { 13 | } 14 | 15 | public static bool Run() 16 | { 17 | try 18 | { 19 | using var client = new SqlConnection(@"Data Source=(localdb)\mssqllocaldb;Integrated Security=True"); 20 | client.Open(); 21 | return !string.IsNullOrEmpty(client.ServerVersion); 22 | } 23 | catch (SqlException ex) when (ex.Number == -2) // -2 means SQL timeout 24 | { 25 | // When running the test in Azure DevOps build pipeline, we'll get a SqlException with "Connection Timeout Expired". 26 | // We can ignore this safely in unit tests. 27 | return true; 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/TestProjects/SqlClientApp/SqlClientApp.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | Exe 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/TestProjects/Strawberry/Strawberry.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using Test.Referenced.Library; 5 | 6 | namespace Test 7 | { 8 | internal class Strawberry : IFruit 9 | { 10 | public string GetFlavor() => nameof(Strawberry); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/TestProjects/Strawberry/Strawberry.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | netstandard2.0 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/TestProjects/TransitiveDep.v1/TransitiveDep.v1.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | Test.Transitive 6 | 1.0.0.0 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/TestProjects/TransitiveDep.v1/TransitiveSharedType.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | namespace Test.Transitive 5 | { 6 | public class TransitiveSharedType { } 7 | } 8 | -------------------------------------------------------------------------------- /test/TestProjects/TransitiveDep.v2/TransitiveDep.v2.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | Test.Transitive 6 | 2.0.0.0 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /test/TestProjects/TransitivePlugin/PluginConfig.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using Test.Transitive; 5 | 6 | namespace TransitivePlugin 7 | { 8 | public class PluginConfig 9 | { 10 | public TransitiveSharedType GetTransitiveType() => new(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/TestProjects/TransitivePlugin/TransitivePlugin.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/TestProjects/WithOurPluginsPluginA/Class1.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using WithOurPluginsPluginContract; 5 | 6 | namespace WithOurPluginsPluginA 7 | { 8 | public class Class1 : ISayHello 9 | { 10 | public string SayHello() => $"Hello from {nameof(WithOurPluginsPluginA)}"; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/TestProjects/WithOurPluginsPluginA/WithOurPluginsPluginA.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | false 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/TestProjects/WithOurPluginsPluginB/Class1.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using WithOurPluginsPluginContract; 5 | 6 | namespace WithOurPluginsPluginB 7 | { 8 | public class Class1 : ISayHello 9 | { 10 | public string SayHello() => $"Hello from {nameof(WithOurPluginsPluginB)}"; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/TestProjects/WithOurPluginsPluginB/WithOurPluginsPluginB.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | false 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/TestProjects/WithOurPluginsPluginContract/ISayHello.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | namespace WithOurPluginsPluginContract 5 | { 6 | public interface ISayHello 7 | { 8 | string SayHello(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/TestProjects/WithOurPluginsPluginContract/WithOurPluginsPluginContract.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /test/TestProjects/WithOwnPlugins/WithOwnPlugins.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Reflection; 9 | using System.Runtime.Loader; 10 | using McMaster.NETCore.Plugins; 11 | using WithOurPluginsPluginContract; 12 | using WithOwnPluginsContract; 13 | 14 | namespace WithOwnPlugins 15 | { 16 | public class WithOwnPlugins : IWithOwnPlugins 17 | { 18 | public bool TryLoadPluginsInCustomContext(AssemblyLoadContext? callingContext) 19 | { 20 | var currentContext = AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly()); 21 | if (currentContext == callingContext) 22 | { 23 | throw new ArgumentException("The context of the caller is the context of this assembly. This invalidates the test."); 24 | } 25 | 26 | /* 27 | Ensure the source calling context does not have our plugin's interfaces loaded. 28 | This guarantees that the Assembly cannot possibly unify with the default load context. 29 | 30 | Note: 31 | The code below this check would fail anyway if the assembly would unify with the default context. 32 | This is more of a safety check to ensure "correctness" as opposed to anything else. 33 | */ 34 | var sayHelloAssembly = typeof(ISayHello).Assembly; 35 | if (callingContext?.Assemblies.Contains(sayHelloAssembly) == true) // .Assemblies API not available in Core 2.X 36 | { 37 | throw new ArgumentException("The context of the caller has this plugin's interface to interact with its own plugins loaded. Test is void."); 38 | } 39 | 40 | // Load our own plugins: Remember, we are in an isolated, non-default ALC. 41 | var plugins = new List(); 42 | string[] assemblyNames = { "Plugins/WithOurPluginsPluginA.dll", "Plugins/WithOurPluginsPluginB.dll" }; 43 | 44 | foreach (var assemblyName in assemblyNames) 45 | { 46 | var currentAssemblyFolderPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? throw new Exception("Unable to get folder path for currently executing assembly."); 47 | var pluginPath = Path.Combine(currentAssemblyFolderPath, assemblyName); 48 | 49 | using var loader = PluginLoader.CreateFromAssemblyFile(pluginPath, new[] { typeof(ISayHello) }); 50 | var assembly = loader.LoadDefaultAssembly(); 51 | var configType = assembly.GetTypes().First(x => typeof(ISayHello).IsAssignableFrom(x) && !x.IsAbstract); 52 | var plugin = (ISayHello?)Activator.CreateInstance(configType); 53 | if (plugin == null) 54 | { 55 | throw new Exception($"Failed to load instance of {nameof(ISayHello)} from plugin."); 56 | } 57 | 58 | plugins.Add(plugin); 59 | } 60 | 61 | // Shouldn't need to check for this but just in case to absolutely make sure. 62 | if (plugins.Any(plugin => String.IsNullOrEmpty(plugin?.SayHello()))) 63 | { 64 | throw new Exception("No value returned from plugin."); 65 | } 66 | 67 | return true; 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /test/TestProjects/WithOwnPlugins/WithOwnPlugins.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /test/TestProjects/WithOwnPluginsContract/IWithOwnPlugins.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System.Runtime.Loader; 5 | 6 | namespace WithOwnPluginsContract 7 | { 8 | public interface IWithOwnPlugins 9 | { 10 | bool TryLoadPluginsInCustomContext(AssemblyLoadContext? callingContext); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/TestProjects/WithOwnPluginsContract/WithOwnPluginsContract.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /test/TestProjects/XunitSample/Class1.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Nate McMaster. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | namespace XunitSample 5 | { 6 | public class Class1 7 | { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/TestProjects/XunitSample/XunitSample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | true 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | --------------------------------------------------------------------------------