Merge pull request '新增PluginCore插件库及插件示例' (#285) from tang1022.cool/Admin.NET.Pro:v2 into v2

Reviewed-on: https://code.adminnet.top/Admin.NET/Admin.NET.Pro/pulls/285
This commit is contained in:
zuohuaijun 2025-03-08 22:47:44 +08:00
commit 0f92c2f475
175 changed files with 16730 additions and 0 deletions

View File

@ -26,7 +26,9 @@
<ItemGroup>
<ProjectReference Include="..\Admin.NET.Core\Admin.NET.Core.csproj" />
<ProjectReference Include="..\Plugins\Admin.NET.Plugin.Core\Admin.NET.Plugin.Core.csproj" />
<ProjectReference Include="..\Plugins\Admin.NET.Plugin.GoView\Admin.NET.Plugin.GoView.csproj" />
<ProjectReference Include="..\Plugins\Admin.NET.Plugin.PluginCoreManager\Admin.NET.Plugin.PluginCoreManager.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1 @@
{"Admin":{"UserName":"admin","Password":"ABC12345"},"FrontendMode":"LocalEmbedded","RemoteFrontend":"https://cdn.jsdelivr.net/gh/yiyungent/plugincore-admin-frontend@0.3.1/dist-cdn","PluginWidgetDebug":false}

View File

@ -0,0 +1 @@
{"EnabledPlugins":["Admin.NET.Plugin.Pay.Alipay"]}

View File

@ -0,0 +1,738 @@
{
"runtimeTarget": {
"name": ".NETCoreApp,Version=v8.0",
"signature": ""
},
"compilationOptions": {},
"targets": {
".NETCoreApp,Version=v8.0": {
"Admin.NET.Plugin.Pay.Alipay/1.0.0": {
"dependencies": {
"Admin.NET.Plugin.Core": "1.0.0",
"Furion.Pure": "4.9.7.3"
},
"runtime": {
"Admin.NET.Plugin.Pay.Alipay.dll": {}
}
},
"Ben.Demystifier/0.4.1": {
"dependencies": {
"System.Reflection.Metadata": "7.0.0"
},
"runtime": {
"lib/netstandard2.1/Ben.Demystifier.dll": {
"assemblyVersion": "0.4.0.0",
"fileVersion": "0.4.0.2"
}
}
},
"Furion.Pure/4.9.7.3": {
"dependencies": {
"Furion.Pure.Extras.DependencyModel.CodeAnalysis": "4.9.7.3",
"MiniProfiler.AspNetCore.Mvc": "4.5.4",
"Swashbuckle.AspNetCore": "7.2.0"
},
"runtime": {
"lib/net8.0/Furion.Pure.dll": {
"assemblyVersion": "4.9.7.3",
"fileVersion": "4.9.7.3"
}
}
},
"Furion.Pure.Extras.DependencyModel.CodeAnalysis/4.9.7.3": {
"dependencies": {
"Ben.Demystifier": "0.4.1",
"Microsoft.AspNetCore.Mvc.NewtonsoftJson": "8.0.11",
"Microsoft.AspNetCore.Razor.Language": "6.0.36",
"Microsoft.CodeAnalysis.CSharp": "4.8.0",
"Microsoft.Extensions.DependencyModel": "8.0.2",
"System.Text.Json": "8.0.5",
"System.Text.RegularExpressions": "4.3.1"
},
"runtime": {
"lib/net8.0/Furion.Pure.Extras.DependencyModel.CodeAnalysis.dll": {
"assemblyVersion": "4.9.7.3",
"fileVersion": "4.9.7.3"
}
}
},
"Microsoft.AspNetCore.Http.Abstractions/2.3.0": {
"dependencies": {
"Microsoft.AspNetCore.Http.Features": "2.3.0",
"System.Text.Encodings.Web": "8.0.0"
}
},
"Microsoft.AspNetCore.Http.Features/2.3.0": {
"dependencies": {
"Microsoft.Extensions.Primitives": "8.0.0"
}
},
"Microsoft.AspNetCore.JsonPatch/8.0.11": {
"dependencies": {
"Microsoft.CSharp": "4.7.0",
"Newtonsoft.Json": "13.0.3"
},
"runtime": {
"lib/net8.0/Microsoft.AspNetCore.JsonPatch.dll": {
"assemblyVersion": "8.0.11.0",
"fileVersion": "8.0.1124.52116"
}
}
},
"Microsoft.AspNetCore.Mvc.NewtonsoftJson/8.0.11": {
"dependencies": {
"Microsoft.AspNetCore.JsonPatch": "8.0.11",
"Newtonsoft.Json": "13.0.3",
"Newtonsoft.Json.Bson": "1.0.2"
},
"runtime": {
"lib/net8.0/Microsoft.AspNetCore.Mvc.NewtonsoftJson.dll": {
"assemblyVersion": "8.0.11.0",
"fileVersion": "8.0.1124.52116"
}
}
},
"Microsoft.AspNetCore.Razor.Language/6.0.36": {
"runtime": {
"lib/netstandard2.0/Microsoft.AspNetCore.Razor.Language.dll": {
"assemblyVersion": "6.0.36.0",
"fileVersion": "6.0.3624.51604"
}
}
},
"Microsoft.CodeAnalysis.Analyzers/3.3.4": {},
"Microsoft.CodeAnalysis.Common/4.8.0": {
"dependencies": {
"Microsoft.CodeAnalysis.Analyzers": "3.3.4",
"System.Collections.Immutable": "7.0.0",
"System.Reflection.Metadata": "7.0.0",
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
},
"runtime": {
"lib/net7.0/Microsoft.CodeAnalysis.dll": {
"assemblyVersion": "4.8.0.0",
"fileVersion": "4.800.23.55801"
}
},
"resources": {
"lib/net7.0/cs/Microsoft.CodeAnalysis.resources.dll": {
"locale": "cs"
},
"lib/net7.0/de/Microsoft.CodeAnalysis.resources.dll": {
"locale": "de"
},
"lib/net7.0/es/Microsoft.CodeAnalysis.resources.dll": {
"locale": "es"
},
"lib/net7.0/fr/Microsoft.CodeAnalysis.resources.dll": {
"locale": "fr"
},
"lib/net7.0/it/Microsoft.CodeAnalysis.resources.dll": {
"locale": "it"
},
"lib/net7.0/ja/Microsoft.CodeAnalysis.resources.dll": {
"locale": "ja"
},
"lib/net7.0/ko/Microsoft.CodeAnalysis.resources.dll": {
"locale": "ko"
},
"lib/net7.0/pl/Microsoft.CodeAnalysis.resources.dll": {
"locale": "pl"
},
"lib/net7.0/pt-BR/Microsoft.CodeAnalysis.resources.dll": {
"locale": "pt-BR"
},
"lib/net7.0/ru/Microsoft.CodeAnalysis.resources.dll": {
"locale": "ru"
},
"lib/net7.0/tr/Microsoft.CodeAnalysis.resources.dll": {
"locale": "tr"
},
"lib/net7.0/zh-Hans/Microsoft.CodeAnalysis.resources.dll": {
"locale": "zh-Hans"
},
"lib/net7.0/zh-Hant/Microsoft.CodeAnalysis.resources.dll": {
"locale": "zh-Hant"
}
}
},
"Microsoft.CodeAnalysis.CSharp/4.8.0": {
"dependencies": {
"Microsoft.CodeAnalysis.Common": "4.8.0"
},
"runtime": {
"lib/net7.0/Microsoft.CodeAnalysis.CSharp.dll": {
"assemblyVersion": "4.8.0.0",
"fileVersion": "4.800.23.55801"
}
},
"resources": {
"lib/net7.0/cs/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "cs"
},
"lib/net7.0/de/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "de"
},
"lib/net7.0/es/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "es"
},
"lib/net7.0/fr/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "fr"
},
"lib/net7.0/it/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "it"
},
"lib/net7.0/ja/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "ja"
},
"lib/net7.0/ko/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "ko"
},
"lib/net7.0/pl/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "pl"
},
"lib/net7.0/pt-BR/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "pt-BR"
},
"lib/net7.0/ru/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "ru"
},
"lib/net7.0/tr/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "tr"
},
"lib/net7.0/zh-Hans/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "zh-Hans"
},
"lib/net7.0/zh-Hant/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "zh-Hant"
}
}
},
"Microsoft.CSharp/4.7.0": {},
"Microsoft.Extensions.ApiDescription.Server/6.0.5": {},
"Microsoft.Extensions.DependencyInjection.Abstractions/9.0.1": {
"runtime": {
"lib/net8.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll": {
"assemblyVersion": "9.0.0.0",
"fileVersion": "9.0.124.61010"
}
}
},
"Microsoft.Extensions.DependencyModel/8.0.2": {
"runtime": {
"lib/net8.0/Microsoft.Extensions.DependencyModel.dll": {
"assemblyVersion": "8.0.0.2",
"fileVersion": "8.0.1024.46610"
}
}
},
"Microsoft.Extensions.Logging.Abstractions/9.0.1": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"System.Diagnostics.DiagnosticSource": "9.0.1"
},
"runtime": {
"lib/net8.0/Microsoft.Extensions.Logging.Abstractions.dll": {
"assemblyVersion": "9.0.0.0",
"fileVersion": "9.0.124.61010"
}
}
},
"Microsoft.Extensions.Primitives/8.0.0": {},
"Microsoft.NETCore.Platforms/1.1.1": {},
"Microsoft.NETCore.Targets/1.1.3": {},
"Microsoft.OpenApi/1.6.22": {
"runtime": {
"lib/netstandard2.0/Microsoft.OpenApi.dll": {
"assemblyVersion": "1.6.22.0",
"fileVersion": "1.6.22.0"
}
}
},
"MiniProfiler.AspNetCore/4.5.4": {
"dependencies": {
"MiniProfiler.Shared": "4.5.4"
},
"runtime": {
"lib/net8.0/MiniProfiler.AspNetCore.dll": {
"assemblyVersion": "4.0.0.0",
"fileVersion": "4.5.4.47516"
}
}
},
"MiniProfiler.AspNetCore.Mvc/4.5.4": {
"dependencies": {
"MiniProfiler.AspNetCore": "4.5.4"
},
"runtime": {
"lib/net8.0/MiniProfiler.AspNetCore.Mvc.dll": {
"assemblyVersion": "4.0.0.0",
"fileVersion": "4.5.4.47516"
}
}
},
"MiniProfiler.Shared/4.5.4": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1"
},
"runtime": {
"lib/net8.0/MiniProfiler.Shared.dll": {
"assemblyVersion": "4.0.0.0",
"fileVersion": "4.5.4.47516"
}
}
},
"Newtonsoft.Json/13.0.3": {
"runtime": {
"lib/net6.0/Newtonsoft.Json.dll": {
"assemblyVersion": "13.0.0.0",
"fileVersion": "13.0.3.27908"
}
}
},
"Newtonsoft.Json.Bson/1.0.2": {
"dependencies": {
"Newtonsoft.Json": "13.0.3"
},
"runtime": {
"lib/netstandard2.0/Newtonsoft.Json.Bson.dll": {
"assemblyVersion": "1.0.0.0",
"fileVersion": "1.0.2.22727"
}
}
},
"SharpZipLib/1.4.2": {
"runtime": {
"lib/net6.0/ICSharpCode.SharpZipLib.dll": {
"assemblyVersion": "1.4.2.13",
"fileVersion": "1.4.2.13"
}
}
},
"Swashbuckle.AspNetCore/7.2.0": {
"dependencies": {
"Microsoft.Extensions.ApiDescription.Server": "6.0.5",
"Swashbuckle.AspNetCore.Swagger": "7.2.0",
"Swashbuckle.AspNetCore.SwaggerGen": "7.2.0",
"Swashbuckle.AspNetCore.SwaggerUI": "7.2.0"
}
},
"Swashbuckle.AspNetCore.Swagger/7.2.0": {
"dependencies": {
"Microsoft.OpenApi": "1.6.22"
},
"runtime": {
"lib/net8.0/Swashbuckle.AspNetCore.Swagger.dll": {
"assemblyVersion": "7.2.0.0",
"fileVersion": "7.2.0.956"
}
}
},
"Swashbuckle.AspNetCore.SwaggerGen/7.2.0": {
"dependencies": {
"Swashbuckle.AspNetCore.Swagger": "7.2.0"
},
"runtime": {
"lib/net8.0/Swashbuckle.AspNetCore.SwaggerGen.dll": {
"assemblyVersion": "7.2.0.0",
"fileVersion": "7.2.0.956"
}
}
},
"Swashbuckle.AspNetCore.SwaggerUI/7.2.0": {
"runtime": {
"lib/net8.0/Swashbuckle.AspNetCore.SwaggerUI.dll": {
"assemblyVersion": "7.2.0.0",
"fileVersion": "7.2.0.956"
}
}
},
"System.Collections.Immutable/7.0.0": {},
"System.Diagnostics.DiagnosticSource/9.0.1": {
"runtime": {
"lib/net8.0/System.Diagnostics.DiagnosticSource.dll": {
"assemblyVersion": "9.0.0.0",
"fileVersion": "9.0.124.61010"
}
}
},
"System.Reflection.Metadata/7.0.0": {
"dependencies": {
"System.Collections.Immutable": "7.0.0"
}
},
"System.Runtime/4.3.1": {
"dependencies": {
"Microsoft.NETCore.Platforms": "1.1.1",
"Microsoft.NETCore.Targets": "1.1.3"
}
},
"System.Runtime.CompilerServices.Unsafe/6.0.0": {},
"System.Text.Encodings.Web/8.0.0": {},
"System.Text.Json/8.0.5": {},
"System.Text.RegularExpressions/4.3.1": {
"dependencies": {
"System.Runtime": "4.3.1"
}
},
"Admin.NET.Plugin.Core/1.0.0": {
"dependencies": {
"PluginCore.AspNetCore": "1.4.3"
},
"runtime": {
"Admin.NET.Plugin.Core.dll": {
"assemblyVersion": "1.0.0",
"fileVersion": "1.0.0.0"
}
}
},
"PluginCore/2.2.5": {
"dependencies": {
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"PluginCore.IPlugins": "0.9.1",
"SharpZipLib": "1.4.2"
},
"runtime": {
"PluginCore.dll": {
"assemblyVersion": "2.2.5",
"fileVersion": "2.2.5.0"
}
}
},
"PluginCore.AspNetCore/1.4.3": {
"dependencies": {
"PluginCore": "2.2.5",
"PluginCore.IPlugins.AspNetCore": "0.1.1"
},
"runtime": {
"PluginCore.AspNetCore.dll": {
"assemblyVersion": "1.4.3",
"fileVersion": "1.4.3.0"
}
}
},
"PluginCore.IPlugins/0.9.1": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"System.Text.Json": "8.0.5"
},
"runtime": {
"PluginCore.IPlugins.dll": {
"assemblyVersion": "0.9.1",
"fileVersion": "0.9.1.0"
}
}
},
"PluginCore.IPlugins.AspNetCore/0.1.1": {
"dependencies": {
"Microsoft.AspNetCore.Http.Abstractions": "2.3.0",
"PluginCore.IPlugins": "0.9.1"
},
"runtime": {
"PluginCore.IPlugins.AspNetCore.dll": {
"assemblyVersion": "0.1.1",
"fileVersion": "0.1.1.0"
}
}
}
}
},
"libraries": {
"Admin.NET.Plugin.Pay.Alipay/1.0.0": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Ben.Demystifier/0.4.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-axFeEMfmEORy3ipAzOXG/lE+KcNptRbei3F0C4kQCdeiQtW+qJW90K5iIovITGrdLt8AjhNCwk5qLSX9/rFpoA==",
"path": "ben.demystifier/0.4.1",
"hashPath": "ben.demystifier.0.4.1.nupkg.sha512"
},
"Furion.Pure/4.9.7.3": {
"type": "package",
"serviceable": true,
"sha512": "sha512-4HAtU6kg3/5cY6ImtbKs+qDa+8BteyD9DUIcCVSq3oo8AWE2ZeMJmXOD9rx+5/3mRQXkt5DIXVa4jIG32FjTHw==",
"path": "furion.pure/4.9.7.3",
"hashPath": "furion.pure.4.9.7.3.nupkg.sha512"
},
"Furion.Pure.Extras.DependencyModel.CodeAnalysis/4.9.7.3": {
"type": "package",
"serviceable": true,
"sha512": "sha512-v6pMgb+tkTeGFpmG98CY8Mv4QNBfTDiDsZmKGb6DEJxTPhdmr7l8onSdTlBuUlxfo94KXIOf7Gq+czr0RX/qbw==",
"path": "furion.pure.extras.dependencymodel.codeanalysis/4.9.7.3",
"hashPath": "furion.pure.extras.dependencymodel.codeanalysis.4.9.7.3.nupkg.sha512"
},
"Microsoft.AspNetCore.Http.Abstractions/2.3.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-39r9PPrjA6s0blyFv5qarckjNkaHRA5B+3b53ybuGGNTXEj1/DStQJ4NWjFL6QTRQpL9zt7nDyKxZdJOlcnq+Q==",
"path": "microsoft.aspnetcore.http.abstractions/2.3.0",
"hashPath": "microsoft.aspnetcore.http.abstractions.2.3.0.nupkg.sha512"
},
"Microsoft.AspNetCore.Http.Features/2.3.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-f10WUgcsKqrkmnz6gt8HeZ7kyKjYN30PO7cSic1lPtH7paPtnQqXPOveul/SIPI43PhRD4trttg4ywnrEmmJpA==",
"path": "microsoft.aspnetcore.http.features/2.3.0",
"hashPath": "microsoft.aspnetcore.http.features.2.3.0.nupkg.sha512"
},
"Microsoft.AspNetCore.JsonPatch/8.0.11": {
"type": "package",
"serviceable": true,
"sha512": "sha512-l1tFnQm2LtFE3M9YRM/bdwtxxCV50Y5jnN0LjliQH1sqvWsN46++Uu3QCJL9IdOweFvXSf3Shi7DI/Vc1jkdKA==",
"path": "microsoft.aspnetcore.jsonpatch/8.0.11",
"hashPath": "microsoft.aspnetcore.jsonpatch.8.0.11.nupkg.sha512"
},
"Microsoft.AspNetCore.Mvc.NewtonsoftJson/8.0.11": {
"type": "package",
"serviceable": true,
"sha512": "sha512-XcfFd8e0g2M0mcAKVNgoHJtWYJfKrPntHhgqiZ1Ci37i3AEJbM0GHIa715i0UPSksiKmDxsJWXnM3rg8keF/Zg==",
"path": "microsoft.aspnetcore.mvc.newtonsoftjson/8.0.11",
"hashPath": "microsoft.aspnetcore.mvc.newtonsoftjson.8.0.11.nupkg.sha512"
},
"Microsoft.AspNetCore.Razor.Language/6.0.36": {
"type": "package",
"serviceable": true,
"sha512": "sha512-n5Mg5D0aRrhHJJ6bJcwKqQydIFcgUq0jTlvuynoJjwA2IvAzh8Aqf9cpYagofQbIlIXILkCP6q6FgbngyVtpYA==",
"path": "microsoft.aspnetcore.razor.language/6.0.36",
"hashPath": "microsoft.aspnetcore.razor.language.6.0.36.nupkg.sha512"
},
"Microsoft.CodeAnalysis.Analyzers/3.3.4": {
"type": "package",
"serviceable": true,
"sha512": "sha512-AxkxcPR+rheX0SmvpLVIGLhOUXAKG56a64kV9VQZ4y9gR9ZmPXnqZvHJnmwLSwzrEP6junUF11vuc+aqo5r68g==",
"path": "microsoft.codeanalysis.analyzers/3.3.4",
"hashPath": "microsoft.codeanalysis.analyzers.3.3.4.nupkg.sha512"
},
"Microsoft.CodeAnalysis.Common/4.8.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-/jR+e/9aT+BApoQJABlVCKnnggGQbvGh7BKq2/wI1LamxC+LbzhcLj4Vj7gXCofl1n4E521YfF9w0WcASGg/KA==",
"path": "microsoft.codeanalysis.common/4.8.0",
"hashPath": "microsoft.codeanalysis.common.4.8.0.nupkg.sha512"
},
"Microsoft.CodeAnalysis.CSharp/4.8.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-+3+qfdb/aaGD8PZRCrsdobbzGs1m9u119SkkJt8e/mk3xLJz/udLtS2T6nY27OTXxBBw10HzAbC8Z9w08VyP/g==",
"path": "microsoft.codeanalysis.csharp/4.8.0",
"hashPath": "microsoft.codeanalysis.csharp.4.8.0.nupkg.sha512"
},
"Microsoft.CSharp/4.7.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-pTj+D3uJWyN3My70i2Hqo+OXixq3Os2D1nJ2x92FFo6sk8fYS1m1WLNTs0Dc1uPaViH0YvEEwvzddQ7y4rhXmA==",
"path": "microsoft.csharp/4.7.0",
"hashPath": "microsoft.csharp.4.7.0.nupkg.sha512"
},
"Microsoft.Extensions.ApiDescription.Server/6.0.5": {
"type": "package",
"serviceable": true,
"sha512": "sha512-Ckb5EDBUNJdFWyajfXzUIMRkhf52fHZOQuuZg/oiu8y7zDCVwD0iHhew6MnThjHmevanpxL3f5ci2TtHQEN6bw==",
"path": "microsoft.extensions.apidescription.server/6.0.5",
"hashPath": "microsoft.extensions.apidescription.server.6.0.5.nupkg.sha512"
},
"Microsoft.Extensions.DependencyInjection.Abstractions/9.0.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-Tr74eP0oQ3AyC24ch17N8PuEkrPbD0JqIfENCYqmgKYNOmL8wQKzLJu3ObxTUDrjnn4rHoR1qKa37/eQyHmCDA==",
"path": "microsoft.extensions.dependencyinjection.abstractions/9.0.1",
"hashPath": "microsoft.extensions.dependencyinjection.abstractions.9.0.1.nupkg.sha512"
},
"Microsoft.Extensions.DependencyModel/8.0.2": {
"type": "package",
"serviceable": true,
"sha512": "sha512-mUBDZZRgZrSyFOsJ2qJJ9fXfqd/kXJwf3AiDoqLD9m6TjY5OO/vLNOb9fb4juC0487eq4hcGN/M2Rh/CKS7QYw==",
"path": "microsoft.extensions.dependencymodel/8.0.2",
"hashPath": "microsoft.extensions.dependencymodel.8.0.2.nupkg.sha512"
},
"Microsoft.Extensions.Logging.Abstractions/9.0.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-w2gUqXN/jNIuvqYwX3lbXagsizVNXYyt6LlF57+tMve4JYCEgCMMAjRce6uKcDASJgpMbErRT1PfHy2OhbkqEA==",
"path": "microsoft.extensions.logging.abstractions/9.0.1",
"hashPath": "microsoft.extensions.logging.abstractions.9.0.1.nupkg.sha512"
},
"Microsoft.Extensions.Primitives/8.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g==",
"path": "microsoft.extensions.primitives/8.0.0",
"hashPath": "microsoft.extensions.primitives.8.0.0.nupkg.sha512"
},
"Microsoft.NETCore.Platforms/1.1.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-TMBuzAHpTenGbGgk0SMTwyEkyijY/Eae4ZGsFNYJvAr/LDn1ku3Etp3FPxChmDp5HHF3kzJuoaa08N0xjqAJfQ==",
"path": "microsoft.netcore.platforms/1.1.1",
"hashPath": "microsoft.netcore.platforms.1.1.1.nupkg.sha512"
},
"Microsoft.NETCore.Targets/1.1.3": {
"type": "package",
"serviceable": true,
"sha512": "sha512-3Wrmi0kJDzClwAC+iBdUBpEKmEle8FQNsCs77fkiOIw/9oYA07bL1EZNX0kQ2OMN3xpwvl0vAtOCYY3ndDNlhQ==",
"path": "microsoft.netcore.targets/1.1.3",
"hashPath": "microsoft.netcore.targets.1.1.3.nupkg.sha512"
},
"Microsoft.OpenApi/1.6.22": {
"type": "package",
"serviceable": true,
"sha512": "sha512-aBvunmrdu/x+4CaA/UP1Jx4xWGwk4kymhoIRnn2Vp+zi5/KOPQJ9EkSXHRUr01WcGKtYl3Au7XfkPJbU1G2sjQ==",
"path": "microsoft.openapi/1.6.22",
"hashPath": "microsoft.openapi.1.6.22.nupkg.sha512"
},
"MiniProfiler.AspNetCore/4.5.4": {
"type": "package",
"serviceable": true,
"sha512": "sha512-meedJsjpYOeHPhE8H6t+dGQ9zLxcCQVpi4DXzmxmYAXywmTzlo6jv2IASUv5QijTU0CxsROln3FHd8RsTO8Z8A==",
"path": "miniprofiler.aspnetcore/4.5.4",
"hashPath": "miniprofiler.aspnetcore.4.5.4.nupkg.sha512"
},
"MiniProfiler.AspNetCore.Mvc/4.5.4": {
"type": "package",
"serviceable": true,
"sha512": "sha512-+NqXyCy9aNdroPm6leW5+cpngtCnkCdoyOlJzvVN62uucSx+MYkx8jmKbgAt+aCP6aghADfHBExwrTIldHxapg==",
"path": "miniprofiler.aspnetcore.mvc/4.5.4",
"hashPath": "miniprofiler.aspnetcore.mvc.4.5.4.nupkg.sha512"
},
"MiniProfiler.Shared/4.5.4": {
"type": "package",
"serviceable": true,
"sha512": "sha512-f8ckFm/xTS8C2Bn4BdVc94dNvg+tRfk0e4XFaETOqRi6r0PUOyn3Z9jTQCVpB3R1pP5WiRsEIrqqxux95BVpTA==",
"path": "miniprofiler.shared/4.5.4",
"hashPath": "miniprofiler.shared.4.5.4.nupkg.sha512"
},
"Newtonsoft.Json/13.0.3": {
"type": "package",
"serviceable": true,
"sha512": "sha512-HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==",
"path": "newtonsoft.json/13.0.3",
"hashPath": "newtonsoft.json.13.0.3.nupkg.sha512"
},
"Newtonsoft.Json.Bson/1.0.2": {
"type": "package",
"serviceable": true,
"sha512": "sha512-QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==",
"path": "newtonsoft.json.bson/1.0.2",
"hashPath": "newtonsoft.json.bson.1.0.2.nupkg.sha512"
},
"SharpZipLib/1.4.2": {
"type": "package",
"serviceable": true,
"sha512": "sha512-yjj+3zgz8zgXpiiC3ZdF/iyTBbz2fFvMxZFEBPUcwZjIvXOf37Ylm+K58hqMfIBt5JgU/Z2uoUS67JmTLe973A==",
"path": "sharpziplib/1.4.2",
"hashPath": "sharpziplib.1.4.2.nupkg.sha512"
},
"Swashbuckle.AspNetCore/7.2.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-vJv19UpWm6OOgnS9QLDnWARNVasXUfj8SFvlG7UVALm4nBnfwRnEky7C0veSDqMUmBeMPC6Ec3d6G1ts/J04Uw==",
"path": "swashbuckle.aspnetcore/7.2.0",
"hashPath": "swashbuckle.aspnetcore.7.2.0.nupkg.sha512"
},
"Swashbuckle.AspNetCore.Swagger/7.2.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-y27fNDfIh1vGhJjXYynLcZjl7DLOW1bSO2MDsY9wB4Zm1fdxpPsuBSiR4U+0acWlAqLmnuOPKr/OeOgwRUkBlw==",
"path": "swashbuckle.aspnetcore.swagger/7.2.0",
"hashPath": "swashbuckle.aspnetcore.swagger.7.2.0.nupkg.sha512"
},
"Swashbuckle.AspNetCore.SwaggerGen/7.2.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-pMrTxGVuXM7t4wqft5CNNU8A0++Yw5kTLmYhB6tbEcyBfO8xEF/Y8pkJhO6BZ/2MYONrRYoQTfPFJqu8fOf5WQ==",
"path": "swashbuckle.aspnetcore.swaggergen/7.2.0",
"hashPath": "swashbuckle.aspnetcore.swaggergen.7.2.0.nupkg.sha512"
},
"Swashbuckle.AspNetCore.SwaggerUI/7.2.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-hgrXeKzyp5OGN8qVvL7A+vhmU7mDJTfGpiMBRL66IcfLOyna8UTLtn3cC3CghamXpRDufcc9ciklTszUGEQK0w==",
"path": "swashbuckle.aspnetcore.swaggerui/7.2.0",
"hashPath": "swashbuckle.aspnetcore.swaggerui.7.2.0.nupkg.sha512"
},
"System.Collections.Immutable/7.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-dQPcs0U1IKnBdRDBkrCTi1FoajSTBzLcVTpjO4MBCMC7f4pDOIPzgBoX8JjG7X6uZRJ8EBxsi8+DR1JuwjnzOQ==",
"path": "system.collections.immutable/7.0.0",
"hashPath": "system.collections.immutable.7.0.0.nupkg.sha512"
},
"System.Diagnostics.DiagnosticSource/9.0.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-yOcDWx4P/s1I83+7gQlgQLmhny2eNcU0cfo1NBWi+en4EAI38Jau+/neT85gUW6w1s7+FUJc2qNOmmwGLIREqA==",
"path": "system.diagnostics.diagnosticsource/9.0.1",
"hashPath": "system.diagnostics.diagnosticsource.9.0.1.nupkg.sha512"
},
"System.Reflection.Metadata/7.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-MclTG61lsD9sYdpNz9xsKBzjsmsfCtcMZYXz/IUr2zlhaTaABonlr1ESeompTgM+Xk+IwtGYU7/voh3YWB/fWw==",
"path": "system.reflection.metadata/7.0.0",
"hashPath": "system.reflection.metadata.7.0.0.nupkg.sha512"
},
"System.Runtime/4.3.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-abhfv1dTK6NXOmu4bgHIONxHyEqFjW8HwXPmpY9gmll+ix9UNo4XDcmzJn6oLooftxNssVHdJC1pGT9jkSynQg==",
"path": "system.runtime/4.3.1",
"hashPath": "system.runtime.4.3.1.nupkg.sha512"
},
"System.Runtime.CompilerServices.Unsafe/6.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==",
"path": "system.runtime.compilerservices.unsafe/6.0.0",
"hashPath": "system.runtime.compilerservices.unsafe.6.0.0.nupkg.sha512"
},
"System.Text.Encodings.Web/8.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ==",
"path": "system.text.encodings.web/8.0.0",
"hashPath": "system.text.encodings.web.8.0.0.nupkg.sha512"
},
"System.Text.Json/8.0.5": {
"type": "package",
"serviceable": true,
"sha512": "sha512-0f1B50Ss7rqxXiaBJyzUu9bWFOO2/zSlifZ/UNMdiIpDYe4cY4LQQicP4nirK1OS31I43rn062UIJ1Q9bpmHpg==",
"path": "system.text.json/8.0.5",
"hashPath": "system.text.json.8.0.5.nupkg.sha512"
},
"System.Text.RegularExpressions/4.3.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-N0kNRrWe4+nXOWlpLT4LAY5brb8caNFlUuIRpraCVMDLYutKkol1aV079rQjLuSxKMJT2SpBQsYX9xbcTMmzwg==",
"path": "system.text.regularexpressions/4.3.1",
"hashPath": "system.text.regularexpressions.4.3.1.nupkg.sha512"
},
"Admin.NET.Plugin.Core/1.0.0": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"PluginCore/2.2.5": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"PluginCore.AspNetCore/1.4.3": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"PluginCore.IPlugins/0.9.1": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"PluginCore.IPlugins.AspNetCore/0.1.1": {
"type": "project",
"serviceable": false,
"sha512": ""
}
}
}

View File

@ -0,0 +1,34 @@
<?xml version="1.0"?>
<doc>
<assembly>
<name>Admin.NET.Plugin.Pay.Alipay</name>
</assembly>
<members>
<member name="M:Admin.NET.Plugin.Pay.Alipay.Middlewares.AlipaySayHelloMiddleware.#ctor(Microsoft.AspNetCore.Http.RequestDelegate)">
<summary>
<see cref="!:PluginApplicationBuilder"/> Build 时, 将会 new Middleware(), 最终将所有 Middleware 包装为一个 <see cref="T:Microsoft.AspNetCore.Http.RequestDelegate"/>
</summary>
<param name="next"></param>
</member>
<member name="M:Admin.NET.Plugin.Pay.Alipay.Middlewares.AlipaySayHelloMiddleware.InvokeAsync(Microsoft.AspNetCore.Http.HttpContext,PluginCore.Interfaces.IPluginFinder)">
<summary>
</summary>
<param name="httpContext"></param>
<param name="pluginFinder">测试是否运行时添加的Middleware是否可以依赖注入</param>
<returns></returns>
</member>
<member name="T:Admin.NET.Plugin.Pay.Alipay.Service.AlipayPluginCoreService">
<summary>
系统动态插件服务
</summary>
</member>
<member name="M:Admin.NET.Plugin.Pay.Alipay.Service.AlipayPluginCoreService.Page">
<summary>
获取动态插件列表
</summary>
<param name="input"></param>
<returns></returns>
</member>
</members>
</doc>

View File

@ -0,0 +1,14 @@
## 说明文档(可选)
- [] 这是一个示例插件
- [x] 感谢使用
## API
- [/SayHello](/SayHello)
- 通过插件在运行时添加 管道Middleware, 并拦截响应
- [/api/plugins/AliWebApi](/api/plugins/AliWebApi)
- 通过插件Controller 响应

View File

@ -0,0 +1,8 @@
{
"PluginId": "Admin.NET.Plugin.Pay.Alipay",
"DisplayName": "Alipay示例",
"Description": "这是支付宝插件",
"Author": "tomny",
"Version": "0.1.0",
"SupportedVersions": [ "0.0.1" ]
}

View File

@ -0,0 +1,3 @@
{
"Hello": "哈哈哈哈哈或或或或或或"
}

View File

@ -0,0 +1,9 @@
* {
margin: 0;
padding: 0;
}
#app {
width: 100%;
color: deepskyblue;
}

View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8" />
<title>HelloWorldPlugin</title>
<!-- 注意:路径, 前置 /plugins/HelloWorldPlugin/ -->
<link href="/plugins/HelloWorldPlugin/css/main.css" rel="stylesheet" />
</head>
<body>
<div id="app">
<h3>HelloWorldPlugin! </h3>
<p>插件的前端文件应当放在 wwwroot 文件夹下</p>
</div>
</body>
</html>

View File

@ -0,0 +1,9 @@
* {
margin: 0;
padding: 0;
}
#app {
width: 100%;
color: deepskyblue;
}

View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8" />
<title>HelloWorldPlugin</title>
<!-- 注意:路径, 前置 /plugins/HelloWorldPlugin/ -->
<link href="/plugins/HelloWorldPlugin/css/main.css" rel="stylesheet" />
</head>
<body>
<div id="app">
<h3>HelloWorldPlugin! </h3>
<p>插件的前端文件应当放在 wwwroot 文件夹下</p>
</div>
</body>
</html>

View File

@ -32,6 +32,24 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Admin.NET.Plugin.PaddleOCR"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Admin.NET.Plugin.WorkWeixin", "Plugins\Admin.NET.Plugin.WorkWeixin\Admin.NET.Plugin.WorkWeixin.csproj", "{12998618-A875-4580-B5B1-0CC50CE85F27}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PluginCore", "PluginCore", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PluginCore", "Plugins\PluginCore\PluginCore\PluginCore.csproj", "{D6A36A77-53EB-222C-B7E7-AA2D81F1A9B0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PluginCore.AspNetCore", "Plugins\PluginCore\PluginCore.AspNetCore\PluginCore.AspNetCore.csproj", "{D66359C8-1F82-669F-A515-5A7F6A65DB8B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PluginCore.IPlugins", "Plugins\PluginCore\PluginCore.IPlugins\PluginCore.IPlugins.csproj", "{9E4C9BFE-E657-F410-9283-CAE6C370F264}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PluginCore.IPlugins.AspNetCore", "Plugins\PluginCore\PluginCore.IPlugins.AspNetCore\PluginCore.IPlugins.AspNetCore.csproj", "{BAB9ACF0-5AEE-290C-6D33-30712B6ADF14}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Test", "Test", "{3F5B6AE4-951D-4358-AF94-288C4D484D48}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Admin.NET.Plugin.Pay.Alipay", "Plugins\PluginCore\Test\Admin.NET.Plugin.Pay.Alipay\Admin.NET.Plugin.Pay.Alipay.csproj", "{0A92F217-3412-053B-AD13-E0018B024DEE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Admin.NET.Plugin.PluginCoreManager", "Plugins\Admin.NET.Plugin.PluginCoreManager\Admin.NET.Plugin.PluginCoreManager.csproj", "{07C1B5BD-0A9F-A112-F257-0C6E4D0CC961}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Admin.NET.Plugin.Core", "Plugins\Admin.NET.Plugin.Core\Admin.NET.Plugin.Core.csproj", "{B78CD743-15E4-4295-1759-277929D38DF0}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -82,6 +100,34 @@ Global
{12998618-A875-4580-B5B1-0CC50CE85F27}.Debug|Any CPU.Build.0 = Debug|Any CPU
{12998618-A875-4580-B5B1-0CC50CE85F27}.Release|Any CPU.ActiveCfg = Release|Any CPU
{12998618-A875-4580-B5B1-0CC50CE85F27}.Release|Any CPU.Build.0 = Release|Any CPU
{D6A36A77-53EB-222C-B7E7-AA2D81F1A9B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D6A36A77-53EB-222C-B7E7-AA2D81F1A9B0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D6A36A77-53EB-222C-B7E7-AA2D81F1A9B0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D6A36A77-53EB-222C-B7E7-AA2D81F1A9B0}.Release|Any CPU.Build.0 = Release|Any CPU
{D66359C8-1F82-669F-A515-5A7F6A65DB8B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D66359C8-1F82-669F-A515-5A7F6A65DB8B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D66359C8-1F82-669F-A515-5A7F6A65DB8B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D66359C8-1F82-669F-A515-5A7F6A65DB8B}.Release|Any CPU.Build.0 = Release|Any CPU
{9E4C9BFE-E657-F410-9283-CAE6C370F264}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9E4C9BFE-E657-F410-9283-CAE6C370F264}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9E4C9BFE-E657-F410-9283-CAE6C370F264}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9E4C9BFE-E657-F410-9283-CAE6C370F264}.Release|Any CPU.Build.0 = Release|Any CPU
{BAB9ACF0-5AEE-290C-6D33-30712B6ADF14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BAB9ACF0-5AEE-290C-6D33-30712B6ADF14}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BAB9ACF0-5AEE-290C-6D33-30712B6ADF14}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BAB9ACF0-5AEE-290C-6D33-30712B6ADF14}.Release|Any CPU.Build.0 = Release|Any CPU
{0A92F217-3412-053B-AD13-E0018B024DEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0A92F217-3412-053B-AD13-E0018B024DEE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0A92F217-3412-053B-AD13-E0018B024DEE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0A92F217-3412-053B-AD13-E0018B024DEE}.Release|Any CPU.Build.0 = Release|Any CPU
{07C1B5BD-0A9F-A112-F257-0C6E4D0CC961}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{07C1B5BD-0A9F-A112-F257-0C6E4D0CC961}.Debug|Any CPU.Build.0 = Debug|Any CPU
{07C1B5BD-0A9F-A112-F257-0C6E4D0CC961}.Release|Any CPU.ActiveCfg = Release|Any CPU
{07C1B5BD-0A9F-A112-F257-0C6E4D0CC961}.Release|Any CPU.Build.0 = Release|Any CPU
{B78CD743-15E4-4295-1759-277929D38DF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B78CD743-15E4-4295-1759-277929D38DF0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B78CD743-15E4-4295-1759-277929D38DF0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B78CD743-15E4-4295-1759-277929D38DF0}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -94,6 +140,15 @@ Global
{9EB9C39E-E14F-443E-9AA3-EE417ABCBC1D} = {76F70D22-8D53-468E-A3B6-1704666A1D71}
{1B106C11-E5BF-44AB-A283-1E948A8BD8C2} = {76F70D22-8D53-468E-A3B6-1704666A1D71}
{12998618-A875-4580-B5B1-0CC50CE85F27} = {76F70D22-8D53-468E-A3B6-1704666A1D71}
{02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {76F70D22-8D53-468E-A3B6-1704666A1D71}
{D6A36A77-53EB-222C-B7E7-AA2D81F1A9B0} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{D66359C8-1F82-669F-A515-5A7F6A65DB8B} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{9E4C9BFE-E657-F410-9283-CAE6C370F264} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{BAB9ACF0-5AEE-290C-6D33-30712B6ADF14} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{3F5B6AE4-951D-4358-AF94-288C4D484D48} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{0A92F217-3412-053B-AD13-E0018B024DEE} = {3F5B6AE4-951D-4358-AF94-288C4D484D48}
{07C1B5BD-0A9F-A112-F257-0C6E4D0CC961} = {76F70D22-8D53-468E-A3B6-1704666A1D71}
{B78CD743-15E4-4295-1759-277929D38DF0} = {76F70D22-8D53-468E-A3B6-1704666A1D71}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {5CD801D7-984A-4F5C-8FA2-211B7A5EA9F3}

View File

@ -0,0 +1,20 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
using PluginCore.IPlugins;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Admin.NET.Plugin.Core.Abstractions.Pay;
public interface IPayPlugin : IPlugin
{
string Name { get; set; }
string Say();
}

View File

@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
<NoWarn>1701;1702;8616;1591;8618;8619;8629;8602;8603;8604;8625;8765</NoWarn>
<DocumentationFile>Admin.NET.Plugin.Core.xml</DocumentationFile>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Const\**" />
<Compile Remove="Extensions\**" />
<Compile Remove="Options\**" />
<EmbeddedResource Remove="Const\**" />
<EmbeddedResource Remove="Extensions\**" />
<EmbeddedResource Remove="Options\**" />
<None Remove="Const\**" />
<None Remove="Extensions\**" />
<None Remove="Options\**" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PluginCore\PluginCore.AspNetCore\PluginCore.AspNetCore.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,8 @@
<?xml version="1.0"?>
<doc>
<assembly>
<name>Admin.NET.Plugin.Core</name>
</assembly>
<members>
</members>
</doc>

View File

@ -0,0 +1 @@


View File

@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
<NoWarn>1701;1702;1591;8632</NoWarn>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>disable</Nullable>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Controllers\AppCenterController.cs" />
<Compile Remove="Controllers\DebugController.cs" />
<Compile Remove="Controllers\PluginsController.cs" />
<Compile Remove="Controllers\PluginWidgetController.cs" />
<Compile Remove="Controllers\UserController.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Admin.NET.Core\Admin.NET.Core.csproj" />
<ProjectReference Include="..\PluginCore\PluginCore.AspNetCore\PluginCore.AspNetCore.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="Elsa.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<Folder Include="Controllers\" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,207 @@
//===================================================
// License: Apache-2.0
// Contributors: yiyungent@gmail.com
// Project: https://moeci.com/PluginCore
// GitHub: https://github.com/yiyungent/PluginCore
//===================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using PluginCore;
using PluginCore.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using PluginCore.AspNetCore.Authorization;
using PluginCore.AspNetCore.ResponseModel;
//using ResponseModel;
namespace PluginCore.AspNetCore.Controllers
{
/// <summary>
/// 应用中心
/// <para>插件</para>
/// </summary>
[Route("api/plugincore/admin/[controller]/[action]")]
// [PluginCoreAdminAuthorize]
[ApiController]
[NonUnify]
public class AppCenterController : ControllerBase
{
#region Fields
private static Dictionary<string, Task> _pluginDownloadTasks;
#endregion
#region Ctor
static AppCenterController()
{
_pluginDownloadTasks = new Dictionary<string, Task>();
}
public AppCenterController()
{
}
#endregion
#region Actions
#region
/// <summary>
/// 插件
/// </summary>
/// <param name="query"></param>
/// <returns></returns>
[HttpGet, HttpPost]
public async Task<ActionResult<BaseResponseModel>> Plugins(string query = "")
{
BaseResponseModel responseDTO = new BaseResponseModel();
IList<PluginRegistryResponseModel> pluginRegistryModels = new List<PluginRegistryResponseModel>();
try
{
// 1. TODO: 从json文件中读取插件订阅源 registry url
string registryUrl = "";
// 2. TODO: 向订阅源发送 http get 获取插件列表信息 eg: http://rem-core-plugins-registry.moeci.com/?query=xxx
IList<string> remotePluginIds = new List<string>();
// 3. 根据本地已有 PluginId 插件情况 状态赋值
PluginConfigModel pluginConfigModel = PluginConfigModelFactory.Create();
// IList<string> localPluginIds = pluginConfigModel.EnabledPlugins.Concat(pluginConfigModel.DisabledPlugins).Concat(pluginConfigModel.UninstalledPlugins).ToList();
IList<string> localPluginIds = PluginPathProvider.AllPluginFolderName();
responseDTO.Code = 1;
responseDTO.Message = "获取远程插件数据成功";
responseDTO.Data = pluginRegistryModels;
}
catch (Exception ex)
{
responseDTO.Code = -1;
responseDTO.Message = "获取远程插件数据失败: " + ex.Message;
responseDTO.Data = pluginRegistryModels;
}
return await Task.FromResult(responseDTO);
}
#endregion
#region
[HttpGet, HttpPost]
public async Task<ActionResult<BaseResponseModel>> DownloadPlugin(string pluginDownloadUrl = "")
{
BaseResponseModel responseDTO = new BaseResponseModel();
#region
if (string.IsNullOrEmpty(pluginDownloadUrl))
{
responseDTO.Code = -1;
responseDTO.Message = "插件下载地址不正确";
return responseDTO;
}
// TODO: 效验是否本地已经存在相同pluginId的插件
#endregion
try
{
// 1.执行下载操作, TODO:存在问题,阻塞对性能不好,但不阻塞又不好通知用户插件下载进度,以及可能存在在插件下载过程中,用户再次点击下载
WebClient webClient = new WebClient();
// TODO: 插件下载文件路径
string pluginDownloadFilePath = "";
//webClient.DownloadFileAsync(new Uri(pluginDownloadFilePath), "");
Task task = webClient.DownloadFileTaskAsync(pluginDownloadUrl, pluginDownloadFilePath);
_pluginDownloadTasks.Add(pluginDownloadUrl, task);
webClient.DownloadFileCompleted += Plugin_DownloadFileCompleted;
webClient.DownloadProgressChanged += Plugin_DownloadProgressChanged;
webClient.Disposed += WebClient_Disposed;
responseDTO.Code = 1;
responseDTO.Message = "开始下载插件";
}
catch (Exception ex)
{
responseDTO.Code = -1;
responseDTO.Message = "下载插件失败: " + ex.Message;
}
return await Task.FromResult(responseDTO);
}
#endregion
#region
[HttpGet, HttpPost]
public async Task<ActionResult<BaseResponseModel>> DownloadPluginProgress()
{
BaseResponseModel responseDTO = new BaseResponseModel();
try
{
responseDTO.Data = new { };
responseDTO.Code = 1;
responseDTO.Message = "获取插件下载进度成功";
}
catch (Exception ex)
{
responseDTO.Code = -1;
responseDTO.Message = "获取插件下载进度失败: " + ex.Message;
}
return await Task.FromResult(responseDTO);
}
#endregion
#endregion
#region Helpers
/// <summary>
/// 插件下载完成
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Plugin_DownloadFileCompleted(object sender, System.ComponentModel.AsyncCompletedEventArgs e)
{
Console.WriteLine("插件下载完成");
// 1.从 _pluginDownloadTasks 中移除
//_pluginDownloadTasks.Remove();
// 2. 解压插件
}
private void Plugin_DownloadProgressChanged(object sender, DownloadProgressChangedEventArgs e)
{
Console.WriteLine($"插件下载进度改变: {e.ProgressPercentage}% {e.BytesReceived}/{e.TotalBytesToReceive}");
}
private void WebClient_Disposed(object sender, EventArgs e)
{
if (sender is WebClient webClient)
{
Console.WriteLine(webClient.BaseAddress);
}
Console.WriteLine(nameof(WebClient_Disposed) + ": " + sender.ToString());
}
#endregion
}
}

View File

@ -0,0 +1,279 @@
using System.Runtime.CompilerServices;
//===================================================
// License: Apache-2.0
// Contributors: yiyungent@gmail.com
// Project: https://moeci.com/PluginCore
// GitHub: https://github.com/yiyungent/PluginCore
//===================================================
using Microsoft.AspNetCore.Mvc;
using PluginCore.AspNetCore.Authorization;
using PluginCore.AspNetCore.ResponseModel;
using PluginCore.Interfaces;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using System.Linq;
using System.Runtime.Loader;
using Microsoft.Extensions.DependencyInjection;
using System.Collections.Concurrent;
using PluginCore.AspNetCore.Extensions;
namespace PluginCore.AspNetCore.Controllers
{
/// <summary>
/// [ASP.NET Core — 依赖注入\_啊晚的博客-CSDN博客\_asp.net core 依赖注入](https://blog.csdn.net/weixin_37648525/article/details/127942292)
/// [ASP.NET Core中的依赖注入3: 服务的注册与提供 - Artech - 博客园](https://www.cnblogs.com/artech/p/asp-net-core-di-register.html)
/// [ASP.NET Core中的依赖注入5: ServiceProvider实现揭秘 【总体设计 】 - Artech - 博客园](https://www.cnblogs.com/artech/p/asp-net-core-di-service-provider-1.html)
/// [dotnet/ServiceProvider.cs at main · dotnet/dotnet](https://github.com/dotnet/dotnet/blob/main/src/runtime/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceProvider.cs)
/// [Net6 DI源码分析Part2 Engine,ServiceProvider - 一身大膘 - 博客园](https://www.cnblogs.com/hts92/p/15800990.html)
/// [【特别的骚气】asp.net core运行时注入服务实现类库热插拔 - 四处观察 - 博客园](https://www.cnblogs.com/1996-Chinese-Chen/p/16154218.html)
///
/// ActivatorUtilities.CreateInstance<PluginCore.IPlugins.IPlugin>(serviceProvider, "test");
/// ActivatorUtilities.GetServiceOrCreateInstance<PluginCore.IPlugins.IPlugin>(serviceProvider);
///
/// </summary>
[Route("api/plugincore/admin/[controller]/[action]")]
//[PluginCoreAdminAuthorize]
[ApiController]
[NonUnify]
public class DebugController : ControllerBase
{
#region Fields
private readonly IPluginContextManager _pluginContextManager;
#endregion
#region Ctor
public DebugController(IPluginContextManager pluginContextManager)
{
_pluginContextManager = pluginContextManager;
}
#endregion
#region Actions
[HttpGet, HttpPost]
public async Task<ActionResult<BaseResponseModel>> PluginContexts()
{
BaseResponseModel responseModel = new BaseResponseModel();
try
{
var pluginContextList = _pluginContextManager.All();
Dictionary<string, List<string>> keyValuePairs = new Dictionary<string, List<string>>();
foreach (var pluginContext in pluginContextList)
{
keyValuePairs.Add($"{pluginContext.GetType().ToString()} - {pluginContext.PluginId} - {pluginContext.GetHashCode()}", pluginContext.Assemblies.Select(m => m.FullName).ToList());
}
responseModel.Code = 1;
responseModel.Message = "success";
responseModel.Data = keyValuePairs;
}
catch (Exception ex)
{
responseModel.Code = -1;
responseModel.Message = "error";
responseModel.Data = ex.ToString();
}
return await Task.FromResult(responseModel);
}
[HttpGet, HttpPost]
public async Task<ActionResult<BaseResponseModel>> AssemblyLoadContexts()
{
BaseResponseModel responseModel = new BaseResponseModel();
try
{
var assemblyLoadContextDefault = AssemblyLoadContext.Default;
var assemblyLoadContextAll = AssemblyLoadContext.All;
var responseDataModel = new AssemblyLoadContextsResponseDataModel();
responseDataModel.Default = new AssemblyLoadContextsResponseDataModel.AssemblyLoadContextModel
{
Name = assemblyLoadContextDefault.Name,
Type = assemblyLoadContextDefault.GetType().ToString(),
Assemblies = assemblyLoadContextDefault.Assemblies.Select(m => new AssemblyModel { FullName = m.FullName, DefinedTypes = m.DefinedTypes.Select(m => m.FullName).ToList() }).ToList()
};
responseDataModel.All = assemblyLoadContextAll.Select(item => new AssemblyLoadContextsResponseDataModel.AssemblyLoadContextModel
{
Name = item.Name,
Type = item.GetType().ToString(),
Assemblies = item.Assemblies.Select(m => new AssemblyModel { FullName = m.FullName, DefinedTypes = m.DefinedTypes.Select(m => m.FullName).ToList() }).ToList()
}).ToList();
responseModel.Code = 1;
responseModel.Message = "success";
responseModel.Data = responseDataModel;
}
catch (Exception ex)
{
responseModel.Code = -1;
responseModel.Message = "error";
responseModel.Data = ex.ToString();
}
return await Task.FromResult(responseModel);
}
[HttpGet, HttpPost]
public async Task<ActionResult<BaseResponseModel>> Assemblies()
{
BaseResponseModel responseModel = new BaseResponseModel();
try
{
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
List<AssemblyModel> assemblyModels = new List<AssemblyModel>();
foreach (var item in assemblies)
{
assemblyModels.Add(new AssemblyModel
{
FullName = item.FullName,
DefinedTypes = item.DefinedTypes.Select(m => m.FullName).ToList()
});
}
responseModel.Code = 1;
responseModel.Message = "success";
responseModel.Data = assemblyModels;
}
catch (Exception ex)
{
responseModel.Code = -1;
responseModel.Message = "error";
responseModel.Data = ex.ToString();
}
return await Task.FromResult(responseModel);
}
[HttpGet, HttpPost]
public async Task<ActionResult<BaseResponseModel>> Services([FromServices] IServiceProvider serviceProvider)
{
BaseResponseModel responseModel = new BaseResponseModel();
try
{
//IServiceProvider serviceProvider = HttpContext.RequestServices;
//var provider = serviceProvider.GetType().GetProperty("RootProvider", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
//var serviceField = provider.GetType().GetField("_realizedServices", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
//var serviceValue = serviceField.GetValue(provider);
//var funcType = serviceField.FieldType.GetGenericArguments()[1].GetGenericArguments()[0];
//ConcurrentDictionary<Type, Func<ServiceProviderEngineScope, object?>> realizedServices = (ConcurrentDictionary<Type, Func<ServiceProviderEngineScope, object?>>)serviceValue;
// 获取所有已经注册的服务
var allService = serviceProvider.GetAllServiceDescriptors();
List<ServiceModel> serviceModels = new List<ServiceModel>();
foreach (var item in allService)
{
serviceModels.Add(new ServiceModel
{
Type = item.Key.ToString(),
ImplementationType = item.Value.ImplementationType?.ToString() ?? "",
Lifetime = item.Value.Lifetime.ToString(),
TypeAssembly = new AssemblyModel
{
FullName = item.Key.Assembly.FullName,
},
ImplementationTypeAssembly = new AssemblyModel
{
FullName = item.Value.ImplementationType?.Assembly?.FullName ?? ""
}
});
}
responseModel.Code = 1;
responseModel.Message = "success";
responseModel.Data = serviceModels;
}
catch (Exception ex)
{
responseModel.Code = -1;
responseModel.Message = "error";
responseModel.Data = ex.ToString();
}
return await Task.FromResult(responseModel);
}
#endregion
public sealed class AssemblyLoadContextsResponseDataModel
{
public AssemblyLoadContextModel Default
{
get; set;
}
public List<AssemblyLoadContextModel> All
{
get; set;
}
public sealed class AssemblyLoadContextModel
{
public string Name
{
get; set;
}
public string Type
{
get; set;
}
public List<AssemblyModel> Assemblies
{
get; set;
}
}
}
public sealed class AssembliesResponseDataModel
{
}
public sealed class ServiceModel
{
public string Type
{
get; set;
}
public string ImplementationType
{
get; set;
}
public string Lifetime
{
get; set;
}
public AssemblyModel TypeAssembly
{
get; set;
}
public AssemblyModel ImplementationTypeAssembly
{
get; set;
}
}
public sealed class AssemblyModel
{
public string FullName
{
get; set;
}
public List<string> DefinedTypes
{
get; set;
}
}
}
}

View File

@ -0,0 +1,91 @@
//===================================================
// License: Apache-2.0
// Contributors: yiyungent@gmail.com
// Project: https://moeci.com/PluginCore
// GitHub: https://github.com/yiyungent/PluginCore
//===================================================
using PluginCore.AspNetCore.ResponseModel;
using PluginCore.Interfaces;
using PluginCore.IPlugins;
namespace PluginCore.AspNetCore.Controllers
{
[Route("api/plugincore/[controller]/[action]")]
[ApiController]
[NonUnify]
public class PluginWidgetController : ControllerBase
{
#region Fields
private readonly IPluginFinder _pluginFinder;
#endregion
#region Ctor
public PluginWidgetController(IPluginFinder pluginFinder)
{
_pluginFinder = pluginFinder;
}
#endregion
#region Actions
#region Widget
/// <summary>
/// Widget
/// </summary>
/// <returns></returns>
[HttpGet, HttpPost]
//public async Task<ActionResult<CommonResponseModel>> Widget(string widgetKey, string extraPars = "")
public async Task<ActionResult> Widget(string widgetKey, string extraPars = "")
{
BaseResponseModel responseModel = new ResponseModel.BaseResponseModel();
string responseData = "";
widgetKey = widgetKey.Trim('"', '\'');
string[] extraParsArr = null;
if (!string.IsNullOrEmpty(extraPars))
{
extraParsArr = extraPars.Split(",", StringSplitOptions.RemoveEmptyEntries);
extraParsArr = extraParsArr.Select(m => m.Trim('"', '\'')).ToArray();
}
StringBuilder sb = new StringBuilder();
sb.AppendLine($"<!-- start:PluginCore.IPlugins.IWidgetPlugin.Widget({widgetKey},{extraPars}) -->");
try
{
List<IWidgetPlugin> plugins = this._pluginFinder.EnablePlugins<IWidgetPlugin>().ToList();
foreach (var item in plugins)
{
string widgetStr = await item.Widget(widgetKey, extraParsArr);
if (!string.IsNullOrEmpty(widgetStr))
{
// TODO: 配合 PluginCoreConfig.PluginWidgetDebug
// TODO: PluginCoreConfig 改为 Options 模式, 避免手动反复读取文件 效率低
//sb.AppendLine($"<!-- {item.GetType().ToString()}: -->");
sb.AppendLine(widgetStr);
}
}
}
catch (Exception ex)
{
Utils.LogUtil.Error<PluginWidgetController>(ex.ToString());
sb.AppendLine($"<!-- Exception: {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}, Details: Console -->");
}
sb.AppendLine($"<!-- end:PluginCore.IPlugins.IWidgetPlugin.Widget({widgetKey},{extraPars}) -->");
responseData = sb.ToString();
responseModel.Code = 1;
responseModel.Message = "Load Widget Success";
responseModel.Data = responseData;
//return await Task.FromResult(responseModel);
return Content(responseData, "text/html;charset=utf-8");
}
#endregion
#endregion
}
}

View File

@ -0,0 +1,680 @@
//===================================================
// License: Apache-2.0
// Contributors: yiyungent@gmail.com
// Project: https://moeci.com/PluginCore
// GitHub: https://github.com/yiyungent/PluginCore
//===================================================
using System;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
//using Core.Common;
//using Framework.Authorization;
using PluginCore;
using PluginCore.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using PluginCore.AspNetCore.Authorization;
using PluginCore.Infrastructure;
using PluginCore.IPlugins;
using PluginCore.AspNetCore.ResponseModel;
using PluginCore.Interfaces;
using PluginCore.AspNetCore.Interfaces;
//using ResponseModel;
namespace PluginCore.AspNetCore.Controllers
{
[Route("api/plugincore/admin/[controller]/[action]")]
// [PluginCoreAdminAuthorize]
[ApiController]
[NonUnify]
public class PluginsController : ControllerBase
{
#region Fields
private readonly IPluginManager _pluginManager;
private readonly IPluginFinder _pluginFinder;
private readonly IPluginApplicationBuilderManager _pluginApplicationBuilderManager;
#endregion
#region Ctor
public PluginsController(IPluginManager pluginManager, IPluginFinder pluginFinder, IPluginApplicationBuilderManager pluginApplicationBuilderManager)
{
_pluginManager = pluginManager;
_pluginFinder = pluginFinder;
_pluginApplicationBuilderManager = pluginApplicationBuilderManager;
}
#endregion
#region Actions
#region
/// <summary>
/// 加载插件列表
/// </summary>
/// <param name="status">插件状态</param>
/// <returns></returns>
[HttpGet, HttpPost]
public async Task<ActionResult<BaseResponseModel>> List(string status = "all")
{
BaseResponseModel responseData = new ResponseModel.BaseResponseModel();
var pluginConfigModel = PluginConfigModelFactory.Create();
// 获取所有插件信息
IList<PluginInfoModel> pluginInfoModels = PluginInfoModelFactory.CreateAll();
IList<PluginInfoResponseModel> responseModels = new List<PluginInfoResponseModel>();
string[] enablePluginIds = _pluginFinder.EnablePluginIds().ToArray();
// 添加插件状态
responseModels = PluginInfoModelToResponseModel(pluginInfoModels, pluginConfigModel, enablePluginIds);
#region
switch (status.ToLower())
{
case "all":
break;
case "enabled":
responseModels = responseModels.Where(m => m.Status == PluginStatus.Enabled).ToList();
break;
case "disabled":
responseModels = responseModels.Where(m => m.Status == PluginStatus.Disabled).ToList();
break;
default:
break;
}
#endregion
responseData.Code = 1;
responseData.Message = "加载插件列表成功";
responseData.Data = responseModels;
return await Task.FromResult(responseData);
}
#endregion
#region
[HttpGet, HttpPost]
public async Task<ActionResult<BaseResponseModel>> Uninstall(string pluginId)
{
BaseResponseModel responseData = new BaseResponseModel();
pluginId = pluginId.Trim();
var pluginConfigModel = PluginConfigModelFactory.Create();
// 卸载插件 必须 先禁用插件
#region
if (pluginConfigModel.EnabledPlugins.Contains(pluginId))
{
responseData.Code = -1;
responseData.Message = "卸载失败: 请先禁用此插件";
return await Task.FromResult(responseData);
}
string pluginDirStr= Path.Combine(PluginPathProvider.PluginsRootPath(), pluginId);
string pluginWwwrootDirStr = Path.Combine(PluginPathProvider.PluginsWwwRootDir(), pluginId);
if (!Directory.Exists(pluginDirStr) && !Directory.Exists(pluginWwwrootDirStr))
{
responseData.Code = -2;
responseData.Message = "卸载失败: 此插件不存在";
return await Task.FromResult(responseData);
}
#endregion
try
{
// PS:卸载插件必须先禁用插件所以此时插件LoadContext已被移除释放(插件Assemblies已被释放), 此处不需移除LoadContext
// 1.删除物理文件
var pluginDir = new DirectoryInfo(pluginDirStr);
if (pluginDir.Exists) {
pluginDir.Delete(true);
}
// 虽然 已禁用 时 pluginWwwrootDirStr/pluginId 已删除, 但为确保, 还是再删除一次
var pluginWwwrootDir = new DirectoryInfo(pluginWwwrootDirStr);
if (pluginWwwrootDir.Exists) {
pluginWwwrootDir.Delete(true);
}
responseData.Code = 1;
responseData.Message = "卸载成功";
}
catch (Exception ex)
{
responseData.Code = -1;
responseData.Message = "卸载失败: " + ex.Message;
}
return await Task.FromResult(responseData);
}
#endregion
#region
[HttpGet, HttpPost]
public async Task<ActionResult<BaseResponseModel>> Enable(string pluginId)
{
BaseResponseModel responseData = new BaseResponseModel();
var pluginConfigModel = PluginConfigModelFactory.Create();
// 效验是否存在于 已禁用插件列表
#region
pluginId = pluginId.Trim();
var pluginDir = new DirectoryInfo(Path.Combine(PluginPathProvider.PluginsRootPath(), pluginId));
if (pluginDir != null && !pluginDir.Exists)
{
responseData.Code = -1;
responseData.Message = "启用失败: 此插件不存在";
return await Task.FromResult(responseData);
}
string[] enablePluginIds = _pluginFinder.EnablePluginIds().ToArray();
if (enablePluginIds.Contains(pluginId)) {
responseData.Code = -2;
responseData.Message = "启用失败: 此插件已启用";
return await Task.FromResult(responseData);
}
#endregion
try
{
// 1. 创建插件程序集加载上下文, 添加到 PluginsLoadContexts
_pluginManager.LoadPlugin(pluginId);
// 2. 添加到 pluginConfigModel.EnabledPlugins
pluginConfigModel.EnabledPlugins.Add(pluginId);
// 4.保存到 plugin.config.json
PluginConfigModelFactory.Save(pluginConfigModel);
// 5. 找到此插件实例
IPlugin plugin = _pluginFinder.Plugin(pluginId);
if (plugin == null)
{
// 7.启用不成功, 回滚插件状态: (1)释放插件上下文 (2)更新 plugin.config.json
try
{
_pluginManager.UnloadPlugin(pluginId);
}
catch (Exception ex)
{ }
// 从 pluginConfigModel.EnabledPlugins 移除
pluginConfigModel.EnabledPlugins.Remove(pluginId);
// 保存到 plugin.config.json
PluginConfigModelFactory.Save(pluginConfigModel);
responseData.Code = -1;
responseData.Message = "启用失败: 此插件不存在";
return await Task.FromResult(responseData);
}
// 6.调取插件的 AfterEnable(), 插件开发者可在此回收资源
var pluginEnableResult = plugin.AfterEnable();
if (!pluginEnableResult.IsSuccess)
{
// 7.启用不成功, 回滚插件状态: (1)释放插件上下文 (2)更新 plugin.config.json
try
{
_pluginManager.UnloadPlugin(pluginId);
}
catch (Exception ex)
{ }
// 从 pluginConfigModel.EnabledPlugins 移除
pluginConfigModel.EnabledPlugins.Remove(pluginId);
// 保存到 plugin.config.json
PluginConfigModelFactory.Save(pluginConfigModel);
responseData.Code = -1;
responseData.Message = "启用失败: 来自插件的错误信息: " + pluginEnableResult.Message;
return await Task.FromResult(responseData);
}
// 7. ReBuild
this._pluginApplicationBuilderManager.ReBuild();
// 8. 尝试复制 插件下的 wwwroot 到 Plugins_wwwroot
string wwwRootDir = PluginPathProvider.WwwRootDir(pluginId);
if (Directory.Exists(wwwRootDir))
{
string targetDir = PluginPathProvider.PluginWwwRootDir(pluginId);
Utils.FileUtil.CopyFolder(wwwRootDir, targetDir);
}
responseData.Code = 1;
responseData.Message = "启用成功";
}
catch (Exception ex)
{
responseData.Code = -2;
responseData.Message = "启用失败: " + ex.Message;
}
return await Task.FromResult(responseData);
}
#endregion
#region
[HttpGet, HttpPost]
public async Task<ActionResult<BaseResponseModel>> Disable(string pluginId)
{
BaseResponseModel responseData = new BaseResponseModel();
#region
pluginId = pluginId.Trim();
var pluginConfigModel = PluginConfigModelFactory.Create();
// string[] enablePluginIds = _pluginFinder.EnablePluginIds().ToArray();
// // 效验是否存在于 已启用插件列表
// if (!enablePluginIds.Contains(pluginId))
// {
// responseData.Code = -1;
// responseData.Message = "禁用失败: 此插件不存在, 或未启用";
// return await Task.FromResult(responseData);
// }
#endregion
try
{
// 1. 找到此插件实例
IPlugin plugin = _pluginFinder.Plugin(pluginId);
if (plugin == null)
{
responseData.Code = -1;
responseData.Message = "禁用失败: 此插件不存在, 或未启用";
return await Task.FromResult(responseData);
}
try
{
// 2.调取插件的 BeforeDisable(), 插件开发者可在此回收资源
var pluginDisableResult = plugin.BeforeDisable();
if (!pluginDisableResult.IsSuccess)
{
responseData.Code = -1;
responseData.Message = "禁用失败: 来自插件的错误信息: " + pluginDisableResult.Message;
return await Task.FromResult(responseData);
}
// 3.移除插件对应的程序集加载上下文
_pluginManager.UnloadPlugin(pluginId);
// 3.1. ReBuild
this._pluginApplicationBuilderManager.ReBuild();
if (pluginConfigModel.EnabledPlugins.Contains(pluginId)) {
// 4.从 pluginConfigModel.EnabledPlugins 移除
pluginConfigModel.EnabledPlugins.Remove(pluginId);
// 5.保存到 plugin.config.json
PluginConfigModelFactory.Save(pluginConfigModel);
}
}
catch (Exception ex)
{
Utils.LogUtil.Error(ex.ToString());
responseData.Code = -1;
responseData.Message = "禁用失败: 此插件不存在, 或未启用";
return await Task.FromResult(responseData);
}
// 7. 尝试移除 Plugins_wwwroot/PluginId
string pluginWwwRootDir = PluginPathProvider.PluginWwwRootDir(pluginId);
if (Directory.Exists(pluginWwwRootDir))
{
Directory.Delete(pluginWwwRootDir, true);
}
responseData.Code = 1;
responseData.Message = "禁用成功";
}
catch (Exception ex)
{
responseData.Code = -2;
responseData.Message = "禁用失败: " + ex.Message;
}
return await Task.FromResult(responseData);
}
#endregion
#region
/// <summary>
/// 上传插件
/// </summary>
/// <param name="file">注意: 参数名一定为 file 对应前端传过来时以 file 为名</param>
/// <returns></returns>
[HttpGet, HttpPost]
public async Task<ActionResult<BaseResponseModel>> Upload([FromForm] IFormFile file)
{
BaseResponseModel responseData = new BaseResponseModel();
#region
if (file == null)
{
responseData.Code = -1;
responseData.Message = "上传的文件不能为空";
return responseData;
}
//文件后缀
string fileExtension = Path.GetExtension(file.FileName);//获取文件格式,拓展名
// 类型标记
UploadFileType uploadFileType = UploadFileType.NoAllowedType;
switch (fileExtension)
{
case ".zip":
uploadFileType = UploadFileType.Zip;
break;
case ".nupkg":
uploadFileType = UploadFileType.Nupkg;
break;
}
if (fileExtension != ".zip" && fileExtension != ".nupkg")
{
responseData.Code = -1;
// nupkg 其实就是 zip
responseData.Message = "只能上传 zip 或 nupkg 格式文件";
return responseData;
}
// PluginCore.AspNetCore-v1.0.2 起 不再限制插件上传大小
//判断文件大小
//var fileSize = file.Length;
//if (fileSize > 1024 * 1024 * 5) // 5M
//{
// responseData.Code = -1;
// responseData.Message = "上传的文件不能大于5MB";
// return responseData;
//}
#endregion
try
{
// 1.先上传到 临时插件上传目录, 用Guid.zip作为保存文件名
string tempZipFilePath = Path.Combine(PluginPathProvider.TempPluginUploadDir(), Guid.NewGuid() + ".zip");
using (var fs = System.IO.File.Create(tempZipFilePath))
{
file.CopyTo(fs); //将上传的文件文件流复制到fs中
fs.Flush();//清空文件流
}
// 2.解压
bool isDecomparessSuccess = false;
if (uploadFileType == UploadFileType.Zip)
{
isDecomparessSuccess = Utils.ZipHelper.DecomparessFile(tempZipFilePath, tempZipFilePath.Replace(".zip", ""));
}
else if (uploadFileType == UploadFileType.Nupkg)
{
isDecomparessSuccess = NupkgService.DecomparessFile(tempZipFilePath, tempZipFilePath.Replace(".zip", ""));
}
// 3.删除原压缩包
System.IO.File.Delete(tempZipFilePath);
if (!isDecomparessSuccess)
{
responseData.Code = -1;
responseData.Message = "解压插件压缩包失败";
return responseData;
}
// 4.读取其中的info.json, 获取 PluginId 值
PluginInfoModel pluginInfoModel = PluginInfoModelFactory.ReadPluginDir(tempZipFilePath.Replace(".zip", ""));
if (pluginInfoModel == null || string.IsNullOrEmpty(pluginInfoModel.PluginId))
{
// 记得删除已不再需要的临时插件文件夹
Directory.Delete(tempZipFilePath.Replace(".zip", ""), true);
responseData.Code = -1;
responseData.Message = "不合法的插件";
return responseData;
}
string pluginId = pluginInfoModel.PluginId;
// 5.检索 此 PluginId 是否本地插件已存在
var pluginConfigModel = PluginConfigModelFactory.Create();
// 本地已经存在的 PluginId
IList<string> localExistPluginIds = PluginPathProvider.AllPluginFolderName();
if (localExistPluginIds.Contains(pluginId))
{
// 记得删除已不再需要的临时插件文件夹
Directory.Delete(tempZipFilePath.Replace(".zip", ""), true);
responseData.Code = -1;
responseData.Message = $"本地已有此插件 (PluginId: {pluginId}), 请前往插件列表删除后, 再上传";
return responseData;
}
// 6.本地无此插件 -> 移动插件文件夹到 Plugins 下, 并以 PluginId 为插件文件夹名
string pluginsRootPath = PluginPathProvider.PluginsRootPath();
string newPluginDir = Path.Combine(pluginsRootPath, pluginId);
Directory.Move(tempZipFilePath.Replace(".zip", ""), newPluginDir);
// 7. 放入 Plugins 中, 默认为 已禁用
responseData.Code = 1;
responseData.Message = $"上传插件成功 (PluginId: {pluginId})";
}
catch (Exception ex)
{
responseData.Code = -1;
responseData.Message = "上传插件失败: " + ex.Message;
ex = ex.InnerException;
while (ex != null)
{
responseData.Message += " - " + ex.InnerException.Message;
ex = ex.InnerException;
}
}
return await Task.FromResult(responseData);
}
#endregion
#region
[HttpGet, HttpPost]
public async Task<ActionResult<BaseResponseModel>> Details(string pluginId)
{
BaseResponseModel responseData = new BaseResponseModel();
try
{
#region
pluginId = pluginId.Trim();
var pluginConfigModel = PluginConfigModelFactory.Create();
string[] localPluginIds = PluginPathProvider.AllPluginFolderName().ToArray();
if (!localPluginIds.Contains(pluginId))
{
responseData.Code = -1;
responseData.Message = $"查看详细失败: 不存在 {pluginId} 插件";
return await Task.FromResult(responseData);
}
#endregion
PluginInfoModel pluginInfoModel = PluginInfoModelFactory.Create(pluginId);
string[] enablePluginIds = _pluginFinder.EnablePluginIds().ToArray();
PluginInfoResponseModel pluginInfoResponseModel = PluginInfoModelToResponseModel(new List<PluginInfoModel>() { pluginInfoModel }, pluginConfigModel, enablePluginIds).FirstOrDefault();
responseData.Code = 1;
responseData.Message = "查看详细成功";
responseData.Data = pluginInfoResponseModel;
}
catch (Exception ex)
{
responseData.Code = -1;
responseData.Message = "查看详细失败: " + ex.Message;
}
return await Task.FromResult(responseData);
}
#endregion
#region
[HttpGet, HttpPost]
public async Task<ActionResult<BaseResponseModel>> Readme(string pluginId)
{
BaseResponseModel responseData = new BaseResponseModel();
try
{
#region
pluginId = pluginId.Trim();
// var pluginConfigModel = PluginConfigModelFactory.Create();
string[] localPluginIds = PluginPathProvider.AllPluginFolderName().ToArray();
if (!localPluginIds.Contains(pluginId))
{
responseData.Code = -1;
responseData.Message = $"查看文档失败: 不存在 {pluginId} 插件";
return await Task.FromResult(responseData);
}
#endregion
PluginReadmeModel readmeModel = PluginReadmeModelFactory.Create(pluginId);
PluginReadmeResponseModel readmeResponseModel = new PluginReadmeResponseModel();
readmeResponseModel.Content = readmeModel?.Content ?? "";
readmeResponseModel.PluginId = pluginId;
responseData.Code = 1;
responseData.Message = "查看文档成功";
responseData.Data = readmeResponseModel;
}
catch (Exception ex)
{
responseData.Code = -1;
responseData.Message = "查看文档失败: " + ex.Message;
}
return await Task.FromResult(responseData);
}
#endregion
#region
[HttpGet]
public async Task<ActionResult<BaseResponseModel>> Settings(string pluginId)
{
BaseResponseModel responseData = new BaseResponseModel();
try
{
#region
pluginId = pluginId.Trim();
// var pluginConfigModel = PluginConfigModelFactory.Create();
string[] localPluginIds = PluginPathProvider.AllPluginFolderName().ToArray();
if (!localPluginIds.Contains(pluginId))
{
responseData.Code = -1;
responseData.Message = $"查看设置失败: 不存在 {pluginId} 插件";
return await Task.FromResult(responseData);
}
#endregion
string settingsJsonStr = PluginSettingsModelFactory.Create(pluginId);
responseData.Code = 1;
responseData.Message = "查看设置成功";
responseData.Data = settingsJsonStr ?? "无设置项";
}
catch (Exception ex)
{
responseData.Code = -1;
responseData.Message = "查看设置失败: " + ex.Message;
}
return await Task.FromResult(responseData);
}
[HttpPost]
public async Task<ActionResult<BaseResponseModel>> Settings(PluginSettingsInputModel inputModel)
{
BaseResponseModel responseData = new BaseResponseModel();
try
{
#region
inputModel.PluginId = inputModel.PluginId.Trim();
// var pluginConfigModel = PluginConfigModelFactory.Create();
string[] localPluginIds = PluginPathProvider.AllPluginFolderName().ToArray();
if (!localPluginIds.Contains(inputModel.PluginId))
{
responseData.Code = -1;
responseData.Message = $"设置失败: 不存在 {inputModel.PluginId} 插件";
return await Task.FromResult(responseData);
}
#endregion
inputModel.Data = inputModel.Data ?? "";
PluginSettingsModelFactory.Save(pluginSettingsJsonStr: inputModel.Data, pluginId: inputModel.PluginId);
responseData.Code = 1;
responseData.Message = "设置成功";
responseData.Data = inputModel.Data;
}
catch (Exception ex)
{
responseData.Code = -1;
responseData.Message = "设置失败: " + ex.Message;
}
return await Task.FromResult(responseData);
}
#endregion
#endregion
#region Helpers
[NonAction]
private IList<PluginInfoResponseModel> PluginInfoModelToResponseModel(IList<PluginInfoModel> pluginInfoModels, PluginConfigModel pluginConfigModel, string[] enablePluginIds)
{
// 获取 Plugins 下所有插件
// DirectoryInfo pluginsDir = new DirectoryInfo(PluginPathProvider.PluginsRootPath());
// List<string> pluginIds = pluginsDir?.GetDirectories()?.Select(m => m.Name)?.ToList() ?? new List<string>();
IList<PluginInfoResponseModel> responseModels = new List<PluginInfoResponseModel>();
#region
foreach (var model in pluginInfoModels)
{
PluginInfoResponseModel responseModel = new PluginInfoResponseModel();
responseModel.Author = model.Author;
responseModel.Description = model.Description;
responseModel.DisplayName = model.DisplayName;
responseModel.PluginId = model.PluginId;
responseModel.SupportedVersions = model.SupportedVersions;
responseModel.Version = model.Version;
responseModel.DependPlugins = model.DependPlugins;
if (pluginConfigModel.EnabledPlugins.Contains(model.PluginId) && !enablePluginIds.Contains(model.PluginId)) {
// 错误情况: 配置 标识 已启用, 但实际没有启用成功
pluginConfigModel.EnabledPlugins.Remove(model.PluginId);
PluginConfigModelFactory.Save(pluginConfigModel);
responseModel.Status = PluginStatus.Disabled;
} else if(!pluginConfigModel.EnabledPlugins.Contains(model.PluginId) && enablePluginIds.Contains(model.PluginId))
{
// 错误情况: 配置没有标识 已启用, 但实际 已启用
pluginConfigModel.EnabledPlugins.Add(model.PluginId);
PluginConfigModelFactory.Save(pluginConfigModel);
responseModel.Status = PluginStatus.Enabled;
} else if (pluginConfigModel.EnabledPlugins.Contains(model.PluginId) && enablePluginIds.Contains(model.PluginId))
{
responseModel.Status = PluginStatus.Enabled;
}
else
{
responseModel.Status = PluginStatus.Disabled;
}
responseModels.Add(responseModel);
}
#endregion
return responseModels;
}
public enum UploadFileType
{
NoAllowedType = 0,
Zip = 1,
Nupkg = 2
}
#endregion
}
}

View File

@ -0,0 +1,204 @@
//===================================================
// License: Apache-2.0
// Contributors: yiyungent@gmail.com
// Project: https://moeci.com/PluginCore
// GitHub: https://github.com/yiyungent/PluginCore
//===================================================
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using PluginCore.AspNetCore.Authorization;
using PluginCore.Config;
using PluginCore.AspNetCore.RequestModel.User;
using PluginCore.AspNetCore.ResponseModel;
using Microsoft.AspNetCore.Authorization;
using Admin.NET.Core.Service;
using Admin.NET.Core;
using Furion.DataEncryption;
using Furion.FriendlyException;
using Lazy.Captcha.Core;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel;
namespace PluginCore.AspNetCore.Controllers
{
[Route("api/plugincore/admin/[controller]/[action]")]
[ApiController]
[NonUnify]
public class UserController : ControllerBase
{
public string RemoteFronted
{
get
{
return PluginCore.Config.PluginCoreConfigFactory.Create().RemoteFrontend;
}
}
private readonly IUserManager _userManager;
private readonly SqlSugarRepository<SysUser> _sysUserRep;
private readonly SysOrgService _sysOrgService;
private readonly SysUserExtOrgService _sysUserExtOrgService;
private readonly SysUserRoleService _sysUserRoleService;
private readonly SysConfigService _sysConfigService;
public UserController(IUserManager userManager,
SqlSugarRepository<SysUser> sysUserRep,
SysOrgService sysOrgService,
SysUserExtOrgService sysUserExtOrgService,
SysUserRoleService sysUserRoleService,
SysConfigService sysConfigService)
{
_userManager = userManager;
_sysUserRep = sysUserRep;
_sysOrgService = sysOrgService;
_sysUserExtOrgService = sysUserExtOrgService;
_sysUserRoleService = sysUserRoleService;
_sysConfigService = sysConfigService;
}
/// <summary>
/// 登录系统
/// </summary>
/// <param name="input"></param>
/// <remarks>用户名/密码superadmin/123456</remarks>
/// <returns></returns>
[AllowAnonymous]
[HttpGet, HttpPost]
[DisplayName("登录系统")]
public async Task<ActionResult<BaseResponseModel>> Login([FromBody] LoginRequestModel input)
{
BaseResponseModel responseModel = new BaseResponseModel();
// 账号是否存在
var user = await _sysUserRep.AsQueryable().Includes(t => t.SysOrg).Filter(null, true).FirstAsync(u => u.Account.Equals(input.UserName));
_ = user ?? throw Oops.Oh(ErrorCodeEnum.D0009);
// 账号是否被冻结
if (user.Status == StatusEnum.Disable)
throw Oops.Oh(ErrorCodeEnum.D1017);
// 租户是否被禁用
var tenant = await _sysUserRep.ChangeRepository<SqlSugarRepository<SysTenant>>().GetFirstAsync(u => u.Id == user.TenantId);
if (tenant != null && tenant.Status == StatusEnum.Disable)
throw Oops.Oh(ErrorCodeEnum.Z1003);
// 密码是否正确
if (CryptogramUtil.CryptoType == CryptogramEnum.MD5.ToString())
{
if (user.Password != MD5Encryption.Encrypt(input.Password))
throw Oops.Oh(ErrorCodeEnum.D1000);
}
else
{
if (CryptogramUtil.Decrypt(user.Password) != input.Password)
throw Oops.Oh(ErrorCodeEnum.D1000);
}
var tokenExpire = await _sysConfigService.GetTokenExpire();
var refreshTokenExpire = await _sysConfigService.GetRefreshTokenExpire();
// 生成Token令牌
var accessToken = JWTEncryption.Encrypt(new Dictionary<string, object>
{
{ ClaimConst.UserId, user.Id },
{ ClaimConst.TenantId, user.TenantId },
{ ClaimConst.Account, user.Account },
{ ClaimConst.RealName, user.RealName },
{ ClaimConst.AccountType, user.AccountType },
{ ClaimConst.OrgId, user.OrgId },
{ ClaimConst.OrgName, user.SysOrg?.Name },
{ ClaimConst.OrgType, user.SysOrg?.OrgType },
}, tokenExpire);
// 生成刷新Token令牌
var refreshToken = JWTEncryption.GenerateRefreshToken(accessToken, refreshTokenExpire);
responseModel.Code = 1;
responseModel.Message = "登录成功";
responseModel.Data = new
{
token = accessToken,
userName = user.NickName,
RefreshToken = refreshToken
};
return await Task.FromResult(responseModel);
}
[HttpGet, HttpPost]
public async Task<ActionResult<BaseResponseModel>> Logout()
{
BaseResponseModel responseModel = new BaseResponseModel()
{
Code = 1,
Message = "退出登录成功"
};
return await Task.FromResult(responseModel);
}
[HttpGet, HttpPost]
public async Task<ActionResult<BaseResponseModel>> Info()
{
BaseResponseModel responseModel = new BaseResponseModel();
try
{
string adminUserName = PluginCoreConfigFactory.Create().Admin.UserName;
responseModel.Code = 1;
responseModel.Message = "成功";
responseModel.Data = new
{
name = adminUserName,
//avatar = this.RemoteFronted + "/images/avatar.gif"
avatar = ""
};
}
catch (Exception ex)
{
responseModel.Code = -1;
responseModel.Message = "失败: " + ex.Message;
}
return await Task.FromResult(responseModel);
}
[HttpGet, HttpPost]
public async Task<ActionResult<BaseResponseModel>> Update([FromBody] UpdateRequestModel requestModel)
{
BaseResponseModel responseModel = new BaseResponseModel();
try
{
PluginCoreConfig pluginCoreConfig = PluginCoreConfigFactory.Create();
pluginCoreConfig.Admin.UserName = requestModel.UserName;
pluginCoreConfig.Admin.Password = requestModel.Password;
PluginCoreConfigFactory.Save(pluginCoreConfig);
responseModel.Code = 1;
responseModel.Message = "修改成功, 需要重新登录";
}
catch (Exception ex)
{
responseModel.Code = -1;
responseModel.Message = "失败: " + ex.Message;
}
return await Task.FromResult(responseModel);
}
}
}

View File

@ -0,0 +1,73 @@
// 麻省理工学院许可证
//
// 版权所有 (c) 2021-2023 zuohuaijun大名科技天津有限公司 联系电话/微信18020030720 QQ515096995
//
// 特此免费授予获得本软件的任何人以处理本软件的权利,但须遵守以下条件:在所有副本或重要部分的软件中必须包括上述版权声明和本许可声明。
//
// 软件按“原样”提供,不提供任何形式的明示或暗示的保证,包括但不限于对适销性、适用性和非侵权的保证。
// 在任何情况下,作者或版权持有人均不对任何索赔、损害或其他责任负责,无论是因合同、侵权或其他方式引起的,与软件或其使用或其他交易有关。
namespace Admin.NET.Plugin.PluginCoreManager;
/// <summary>
/// 系统动态插件表
/// </summary>
[SugarTable(null, "系统动态插件表")]
[SysTable]
public class SysPluginCore : EntityTenant
{
/// <summary>
/// 插件ID
/// </summary>
[SugarColumn(ColumnDescription = "插件ID", Length = 128)]
[Required]
public virtual string PluginId { get; set; }
/// <summary>
/// 名称
/// </summary>
[SugarColumn(ColumnDescription = "名称", Length = 64)]
[Required, MaxLength(64)]
public virtual string DisplayName { get; set; }
/// <summary>
/// 作者
/// </summary>
[SugarColumn(ColumnDescription = "作者", Length = 64)]
[Required, MaxLength(64)]
public virtual string Author { get; set; }
/// <summary>
/// 版本
/// </summary>
[SugarColumn(ColumnDescription = "版本", Length = 64)]
[Required, MaxLength(64)]
public virtual string Version { get; set; }
/// <summary>
/// 描述
/// </summary>
[SugarColumn(ColumnDescription = "描述", Length = 512)]
[MaxLength(512)]
public string? Description { get; set; }
/// <summary>
/// 排序
/// </summary>
[SugarColumn(ColumnDescription = "排序")]
public int OrderNo { get; set; } = 100;
/// <summary>
/// 状态
/// </summary>
[SugarColumn(ColumnDescription = "状态")]
public StatusEnum Status { get; set; } = StatusEnum.Enable;
/// <summary>
/// 备注
/// </summary>
[SugarColumn(ColumnDescription = "备注", Length = 128)]
[MaxLength(128)]
public string? Remark { get; set; }
}

View File

@ -0,0 +1,25 @@
// 麻省理工学院许可证
//
// 版权所有 (c) 2021-2023 zuohuaijun大名科技天津有限公司 联系电话/微信18020030720 QQ515096995
//
// 特此免费授予获得本软件的任何人以处理本软件的权利,但须遵守以下条件:在所有副本或重要部分的软件中必须包括上述版权声明和本许可声明。
//
// 软件按“原样”提供,不提供任何形式的明示或暗示的保证,包括但不限于对适销性、适用性和非侵权的保证。
// 在任何情况下,作者或版权持有人均不对任何索赔、损害或其他责任负责,无论是因合同、侵权或其他方式引起的,与软件或其使用或其他交易有关。
global using Admin.NET.Core;
global using Admin.NET.Core.Service;
global using Furion;
global using Furion.DependencyInjection;
global using Furion.DynamicApiController;
global using Furion.FriendlyException;
global using Mapster;
global using Microsoft.AspNetCore.Http;
global using Microsoft.AspNetCore.Mvc;
global using Microsoft.Extensions.DependencyInjection;
global using Newtonsoft.Json;
global using SqlSugar;
global using System.ComponentModel;
global using System.ComponentModel.DataAnnotations;
global using System.Data;
global using System.Linq.Dynamic.Core;
global using System.Text;

View File

@ -0,0 +1,39 @@
// 麻省理工学院许可证
//
// 版权所有 (c) 2021-2023 zuohuaijun大名科技天津有限公司 联系电话/微信18020030720 QQ515096995
//
// 特此免费授予获得本软件的任何人以处理本软件的权利,但须遵守以下条件:在所有副本或重要部分的软件中必须包括上述版权声明和本许可声明。
//
// 软件按“原样”提供,不提供任何形式的明示或暗示的保证,包括但不限于对适销性、适用性和非侵权的保证。
// 在任何情况下,作者或版权持有人均不对任何索赔、损害或其他责任负责,无论是因合同、侵权或其他方式引起的,与软件或其使用或其他交易有关。
namespace Admin.NET.Plugin.PluginCoreManager;
/// <summary>
/// 系统菜单表种子数据
/// </summary>
[IncreSeed]
public class SysMenu_PluginCore_SeedData : ISqlSugarEntitySeedData<SysMenu>
{
/// <summary>
/// 种子数据
/// </summary>
/// <returns></returns>
public IEnumerable<SysMenu> HasData()
{
return new[]
{
// 建议此处Id范围之间放置具体业务应用菜单
new SysMenu{ Id=1310000000802, Pid=1310000000301, Title="应用插件", Path="/platform/plugincore", Name="plugincore", Component="/system/plugincore/index", Icon="ele-TurnOff", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=210 },
new SysMenu{ Id=1310000000803, Pid=1310000000802, Title="启用", Permission="sysPluginCore/enable", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=212 },
new SysMenu{ Id=1310000000804, Pid=1310000000802, Title="卸载", Permission="sysPluginCore/delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=213 },
new SysMenu{ Id=1310000000805, Pid=1310000000802, Title="禁用", Permission="sysPluginCore/disable", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=214 },
new SysMenu{ Id=1310000000806, Pid=1310000000802, Title="详细", Permission="sysPluginCore/details", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=215 },
new SysMenu{ Id=1310000000807, Pid=1310000000802, Title="文档", Permission="sysPluginCore/readme", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=216 },
new SysMenu{ Id=1310000000808, Pid=1310000000802, Title="设置", Permission="sysPluginCore/setting", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=217 },
};
}
}

View File

@ -0,0 +1,39 @@
// 麻省理工学院许可证
//
// 版权所有 (c) 2021-2023 zuohuaijun大名科技天津有限公司 联系电话/微信18020030720 QQ515096995
//
// 特此免费授予获得本软件的任何人以处理本软件的权利,但须遵守以下条件:在所有副本或重要部分的软件中必须包括上述版权声明和本许可声明。
//
// 软件按“原样”提供,不提供任何形式的明示或暗示的保证,包括但不限于对适销性、适用性和非侵权的保证。
// 在任何情况下,作者或版权持有人均不对任何索赔、损害或其他责任负责,无论是因合同、侵权或其他方式引起的,与软件或其使用或其他交易有关。
namespace Admin.NET.Core;
/// <summary>
/// 系统角色菜单表种子数据
/// </summary>
[IncreSeed]
public class SysRoleMenu_PluginCore_SeedData : ISqlSugarEntitySeedData<SysRoleMenu>
{
/// <summary>
/// 种子数据
/// </summary>
/// <returns></returns>
public IEnumerable<SysRoleMenu> HasData()
{
return new[]
{
// 应用插件
new SysRoleMenu{ Id=1300000000831, RoleId=1300000000101, MenuId=1310000000802 },
new SysRoleMenu{ Id=1300000000832, RoleId=1300000000101, MenuId=1310000000803 },
new SysRoleMenu{ Id=1300000000833, RoleId=1300000000101, MenuId=1310000000803 },
new SysRoleMenu{ Id=1300000000834, RoleId=1300000000101, MenuId=1310000000804 },
new SysRoleMenu{ Id=1300000000835, RoleId=1300000000101, MenuId=1310000000805 },
new SysRoleMenu{ Id=1300000000836, RoleId=1300000000101, MenuId=1310000000806 },
new SysRoleMenu{ Id=1300000000837, RoleId=1300000000101, MenuId=1310000000807 },
new SysRoleMenu{ Id=1300000000830, RoleId=1300000000101, MenuId=1310000000808 },
};
}
}

View File

@ -0,0 +1,43 @@
// 麻省理工学院许可证
//
// 版权所有 (c) 2021-2023 zuohuaijun大名科技天津有限公司 联系电话/微信18020030720 QQ515096995
//
// 特此免费授予获得本软件的任何人以处理本软件的权利,但须遵守以下条件:在所有副本或重要部分的软件中必须包括上述版权声明和本许可声明。
//
// 软件按“原样”提供,不提供任何形式的明示或暗示的保证,包括但不限于对适销性、适用性和非侵权的保证。
// 在任何情况下,作者或版权持有人均不对任何索赔、损害或其他责任负责,无论是因合同、侵权或其他方式引起的,与软件或其使用或其他交易有关。
namespace Admin.NET.Plugin.PluginCoreManager.Service.Plugin.Dto;
public class PagePluginCoreInput : BasePageInput
{
/// <summary>
/// 名称
/// </summary>
public string Name { get; set; }
/// <summary>
/// 编码
/// </summary>
public string Code { get; set; }
}
public class AddPluginCoreInput : SysPluginCore
{
}
public class UpdatePluginCoreInput : AddPluginCoreInput
{
}
public class DeletePluginCoreInput : BaseIdInput
{
}
public class EnablePluginCoreInput : BaseIdInput
{
}
public class UpdatePluginCoreSettingInput : BaseIdInput
{
public string Data { get; set; }
}

View File

@ -0,0 +1,729 @@
// 麻省理工学院许可证
//
// 版权所有 (c) 2021-2023 zuohuaijun大名科技天津有限公司 联系电话/微信18020030720 QQ515096995
//
// 特此免费授予获得本软件的任何人以处理本软件的权利,但须遵守以下条件:在所有副本或重要部分的软件中必须包括上述版权声明和本许可声明。
//
// 软件按“原样”提供,不提供任何形式的明示或暗示的保证,包括但不限于对适销性、适用性和非侵权的保证。
// 在任何情况下,作者或版权持有人均不对任何索赔、损害或其他责任负责,无论是因合同、侵权或其他方式引起的,与软件或其使用或其他交易有关。
using Admin.NET.Core;
using Admin.NET.Plugin.PluginCoreManager.Service.Plugin.Dto;
using Furion.VirtualFileServer;
using Minio;
using OnceMi.AspNetCore.OSS;
using Org.BouncyCastle.Asn1.Ocsp;
using PluginCore;
using PluginCore.AspNetCore.Interfaces;
using PluginCore.AspNetCore.ResponseModel;
using PluginCore.Infrastructure;
using PluginCore.Interfaces;
using PluginCore.IPlugins;
using PluginCore.Models;
namespace Admin.NET.Plugin.PluginCoreManager.Service.Plugin;
/// <summary>
/// 系统动态插件服务
/// </summary>
[ApiDescriptionSettings(Order = 245)]
public class SysPluginCoreService : IDynamicApiController, ITransient
{
#region Fields
private readonly IPluginManager _pluginManager;
private readonly IPluginFinder _pluginFinder;
private readonly IPluginApplicationBuilderManager _pluginApplicationBuilderManager;
#endregion
private readonly IDynamicApiRuntimeChangeProvider _provider;
private readonly SqlSugarRepository<SysPluginCore> _sysPluginRep;
public SysPluginCoreService(IPluginManager pluginManager, IPluginFinder pluginFinder, IPluginApplicationBuilderManager pluginApplicationBuilderManager, IDynamicApiRuntimeChangeProvider provider,
SqlSugarRepository<SysPluginCore> sysPluginRep)
{
_pluginManager = pluginManager;
_pluginFinder = pluginFinder;
_pluginApplicationBuilderManager = pluginApplicationBuilderManager;
_provider = provider;
_sysPluginRep = sysPluginRep;
}
/// <summary>
/// 获取动态插件列表
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[DisplayName("获取动态插件列表")]
public async Task<SqlSugarPagedList<SysPluginCore>> Page(PagePluginInput input)
{
return await _sysPluginRep.AsQueryable()
.WhereIF(!string.IsNullOrWhiteSpace(input.Name), u => u.DisplayName.Contains(input.Name))
.OrderBy(u => u.OrderNo)
.ToPagedListAsync(input.Page, input.PageSize);
}
/// <summary>
/// 查看详细
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[ApiDescriptionSettings(Name = "Details"), HttpGet]
[DisplayName("查看详细")]
public async Task<PluginInfoResponseModel> Details(long id)
{
var pluginCore = await _sysPluginRep.GetByIdAsync(id);
if (pluginCore == null) throw Oops.Oh("查询插件ID失败"); ;
// 先移除再添加动态程序集/接口
try
{
#region
var pluginId = pluginCore.PluginId;
var pluginConfigModel = PluginConfigModelFactory.Create();
string[] localPluginIds = PluginPathProvider.AllPluginFolderName().ToArray();
if (!localPluginIds.Contains(pluginId))
{
throw Oops.Oh($"查看详细失败: 不存在 {pluginId} 插件");
}
#endregion
PluginInfoModel pluginInfoModel = PluginInfoModelFactory.Create(pluginId);
string[] enablePluginIds = _pluginFinder.EnablePluginIds().ToArray();
PluginInfoResponseModel pluginInfoResponseModel = PluginInfoModelToResponseModel(new List<PluginInfoModel>() { pluginInfoModel }, pluginConfigModel, enablePluginIds).FirstOrDefault();
return pluginInfoResponseModel;
}
catch (Exception ex)
{
throw Oops.Oh("查看详细失败: " + ex.Message);
}
}
#region
/// <summary>
/// 查看文档
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[ApiDescriptionSettings(Name = "Readme"), HttpGet]
[DisplayName("查看文档")]
public async Task<PluginReadmeResponseModel> Readme(long id)
{
var pluginCore = await _sysPluginRep.GetByIdAsync(id);
if (pluginCore == null) throw Oops.Oh("查询插件ID失败"); ;
try
{
#region
var pluginId = pluginCore.PluginId;
var pluginConfigModel = PluginConfigModelFactory.Create();
string[] localPluginIds = PluginPathProvider.AllPluginFolderName().ToArray();
if (!localPluginIds.Contains(pluginId))
{
throw Oops.Oh($"查看详细失败: 不存在 {pluginId} 插件");
}
#endregion
PluginReadmeModel readmeModel = PluginReadmeModelFactory.Create(pluginId);
PluginReadmeResponseModel readmeResponseModel = new PluginReadmeResponseModel();
readmeResponseModel.Content = readmeModel?.Content ?? "";
readmeResponseModel.PluginId = pluginId;
return readmeResponseModel;
}
catch (Exception ex)
{
throw Oops.Oh("查看详细失败: " + ex.Message);
}
}
#endregion
/// <summary>
/// 卸载动态插件
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[ApiDescriptionSettings(Name = "Uninstall"), HttpPost]
[DisplayName("卸载动态插件")]
public async Task Uninstall(DeletePluginCoreInput input)
{
var pluginCore = await _sysPluginRep.GetByIdAsync(input.Id);
if (pluginCore == null) throw Oops.Oh("查询插件ID失败");
// 卸载插件 必须 先禁用插件
#region
var pluginId = pluginCore.PluginId;
var pluginConfigModel = PluginConfigModelFactory.Create();
if (pluginConfigModel.EnabledPlugins.Contains(pluginId))
{
throw Oops.Oh("卸载失败: 请先禁用此插件");
}
string pluginDirStr = Path.Combine(PluginPathProvider.PluginsRootPath(), pluginId);
string pluginWwwrootDirStr = Path.Combine(PluginPathProvider.PluginsWwwRootDir(), pluginId);
if (!Directory.Exists(pluginDirStr) && !Directory.Exists(pluginWwwrootDirStr))
{
throw Oops.Oh("卸载失败: 此插件不存在");
}
#endregion
try
{
// PS:卸载插件必须先禁用插件所以此时插件LoadContext已被移除释放(插件Assemblies已被释放), 此处不需移除LoadContext
// 1.删除物理文件
var pluginDir = new DirectoryInfo(pluginDirStr);
if (pluginDir.Exists)
{
pluginDir.Delete(true);
}
// 虽然 已禁用 时 pluginWwwrootDirStr/pluginId 已删除, 但为确保, 还是再删除一次
var pluginWwwrootDir = new DirectoryInfo(pluginWwwrootDirStr);
if (pluginWwwrootDir.Exists)
{
pluginWwwrootDir.Delete(true);
}
await _sysPluginRep.DeleteAsync(u => u.Id == input.Id);
}
catch (Exception ex)
{
throw Oops.Oh("卸载失败: " + ex.Message);
}
}
#region
/// <summary>
/// 插件设置设置
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[ApiDescriptionSettings(Name = "Settings"), HttpGet]
[DisplayName("插件设置设置")]
public async Task<string> Settings(long id)
{
var pluginCore = await _sysPluginRep.GetByIdAsync(id);
if (pluginCore == null) throw Oops.Oh("查询插件ID失败"); ;
try
{
#region
var pluginId = pluginCore.PluginId;
var pluginConfigModel = PluginConfigModelFactory.Create();
string[] localPluginIds = PluginPathProvider.AllPluginFolderName().ToArray();
if (!localPluginIds.Contains(pluginId))
{
throw Oops.Oh($"查看详细失败: 不存在 {pluginId} 插件");
}
#endregion
string settingsJsonStr = PluginSettingsModelFactory.Create(pluginId);
return settingsJsonStr ?? "无设置项";
}
catch (Exception ex)
{
throw Oops.Oh("查看设置失败: " + ex.Message);
}
}
/// <summary>
/// 插件设置设置
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[ApiDescriptionSettings(Name = "Settings"), HttpPost]
[DisplayName("插件设置设置")]
public async Task Settings(UpdatePluginCoreSettingInput input)
{
var pluginCore = await _sysPluginRep.GetByIdAsync(input.Id);
if (pluginCore == null) throw Oops.Oh("查询插件ID失败"); ;
try
{
#region
var pluginId = pluginCore.PluginId;
var pluginConfigModel = PluginConfigModelFactory.Create();
string[] localPluginIds = PluginPathProvider.AllPluginFolderName().ToArray();
if (!localPluginIds.Contains(pluginId))
{
throw Oops.Oh($"查看详细失败: 不存在 {pluginId} 插件");
}
#endregion
input.Data = input.Data ?? "";
PluginSettingsModelFactory.Save(pluginSettingsJsonStr: input.Data, pluginId: pluginCore.PluginId);
}
catch (Exception ex)
{
throw Oops.Oh("设置失败: " + ex.Message);
}
}
#endregion
/// <summary>
/// 启用插件
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[ApiDescriptionSettings(Name = "Enable"), HttpPost]
[DisplayName("启用插件")]
public async Task Enable(EnablePluginCoreInput input)
{
var pluginCore = await _sysPluginRep.GetByIdAsync(input.Id);
if (pluginCore == null) return;
// 移除动态程序集/接口
var pluginConfigModel = PluginConfigModelFactory.Create();
// 效验是否存在于 已禁用插件列表
#region
var pluginId = pluginCore.PluginId;
var pluginDir = new DirectoryInfo(Path.Combine(PluginPathProvider.PluginsRootPath(), pluginId));
if (pluginDir != null && !pluginDir.Exists)
{
throw Oops.Oh("启用失败: 此插件不存在");
}
string[] enablePluginIds = _pluginFinder.EnablePluginIds().ToArray();
if (enablePluginIds.Contains(pluginId))
{
throw Oops.Oh("启用失败: 此插件已启用");
}
#endregion
try
{
// 1. 创建插件程序集加载上下文, 添加到 PluginsLoadContexts
_pluginManager.LoadPlugin(pluginId);
// 2. 添加到 pluginConfigModel.EnabledPlugins
pluginConfigModel.EnabledPlugins.Add(pluginId);
// 4.保存到 plugin.config.json
PluginConfigModelFactory.Save(pluginConfigModel);
// 5. 找到此插件实例
IPlugin plugin = _pluginFinder.Plugin(pluginId);
if (plugin == null)
{
// 7.启用不成功, 回滚插件状态: (1)释放插件上下文 (2)更新 plugin.config.json
try
{
_pluginManager.UnloadPlugin(pluginId);
}
catch (Exception ex)
{ }
// 从 pluginConfigModel.EnabledPlugins 移除
pluginConfigModel.EnabledPlugins.Remove(pluginId);
// 保存到 plugin.config.json
PluginConfigModelFactory.Save(pluginConfigModel);
throw Oops.Oh("启用失败: 此插件不存在");
}
// 6.调取插件的 AfterEnable(), 插件开发者可在此回收资源
var pluginEnableResult = plugin.AfterEnable();
if (!pluginEnableResult.IsSuccess)
{
// 7.启用不成功, 回滚插件状态: (1)释放插件上下文 (2)更新 plugin.config.json
try
{
_pluginManager.UnloadPlugin(pluginId);
}
catch (Exception ex)
{ }
// 从 pluginConfigModel.EnabledPlugins 移除
pluginConfigModel.EnabledPlugins.Remove(pluginId);
// 保存到 plugin.config.json
PluginConfigModelFactory.Save(pluginConfigModel);
throw Oops.Oh("启用失败: 来自插件的错误信息: " + pluginEnableResult.Message);
}
// 7. ReBuild
this._pluginApplicationBuilderManager.ReBuild();
// 8. 尝试复制 插件下的 wwwroot 到 Plugins_wwwroot
string wwwRootDir = PluginPathProvider.WwwRootDir(pluginId);
if (Directory.Exists(wwwRootDir))
{
string targetDir = PluginPathProvider.PluginWwwRootDir(pluginId);
PluginCore.Utils.FileUtil.CopyFolder(wwwRootDir, targetDir);
}
//9.载入Furion动态插件
var pluginMainAssembly = _pluginManager.GetPluginAssembly(pluginId);
// 将程序集添加进动态 WebAPI 应用部件
_provider.AddAssembliesWithNotifyChanges(pluginMainAssembly);
await _sysPluginRep.UpdateAsync(u => new SysPluginCore() { Status = StatusEnum.Enable }, u => u.Id == input.Id);
}
catch (Exception ex)
{
throw Oops.Oh("启用失败: " + ex.Message);
}
}
/// <summary>
/// 禁用插件
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[ApiDescriptionSettings(Name = "Disable"), HttpPost]
[DisplayName("禁用插件")]
public async Task Disable(EnablePluginCoreInput input)
{
var pluginCore = await _sysPluginRep.GetByIdAsync(input.Id);
if (pluginCore == null) return;
// 移除动态程序集/
#region
var pluginId = pluginCore.PluginId;
var pluginConfigModel = PluginConfigModelFactory.Create();
// string[] enablePluginIds = _pluginFinder.EnablePluginIds().ToArray();
// // 效验是否存在于 已启用插件列表
// if (!enablePluginIds.Contains(pluginId))
// {
// responseData.Code = -1;
// responseData.Message = "禁用失败: 此插件不存在, 或未启用";
// return await Task.FromResult(responseData);
// }
#endregion
try
{
// 1. 找到此插件实例
IPlugin plugin = _pluginFinder.Plugin(pluginId);
if (plugin == null)
{
throw Oops.Oh("禁用失败: 此插件不存在, 或未启用");
}
try
{
// 2.调取插件的 BeforeDisable(), 插件开发者可在此回收资源
var pluginDisableResult = plugin.BeforeDisable();
if (!pluginDisableResult.IsSuccess)
{
throw Oops.Oh("禁用失败: 来自插件的错误信息: " + pluginDisableResult.Message);
}
// 3.移除插件对应的程序集加载上下文
_pluginManager.UnloadPlugin(pluginId);
// 3.1. ReBuild
this._pluginApplicationBuilderManager.ReBuild();
if (pluginConfigModel.EnabledPlugins.Contains(pluginId))
{
// 4.从 pluginConfigModel.EnabledPlugins 移除
pluginConfigModel.EnabledPlugins.Remove(pluginId);
// 5.保存到 plugin.config.json
PluginConfigModelFactory.Save(pluginConfigModel);
}
}
catch (Exception ex)
{
PluginCore.Utils.LogUtil.Error<SysPluginCoreService>(ex.ToString());
throw Oops.Oh("禁用失败: 此插件不存在, 或未启用");
}
// 7. 尝试移除 Plugins_wwwroot/PluginId
string pluginWwwRootDir = PluginPathProvider.PluginWwwRootDir(pluginId);
if (Directory.Exists(pluginWwwRootDir))
{
Directory.Delete(pluginWwwRootDir, true);
}
//8.移除Furion动态插件
//9.载入Furion动态插件
var pluginMainAssembly = _pluginManager.GetPluginAssembly(pluginId);
// 将程序集添加进动态 WebAPI 应用部件
_provider.RemoveAssembliesWithNotifyChanges(pluginMainAssembly);
await _sysPluginRep.UpdateAsync(u => new SysPluginCore() { Status = StatusEnum.Disable }, u => u.Id == input.Id);
}
catch (Exception ex)
{
throw Oops.Oh("禁用失败: " + ex.Message);
}
}
/// <summary>
/// 上传文件
/// </summary>
/// <param name="file"></param>
/// <param name="path"></param>
/// <returns></returns>
[DisplayName("上传文件")]
public async Task UploadFile([Required] IFormFile file, [FromQuery] string? path)
{
var sysFile = await HandleUploadFile(file, path);
//return new FileOutput
//{
//};
}
/// <summary>
/// 上传文件
/// </summary>
/// <param name="file">文件</param>
/// <param name="savePath">路径</param>
/// <returns></returns>
[NonAction]
private async Task<BaseResponseModel> HandleUploadFile(IFormFile file, string savePath)
{
if (file == null) throw Oops.Oh(ErrorCodeEnum.D8000);
var path = savePath;
BaseResponseModel responseData = new BaseResponseModel();
#region
if (file == null)
{
throw Oops.Oh("上传的文件不能为空");
//responseData.Code = -1;
//responseData.Message = "上传的文件不能为空";
//return responseData;
}
//文件后缀
string fileExtension = Path.GetExtension(file.FileName);//获取文件格式,拓展名
// 类型标记
UploadFileType uploadFileType = UploadFileType.NoAllowedType;
switch (fileExtension)
{
case ".zip":
uploadFileType = UploadFileType.Zip;
break;
case ".nupkg":
uploadFileType = UploadFileType.Nupkg;
break;
}
if (fileExtension != ".zip" && fileExtension != ".nupkg")
{
throw Oops.Oh("只能上传 zip 或 nupkg 格式文件");
//responseData.Code = -1;
//// nupkg 其实就是 zip
//responseData.Message = "只能上传 zip 或 nupkg 格式文件";
//return responseData;
}
// PluginCore.AspNetCore-v1.0.2 起 不再限制插件上传大小
//判断文件大小
//var fileSize = file.Length;
//if (fileSize > 1024 * 1024 * 5) // 5M
//{
// responseData.Code = -1;
// responseData.Message = "上传的文件不能大于5MB";
// return responseData;
//}
#endregion
try
{
// 1.先上传到 临时插件上传目录, 用Guid.zip作为保存文件名
string tempZipFilePath = Path.Combine(PluginPathProvider.TempPluginUploadDir(), Guid.NewGuid() + ".zip");
using (var fs = System.IO.File.Create(tempZipFilePath))
{
file.CopyTo(fs); //将上传的文件文件流复制到fs中
fs.Flush();//清空文件流
}
// 2.解压
bool isDecomparessSuccess = false;
if (uploadFileType == UploadFileType.Zip)
{
isDecomparessSuccess = PluginCore.Utils.ZipHelper.DecomparessFile(tempZipFilePath, tempZipFilePath.Replace(".zip", ""));
}
else if (uploadFileType == UploadFileType.Nupkg)
{
isDecomparessSuccess = NupkgService.DecomparessFile(tempZipFilePath, tempZipFilePath.Replace(".zip", ""));
}
// 3.删除原压缩包
System.IO.File.Delete(tempZipFilePath);
if (!isDecomparessSuccess)
{
throw Oops.Oh("解压插件压缩包失败");
//responseData.Code = -1;
//responseData.Message = "解压插件压缩包失败";
//return responseData;
}
// 4.读取其中的info.json, 获取 PluginId 值
PluginInfoModel pluginInfoModel = PluginInfoModelFactory.ReadPluginDir(tempZipFilePath.Replace(".zip", ""));
if (pluginInfoModel == null || string.IsNullOrEmpty(pluginInfoModel.PluginId))
{
// 记得删除已不再需要的临时插件文件夹
Directory.Delete(tempZipFilePath.Replace(".zip", ""), true);
throw Oops.Oh("不合法的插件");
//responseData.Code = -1;
//responseData.Message = "不合法的插件";
//return responseData;
}
string pluginId = pluginInfoModel.PluginId;
// 5.检索 此 PluginId 是否本地插件已存在
var pluginConfigModel = PluginConfigModelFactory.Create();
// 本地已经存在的 PluginId
IList<string> localExistPluginIds = PluginPathProvider.AllPluginFolderName();
if (localExistPluginIds.Contains(pluginId))
{
// 记得删除已不再需要的临时插件文件夹
Directory.Delete(tempZipFilePath.Replace(".zip", ""), true);
throw Oops.Oh($"本地已有此插件 (PluginId: {pluginId}), 请前往插件列表删除后, 再上传");
//responseData.Code = -1;
//responseData.Message = $"本地已有此插件 (PluginId: {pluginId}), 请前往插件列表删除后, 再上传";
//return responseData;
}
// 6.本地无此插件 -> 移动插件文件夹到 Plugins 下, 并以 PluginId 为插件文件夹名
string pluginsRootPath = PluginPathProvider.PluginsRootPath();
string newPluginDir = Path.Combine(pluginsRootPath, pluginId);
Directory.Move(tempZipFilePath.Replace(".zip", ""), newPluginDir);
// 7. 放入 Plugins 中, 默认为 已禁用
var pluginCore = new SysPluginCore();
pluginCore.PluginId = pluginId;
pluginCore.DisplayName = pluginInfoModel.DisplayName;
pluginCore.Description = pluginInfoModel.Description;
pluginCore.Author = pluginInfoModel.Author;
pluginCore.Version = pluginInfoModel.Version;
pluginCore.Status = StatusEnum.Disable;
await _sysPluginRep.InsertAsync(pluginCore.Adapt<SysPluginCore>());
responseData.Code = 1;
responseData.Message = $"上传插件成功 (PluginId: {pluginId})";
}
catch (Exception ex)
{
throw Oops.Oh("上传插件失败: " + ex.Message);
//responseData.Code = -1;
//responseData.Message = "上传插件失败: " + ex.Message;
//ex = ex.InnerException;
//while (ex != null)
//{
// responseData.Message += " - " + ex.InnerException.Message;
// ex = ex.InnerException;
//}
}
return responseData;
}
#region Helpers
[NonAction]
private IList<PluginInfoResponseModel> PluginInfoModelToResponseModel(IList<PluginInfoModel> pluginInfoModels, PluginConfigModel pluginConfigModel, string[] enablePluginIds)
{
// 获取 Plugins 下所有插件
// DirectoryInfo pluginsDir = new DirectoryInfo(PluginPathProvider.PluginsRootPath());
// List<string> pluginIds = pluginsDir?.GetDirectories()?.Select(m => m.Name)?.ToList() ?? new List<string>();
IList<PluginInfoResponseModel> responseModels = new List<PluginInfoResponseModel>();
#region
foreach (var model in pluginInfoModels)
{
PluginInfoResponseModel responseModel = new PluginInfoResponseModel();
responseModel.Author = model.Author;
responseModel.Description = model.Description;
responseModel.DisplayName = model.DisplayName;
responseModel.PluginId = model.PluginId;
responseModel.SupportedVersions = model.SupportedVersions;
responseModel.Version = model.Version;
responseModel.DependPlugins = model.DependPlugins;
if (pluginConfigModel.EnabledPlugins.Contains(model.PluginId) && !enablePluginIds.Contains(model.PluginId))
{
// 错误情况: 配置 标识 已启用, 但实际没有启用成功
pluginConfigModel.EnabledPlugins.Remove(model.PluginId);
PluginConfigModelFactory.Save(pluginConfigModel);
responseModel.Status = PluginStatus.Disabled;
}
else if (!pluginConfigModel.EnabledPlugins.Contains(model.PluginId) && enablePluginIds.Contains(model.PluginId))
{
// 错误情况: 配置没有标识 已启用, 但实际 已启用
pluginConfigModel.EnabledPlugins.Add(model.PluginId);
PluginConfigModelFactory.Save(pluginConfigModel);
responseModel.Status = PluginStatus.Enabled;
}
else if (pluginConfigModel.EnabledPlugins.Contains(model.PluginId) && enablePluginIds.Contains(model.PluginId))
{
responseModel.Status = PluginStatus.Enabled;
}
else
{
responseModel.Status = PluginStatus.Disabled;
}
responseModels.Add(responseModel);
}
#endregion
return responseModels;
}
public enum UploadFileType
{
NoAllowedType = 0,
Zip = 1,
Nupkg = 2
}
#endregion
}

View File

@ -0,0 +1,34 @@
// 麻省理工学院许可证
//
// 版权所有 (c) 2021-2023 zuohuaijun大名科技天津有限公司 联系电话/微信18020030720 QQ515096995
//
// 特此免费授予获得本软件的任何人以处理本软件的权利,但须遵守以下条件:在所有副本或重要部分的软件中必须包括上述版权声明和本许可声明。
//
// 软件按“原样”提供,不提供任何形式的明示或暗示的保证,包括但不限于对适销性、适用性和非侵权的保证。
// 在任何情况下,作者或版权持有人均不对任何索赔、损害或其他责任负责,无论是因合同、侵权或其他方式引起的,与软件或其使用或其他交易有关。
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using PluginCore.AspNetCore.Extensions;
using System;
namespace Admin.NET.Plugin.PluginCoreManager;
[AppStartup(100)]
public class Startup : AppStartup
{
public void ConfigureServices(IServiceCollection services)
{
RunOptions.Default
.AddComponent<StartupServiceComponent>()
.UseComponent<StartupApplicationComponent>();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
}
}

View File

@ -0,0 +1,56 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using PluginCore;
using PluginCore.AspNetCore.Extensions;
using PluginCore.Interfaces;
using PluginCore.Models;
using static SKIT.FlurlHttpClient.Wechat.Api.Models.WxaPluginListResponse.Types;
namespace Admin.NET.Plugin.PluginCoreManager;
// 模拟 ConfigureService
public sealed class StartupServiceComponent : IServiceComponent
{
public void Load(IServiceCollection services, ComponentContext componentContext)
{
// 1. 添加 PluginCore
services.AddPluginCore();
IPluginManager pluginManager = App.GetService<IPluginManager>();
IDynamicApiRuntimeChangeProvider provider = App.GetService<IDynamicApiRuntimeChangeProvider>();
#region PluginConfigModel
PluginConfigModel pluginConfigModel = PluginConfigModelFactory.Create();
#endregion
// 已启用的插件
#region Assemblies
IList<string> enabledPluginIds = pluginConfigModel.EnabledPlugins;
foreach (var pluginId in enabledPluginIds)
{
//9.载入Furion动态插件
var pluginMainAssembly = pluginManager.GetPluginAssembly(pluginId);
// 将程序集添加进动态 WebAPI 应用部件
provider.AddAssembliesWithNotifyChanges(pluginMainAssembly);
}
#endregion
}
}
// 模拟 Configure
public sealed class StartupApplicationComponent : IApplicationComponent
{
public void Load(IApplicationBuilder app, IWebHostEnvironment env, ComponentContext componentContext)
{
// 2. 使用 PluginCore
app.UsePluginCore();
}
}

View File

@ -0,0 +1,95 @@
//===================================================
// License: GNU LGPLv3
// Contributors: yiyungent@gmail.com
// Project: https://yiyungent.github.io/PluginCore
// GitHub: https://github.com/yiyungent/PluginCore
//===================================================
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Options;
namespace PluginCore.AspNetCore.AdminUI
{
public static class PluginCoreAdminUIBuilderExtensions
{
/// <summary>
/// Register the SwaggerUI middleware with provided options
/// </summary>
public static IApplicationBuilder UsePluginCoreAdminUI(this IApplicationBuilder app, PluginCoreAdminUIOptions options)
{
#region Old -
//Config.PluginCoreConfig pluginCoreConfig = Config.PluginCoreConfigFactory.Create();
//switch (pluginCoreConfig.FrontendMode?.ToLower())
//{
// case "localembedded":
// app.UseMiddleware<PluginCoreAdminUIMiddleware>(options);
// break;
// case "localfolder":
// #region LocalFolder
// //string contentRootPath = Directory.GetCurrentDirectory();
// // https://docs.microsoft.com/zh-CN/aspnet/core/fundamentals/static-files?view=aspnetcore-5.0
// //var options = new DefaultFilesOptions()
// //{
// // RequestPath = "/PluginCore/Admin",
// //};
// //// TODO: 404: 无效, 失败, 改为使用 Controller 手动指定
// ////options.DefaultFileNames.Add("PluginCoreAdmin/index.html");
// //app.UseDefaultFiles(options);
// // 注意: 为了无需重启Web而更新是否本地前端配置, 因此此项保持常驻开启
// // 因此, 需要保证 PluginCoreAdmin 文件夹存在
// string pluginCoreAdminDir = PluginPathProvider.PluginCoreAdminDir();
// app.UseStaticFiles(new StaticFileOptions
// {
// FileProvider = new PhysicalFileProvider(
// pluginCoreAdminDir),
// RequestPath = "/PluginCore/Admin"
// });
// #endregion
// break;
// case "remotecdn":
// break;
// default:
// app.UseMiddleware<PluginCoreAdminUIMiddleware>(options);
// break;
//}
//return app;
#endregion
return app.UseMiddleware<PluginCoreAdminUIMiddleware>(options);
}
/// <summary>
/// Register the SwaggerUI middleware with optional setup action for DI-injected options
/// </summary>
public static IApplicationBuilder UsePluginCoreAdminUI(
this IApplicationBuilder app)
{
PluginCoreAdminUIOptions options = new PluginCoreAdminUIOptions()
{
};
return app.UsePluginCoreAdminUI(options);
}
}
}

View File

@ -0,0 +1,150 @@
//===================================================
// License: GNU LGPLv3
// Contributors: yiyungent@gmail.com
// Project: https://yiyungent.github.io/PluginCore
// GitHub: https://github.com/yiyungent/PluginCore
//===================================================
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace PluginCore.AspNetCore.AdminUI
{
public class PluginCoreAdminUIMiddleware
{
private const string EmbeddedFileNamespace = "PluginCore.AspNetCore.node_modules.plugincore_admin_frontend.dist";
private readonly PluginCoreAdminUIOptions _options;
private readonly StaticFileMiddleware _staticFileMiddleware;
public PluginCoreAdminUIMiddleware(
RequestDelegate next,
IWebHostEnvironment hostingEnv,
ILoggerFactory loggerFactory,
PluginCoreAdminUIOptions options)
{
_options = options ?? new PluginCoreAdminUIOptions();
_staticFileMiddleware = CreateStaticFileMiddleware(next, hostingEnv, loggerFactory, options);
}
public async Task Invoke(HttpContext httpContext)
{
var httpMethod = httpContext.Request.Method;
var path = httpContext.Request.Path.Value;
// If the RoutePrefix is requested (with or without trailing slash), redirect to index URL
if (httpMethod == "GET" && Regex.IsMatch(path, $"^/?{Regex.Escape(_options.RoutePrefix)}/?$", RegexOptions.IgnoreCase))
{
// Use relative redirect to support proxy environments
var relativeIndexUrl = string.IsNullOrEmpty(path) || path.EndsWith("/")
? "index.html"
: $"{path.Split('/').Last()}/index.html";
RespondWithRedirect(httpContext.Response, relativeIndexUrl);
return;
}
if (httpMethod == "GET" && Regex.IsMatch(path, $"^/{Regex.Escape(_options.RoutePrefix)}/?index.html$", RegexOptions.IgnoreCase))
{
await RespondWithIndexHtml(httpContext.Response);
return;
}
await _staticFileMiddleware.Invoke(httpContext);
}
private StaticFileMiddleware CreateStaticFileMiddleware(
RequestDelegate next,
IWebHostEnvironment hostingEnv,
ILoggerFactory loggerFactory,
PluginCoreAdminUIOptions options)
{
Config.PluginCoreConfig pluginCoreConfig = Config.PluginCoreConfigFactory.Create();
IFileProvider fileProvider = null;
switch (pluginCoreConfig.FrontendMode?.ToLower())
{
case "localembedded":
fileProvider = new EmbeddedFileProvider(typeof(PluginCoreAdminUIMiddleware).GetTypeInfo().Assembly,
EmbeddedFileNamespace);
break;
case "localfolder":
string absoluteRootPath = PluginPathProvider.PluginCoreAdminDir();
fileProvider = new PhysicalFileProvider(absoluteRootPath);
break;
case "remotecdn":
fileProvider = new PluginCoreAdminUIRemoteFileProvider(pluginCoreConfig.RemoteFrontend);
break;
default:
fileProvider = new EmbeddedFileProvider(typeof(PluginCoreAdminUIMiddleware).GetTypeInfo().Assembly,
EmbeddedFileNamespace);
break;
}
var staticFileOptions = new StaticFileOptions
{
RequestPath = string.IsNullOrEmpty(options.RoutePrefix) ? string.Empty : $"/{options.RoutePrefix}",
FileProvider = fileProvider,
};
return new StaticFileMiddleware(next, hostingEnv, Options.Create(staticFileOptions), loggerFactory);
}
private void RespondWithRedirect(HttpResponse response, string location)
{
response.StatusCode = 301;
response.Headers["Location"] = location;
}
private async Task RespondWithIndexHtml(HttpResponse response)
{
response.StatusCode = 200;
response.ContentType = "text/html;charset=utf-8";
using (var stream = _options.IndexStream())
{
// Inject arguments before writing to response
var htmlBuilder = new StringBuilder(new StreamReader(stream).ReadToEnd());
foreach (var entry in GetIndexArguments())
{
htmlBuilder.Replace(entry.Key, entry.Value);
}
await response.WriteAsync(htmlBuilder.ToString(), Encoding.UTF8);
}
}
private IDictionary<string, string> GetIndexArguments()
{
return new Dictionary<string, string>()
{
//{ "%(DocumentTitle)", _options.DocumentTitle },
//{ "%(HeadContent)", _options.HeadContent },
//{ "%(ConfigObject)", JsonSerializer.Serialize(_options.ConfigObject, _jsonSerializerOptions) },
//{ "%(OAuthConfigObject)", JsonSerializer.Serialize(_options.OAuthConfigObject, _jsonSerializerOptions) },
//{ "%(Interceptors)", JsonSerializer.Serialize(_options.Interceptors) },
};
}
}
}

View File

@ -0,0 +1,68 @@
//===================================================
// License: GNU LGPLv3
// Contributors: yiyungent@gmail.com
// Project: https://yiyungent.github.io/PluginCore
// GitHub: https://github.com/yiyungent/PluginCore
//===================================================
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Reflection;
using System.Text;
namespace PluginCore.AspNetCore.AdminUI
{
public class PluginCoreAdminUIOptions
{
/// <summary>
/// Gets or sets a route prefix for accessing the swagger-ui
/// </summary>
public string RoutePrefix { get; set; } = "PluginCore/Admin";
/// <summary>
/// Gets or sets a Stream function for retrieving the swagger-ui page
/// </summary>
public Func<Stream> IndexStream
{
get
{
Func<Stream> funcStream = null;
;
Config.PluginCoreConfig pluginCoreConfig = Config.PluginCoreConfigFactory.Create();
switch (pluginCoreConfig.FrontendMode?.ToLower())
{
case "localembedded":
funcStream = () => typeof(PluginCoreAdminUIOptions).GetTypeInfo().Assembly
.GetManifestResourceStream("PluginCore.AspNetCore.node_modules.plugincore_admin_frontend.dist.index.html");
break;
case "localfolder":
string absoluteRootPath = PluginPathProvider.PluginCoreAdminDir();
string indexFilePath = Path.Combine(absoluteRootPath, "index.html");
funcStream = () => (Stream)new FileStream(indexFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 1, FileOptions.Asynchronous | FileOptions.SequentialScan);
break;
case "remotecdn":
string remoteFrontendRootPath = pluginCoreConfig.RemoteFrontend;
string indexFileRemotePath = remoteFrontendRootPath + "/" + "index.html";
funcStream = () => new HttpClient().GetStreamAsync(indexFileRemotePath).Result;
break;
default:
funcStream = () => typeof(PluginCoreAdminUIOptions).GetTypeInfo().Assembly
.GetManifestResourceStream("PluginCore.AspNetCore.node_modules.plugincore_admin_frontend.dist.index.html");
break;
}
return funcStream;
}
}
}
}

View File

@ -0,0 +1,132 @@
//===================================================
// License: GNU LGPLv3
// Contributors: yiyungent@gmail.com
// Project: https://yiyungent.github.io/PluginCore
// GitHub: https://github.com/yiyungent/PluginCore
//===================================================
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Text;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Primitives;
namespace PluginCore.AspNetCore.AdminUI
{
public class PluginCoreAdminUIRemoteFileProvider : IFileProvider
{
protected string RootUrl { get; set; }
public PluginCoreAdminUIRemoteFileProvider(string rootUrl)
{
this.RootUrl = rootUrl;
}
public IDirectoryContents GetDirectoryContents(string subpath)
{
return (IDirectoryContents)NotFoundDirectoryContents.Singleton;
}
public IFileInfo GetFileInfo(string subpath)
{
if (string.IsNullOrEmpty(subpath))
return (IFileInfo)new NotFoundFileInfo(subpath);
IFileInfo fileInfo = new PluginCoreAdminUIFileInfo(this.RootUrl, subpath);
return fileInfo;
}
public IChangeToken Watch(string filter)
{
throw new NotImplementedException();
}
public class PluginCoreAdminUIFileInfo : IFileInfo
{
protected string RootUrl { get; set; }
protected string SubPath { get; set; }
private string _name;
public PluginCoreAdminUIFileInfo(string rootUrl, string subpath)
{
this.RootUrl = rootUrl;
this.SubPath = subpath;
this._name = this.SubPath.Substring(this.SubPath.LastIndexOf("/") + 1);
}
public Stream CreateReadStream()
{
HttpClient httpClient = new HttpClient();
return httpClient.GetStreamAsync($"{this.RootUrl}/{this.SubPath}").Result;
}
public bool Exists
{
get
{
bool isExist = false;
if (this.Name == "index.html")
{
isExist = true;
}
return isExist;
}
}
public bool IsDirectory
{
get
{
return false;
}
}
public DateTimeOffset LastModified
{
get
{
return new DateTimeOffset(DateTime.Now);
}
}
public long Length
{
get
{
return 111;
}
}
public string Name
{
get
{
return this._name;
}
}
public string PhysicalPath
{
get
{
return "";
}
}
}
}
}

View File

@ -0,0 +1,72 @@
//===================================================
// License: GNU LGPLv3
// Contributors: yiyungent@gmail.com
// Project: https://yiyungent.github.io/PluginCore
// GitHub: https://github.com/yiyungent/PluginCore
//===================================================
using System;
using System.Collections.Generic;
using System.Security.Claims;
using System.Text;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using PluginCore.AspNetCore.Authorization;
namespace PluginCore.AspNetCore.Authentication
{
/// <summary>
/// https://stackoverflow.com/questions/52287542/invalidoperationexception-no-authenticationscheme-was-specified-and-there-was
/// </summary>
public class PluginCoreAuthenticationHandler : AuthenticationHandler<PluginCoreAuthenticationSchemeOptions>
{
private readonly AccountManager _accountManager;
public PluginCoreAuthenticationHandler(IOptionsMonitor<PluginCoreAuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, AccountManager accountManager) : base(options, logger, encoder, clock)
{
this._accountManager = accountManager;
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
string token = this._accountManager.CurrentToken();
if (string.IsNullOrEmpty(token))
{
return AuthenticateResult.NoResult();
}
bool isAdmin = AccountManager.IsAdminToken(token);
if (!isAdmin)
{
return AuthenticateResult.Fail($"token is not admin");
}
else
{
var id = new ClaimsIdentity(
// new Claim[] { new Claim("PluginCore.Token", token) }, // not safe , just as an example , should custom claims on your own
claims: new Claim[] { new Claim(type: IPlugins.Constants.AspNetCoreAuthenticationClaimType, value: token) }, // not safe , just as an example , should custom claims on your own
authenticationType: Scheme.Name
);
ClaimsPrincipal principal = new ClaimsPrincipal(identity: id);
var ticket = new AuthenticationTicket(
principal: principal,
properties: new AuthenticationProperties(),
authenticationScheme: Scheme.Name);
// Utils.LogUtil.Info<PluginCoreAuthenticationHandler>($"通过 Authentication: token: {token}");
Utils.LogUtil.Info<PluginCoreAuthenticationHandler>($"Authentication Passed");
return AuthenticateResult.Success(ticket);
}
}
}
}

View File

@ -0,0 +1,23 @@
//===================================================
// License: GNU LGPLv3
// Contributors: yiyungent@gmail.com
// Project: https://yiyungent.github.io/PluginCore
// GitHub: https://github.com/yiyungent/PluginCore
//===================================================
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.AspNetCore.Authentication;
namespace PluginCore.AspNetCore.Authentication
{
public class PluginCoreAuthenticationSchemeOptions : AuthenticationSchemeOptions
{
}
}

View File

@ -0,0 +1,149 @@
//===================================================
// License: GNU LGPLv3
// Contributors: yiyungent@gmail.com
// Project: https://yiyungent.github.io/PluginCore
// GitHub: https://github.com/yiyungent/PluginCore
//===================================================
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.AspNetCore.Http;
using PluginCore.Utils;
namespace PluginCore.AspNetCore.Authorization
{
public class AccountManager
{
private readonly IHttpContextAccessor _httpContextAccessor;
public Microsoft.AspNetCore.Http.HttpContext HttpContext
{
get
{
return this._httpContextAccessor.HttpContext;
}
}
public AccountManager(IHttpContextAccessor httpContextAccessor)
{
// Exception: IFeatureCollection has been disposed. Object name: 'Collection'.
// https://stackoverflow.com/questions/59963383/session-setstring-method-throws-exception-ifeaturecollection-has-been-disposed
//HttpContext = ((HttpContextAccessor)httpContextAccessor).HttpContext;
//HttpContext = httpContextAccessor.HttpContext;
// 注意: 不要将 HttpContext 保存起来,应当每次都从 httpContextAccessor 取
_httpContextAccessor = httpContextAccessor;
}
public static Config.PluginCoreConfig.AdminModel Admin
{
get
{
return Config.PluginCoreConfigFactory.Create().Admin;
}
set
{
var sourceModel = Config.PluginCoreConfigFactory.Create();
sourceModel.Admin = value;
Config.PluginCoreConfigFactory.Save(sourceModel);
}
}
public static string CurrentToken(HttpContext httpContext)
{
string token = null;
HttpRequest request = httpContext.Request;
try
{
// header -> cookie
try
{
// header 中找 token
if (request.Headers.ContainsKey("Authorization"))
{
string authHeader = request.Headers["Authorization"];
if (!string.IsNullOrEmpty(authHeader) && authHeader.StartsWith("Bearer"))
{
token = authHeader.Substring("Bearer ".Length).Trim();
}
}
}
catch (Exception ex)
{
//throw ex;
}
if (string.IsNullOrEmpty(token))
{
// cookie 中找 token
//string tokenCookieName = "token";
// string tokenCookieName = "PluginCore.Admin.Token";
string tokenCookieName = IPlugins.Constants.AspNetCoreAuthorizationTokenCookieName;
if (request.Cookies.Keys.Contains(tokenCookieName))
{
if (request.Cookies[tokenCookieName] != null && string.IsNullOrEmpty(request.Cookies[tokenCookieName]) == false)
{
token = request.Cookies[tokenCookieName];
}
}
}
}
catch (Exception ex)
{
throw ex;
}
return token;
}
public string CurrentToken()
{
return CurrentToken(this.HttpContext);
}
public static string CreateToken()
{
return CreateToken(Admin.UserName, Admin.Password);
}
public static string CreateToken(string userName, string password)
{
string token = $"UserName={userName}&Password={password}";
token = Md5Helper.MD5Encrypt32(token);
return token;
}
public static bool IsAdminToken(string token)
{
bool isAdmin = false;
isAdmin = CreateToken().Equals(token);
return isAdmin;
}
public bool IsAdmin()
{
return IsAdmin(this.HttpContext);
}
public static bool IsAdmin(HttpContext httpContext)
{
bool isAdmin = false;
try
{
string currentToken = CurrentToken(httpContext);
isAdmin = IsAdminToken(currentToken);
}
catch (Exception ex)
{
throw ex;
}
return isAdmin;
}
}
}

View File

@ -0,0 +1,59 @@
//===================================================
// License: GNU LGPLv3
// Contributors: yiyungent@gmail.com
// Project: https://yiyungent.github.io/PluginCore
// GitHub: https://github.com/yiyungent/PluginCore
//===================================================
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
namespace PluginCore.AspNetCore.Authorization
{
public class PluginCoreAdminAuthorizationHandler : AuthorizationHandler<PluginCoreAdminRequirement>
{
private readonly AccountManager _accountManager;
public PluginCoreAdminAuthorizationHandler(AccountManager accountManager)
{
_accountManager = accountManager;
}
/// <summary>
/// 必须在其中呼叫一次 <see cref="AuthorizationHandlerContext.Succeed(IAuthorizationRequirement)"/> 代表满足 <see cref="PluginCoreAdminRequirement"/>,否则皆为 不满足此 Requirement
/// </summary>
/// <param name="context"></param>
/// <param name="requirement"></param>
/// <returns></returns>
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
PluginCoreAdminRequirement requirement)
{
bool isAdmin = this._accountManager.IsAdmin();
if (!isAdmin)
{
context.Fail();
}
else
{
// 认证通过后, 可通过下面方式获取 token
var identity = context.User.Identity;
string token = this._accountManager.CurrentToken();
// Utils.LogUtil.Info<PluginCoreAdminAuthorizationHandler>($"通过 Authorization: token: {token}");
Utils.LogUtil.Info<PluginCoreAdminAuthorizationHandler>($"Authorization Granted");
context.Succeed(requirement);
}
await Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,31 @@
//===================================================
// License: GNU LGPLv3
// Contributors: yiyungent@gmail.com
// Project: https://yiyungent.github.io/PluginCore
// GitHub: https://github.com/yiyungent/PluginCore
//===================================================
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.AspNetCore.Authorization;
namespace PluginCore.AspNetCore.Authorization
{
/// <summary>
/// 注意: PluginCoreAdmin -> PluginCore.Admin
/// </summary>
public class PluginCoreAdminAuthorizeAttribute : AuthorizeAttribute
{
// public PluginCoreAdminAuthorizeAttribute() : base("PluginCore.Admin")
public PluginCoreAdminAuthorizeAttribute() : base(policy: IPlugins.Constants.AspNetCoreAuthorizationPolicyName)
{
// 同时明确指定 认证方案 与 授权策略
AuthenticationSchemes = PluginCore.IPlugins.Constants.AspNetCoreAuthenticationScheme;
}
}
}

View File

@ -0,0 +1,26 @@
//===================================================
// License: GNU LGPLv3
// Contributors: yiyungent@gmail.com
// Project: https://yiyungent.github.io/PluginCore
// GitHub: https://github.com/yiyungent/PluginCore
//===================================================
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.AspNetCore.Authorization;
namespace PluginCore.AspNetCore.Authorization
{
public class PluginCoreAdminRequirement : IAuthorizationRequirement
{
public PluginCoreAdminRequirement()
{
}
}
}

View File

@ -0,0 +1,27 @@
//===================================================
// License: GNU LGPLv3
// Contributors: yiyungent@gmail.com
// Project: https://yiyungent.github.io/PluginCore
// GitHub: https://github.com/yiyungent/PluginCore
//===================================================
using Microsoft.Extensions.DependencyInjection;
namespace PluginCore.AspNetCore.BackgroundServices
{
public static class BackgroundServicesHelper
{
public static IServiceCollection AddBackgroundServices(this IServiceCollection services)
{
//services.AddScoped(typeof(IHostedService), typeof(TimeBackgroundService));
// AddHostedService: Microsoft.AspNetCore.App
services.AddHostedService<PluginTimeJobBackgroundService>(); // 以这种方式注入就是单例
return services;
}
}
}

View File

@ -0,0 +1,92 @@
//===================================================
// License: GNU LGPLv3
// Contributors: yiyungent@gmail.com
// Project: https://yiyungent.github.io/PluginCore
// GitHub: https://github.com/yiyungent/PluginCore
//===================================================
using System;
using System.Collections.Generic;
using System.Text;
using System.Linq;
using System.Threading.Tasks;
using PluginCore.Interfaces;
using PluginCore.IPlugins;
using PluginCore.Utils;
namespace PluginCore.AspNetCore.BackgroundServices
{
public class PluginTimeJobBackgroundService : TimeBackgroundService
{
/// <summary>
/// 插件与之最近执行时间
/// <para>最近执行时间: 10位秒 时间戳</para>
/// </summary>
private readonly Dictionary<string, long> _pluginAndLastExecuteTimeDic = new Dictionary<string, long>();
private readonly IPluginFinder _pluginFinder;
private static readonly object _doWorklock = new object();
public PluginTimeJobBackgroundService(IPluginFinder pluginFinder)
{
_pluginFinder = pluginFinder;
// 最小间隔 1 秒
_timerPeriod = TimeSpan.FromSeconds(1);
}
protected override void DoWork(object state)
{
lock (_doWorklock)
{
//Console.WriteLine("Memory used before collection: {0:N0}", GC.GetTotalMemory(false));
var plugins = this._pluginFinder.EnablePlugins<ITimeJobPlugin>().ToList();
List<string> enabledPluginKeyList = new List<string>();
foreach (var item in plugins)
{
string pluginKey = item.GetType().ToString();
enabledPluginKeyList.Add(pluginKey);
if (this._pluginAndLastExecuteTimeDic.ContainsKey(pluginKey))
{
long lastExecuteTime = this._pluginAndLastExecuteTimeDic[pluginKey];
long nowTime = DateTime.Now.ToTimeStamp10();
if (nowTime - lastExecuteTime >= item.SecondsPeriod)
{
// 调用
Utils.LogUtil.Info<PluginTimeJobBackgroundService>($"{pluginKey}: {nameof(ITimeJobPlugin)}.{nameof(ITimeJobPlugin.ExecuteAsync)}");
Task task = item?.ExecuteAsync();
this._pluginAndLastExecuteTimeDic[pluginKey] = DateTime.Now.ToTimeStamp10();
}
}
else
{
// 调用
Utils.LogUtil.Info<PluginTimeJobBackgroundService>($"{pluginKey}: {nameof(ITimeJobPlugin)}.{nameof(ITimeJobPlugin.ExecuteAsync)}");
Task task = item?.ExecuteAsync();
this._pluginAndLastExecuteTimeDic.Add(pluginKey, DateTime.Now.ToTimeStamp10());
}
}
// 所有插件遍历结束
// 出现在了 _pluginAndLastExecuteTimeDic 中,但没有出现在 enabledPluginKeyList, 说明为之前启用过,但现在已禁用的插件,需要去除掉
List<string> keys = this._pluginAndLastExecuteTimeDic.Select(m => m.Key).ToList();
foreach (string key in keys)
{
if (!enabledPluginKeyList.Contains(key))
{
this._pluginAndLastExecuteTimeDic.Remove(key);
}
}
GC.Collect();
GC.WaitForPendingFinalizers();
//Console.WriteLine("Memory used after full collection: {0:N0}", GC.GetTotalMemory(true));
}
}
}
}

View File

@ -0,0 +1,47 @@
//===================================================
// License: GNU LGPLv3
// Contributors: yiyungent@gmail.com
// Project: https://yiyungent.github.io/PluginCore
// GitHub: https://github.com/yiyungent/PluginCore
//===================================================
using Microsoft.Extensions.Hosting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace PluginCore.AspNetCore.BackgroundServices
{
public abstract class TimeBackgroundService : IHostedService, IDisposable
{
protected Timer _timer;
protected TimeSpan _timerPeriod;
public virtual Task StartAsync(CancellationToken cancellationToken)
{
_timer = new Timer(DoWork, null, TimeSpan.Zero, _timerPeriod);
return Task.CompletedTask;
}
public virtual Task StopAsync(CancellationToken cancellationToken)
{
_timer?.Change(Timeout.Infinite, 0);
return Task.CompletedTask;
}
protected abstract void DoWork(object state);
public virtual void Dispose()
{
_timer?.Dispose();
}
}
}

View File

@ -0,0 +1,280 @@
# Changelog
All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines.
---
## [unreleased]
### Documentation
- **(CHANGELOG.md)** update - ([85bf0d9](https://github.com/yiyungent/PluginCore/commit/85bf0d9bd8e2f9efa7e8c0db3127ec8aaa054bc8)) - github-actions[bot]
- **(CHANGELOG.md)** update - ([a329e4e](https://github.com/yiyungent/PluginCore/commit/a329e4e7ec4048ec445e826f4536402abd832e46)) - github-actions[bot]
- **(CHANGELOG.md)** update - ([0bbfc89](https://github.com/yiyungent/PluginCore/commit/0bbfc8955b7f6338db2125c78ec250e9eeeadcce)) - github-actions[bot]
- **(CHANGELOG.md)** update - ([4f6b47b](https://github.com/yiyungent/PluginCore/commit/4f6b47b3f86bfce4a8f660166837a7322c568d78)) - github-actions[bot]
### Miscellaneous Chores
- **(src/plugincore.aspnetcore)** cliff.toml, CHANGELOG.md - ([bf95c28](https://github.com/yiyungent/PluginCore/commit/bf95c287ae9902608c4711e4dec05575c2d0e794)) - yiyun
---
## [PluginCore.AspNetCore-v1.4.3](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v1.4.2..PluginCore.AspNetCore-v1.4.3) - 2024-08-31
### Bug Fixes
- **(PluginCore.AspNetCore/Controllers/PluginsController)** swagger [FromForm] - ([241d9a7](https://github.com/yiyungent/PluginCore/commit/241d9a72973d9cf1a11c11264a91e9370b2a6cda)) - yiyun
### Features
- **(src/plugincore.aspnetcore/extensions/plugincorestartupextensions.cs)** 插件 wwwroot 默认页 指定, 无需再手动 - ([469ae81](https://github.com/yiyungent/PluginCore/commit/469ae81fc31adce0c0b0701e2dfa1d98634ee184)) - yiyun
### Miscellaneous Chores
- **(PluginCore.AspNetCore.csproj)** 1.4.2 -> 1.4.3 - ([1924e0c](https://github.com/yiyungent/PluginCore/commit/1924e0c179aaac1a1d1f57a8bdebc7578d0b7650)) - yiyun
---
## [PluginCore.AspNetCore-v1.4.2](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v1.4.1..PluginCore.AspNetCore-v1.4.2) - 2024-04-06
### Features
- **(src/plugincore.aspnetcore/backgroundservices/plugintimejobbackgroundservice.cs)** log - ([5da04c2](https://github.com/yiyungent/PluginCore/commit/5da04c20b57a166811ef8af828c5fd0bafab844c)) - yiyun
- **(src/plugincore.aspnetcore/extensions/plugincorestartupextensions.cs)** plugin:startup->appstart - ([71cafe7](https://github.com/yiyungent/PluginCore/commit/71cafe7fa7348b4d8ccb0b342f31ff848c6e77e9)) - yiyun
### Build
- **(src/plugincore.aspnetcore/plugincore.aspnetcore.csproj)** 1.4.1 -> 1.4.2 - ([75f17be](https://github.com/yiyungent/PluginCore/commit/75f17becb01b6d833759811e65cf464ea7a745b1)) - yiyun
---
## [PluginCore.AspNetCore-v1.4.1](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v1.4.0..PluginCore.AspNetCore-v1.4.1) - 2024-03-14
### Bug Fixes
- **(src/plugincore.aspnetcore/extensions/plugincorestartupextensions.cs)** log, _serviceProvider - ([7d7a904](https://github.com/yiyungent/PluginCore/commit/7d7a904fd1794e26f278655f6f3f286b92bd1492)) - yiyun
### Build
- **(plugincore.aspnetcore.csproj)** 1.4.0 -> 1.4.1 - ([393d861](https://github.com/yiyungent/PluginCore/commit/393d861870c45a70f63421b5451df76e4ed9c808)) - yiyun
---
## [PluginCore.AspNetCore-v1.4.0](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v1.3.4..PluginCore.AspNetCore-v1.4.0) - 2024-02-15
### Features
- **(src/plugincore.aspnetcore)** authentication & Authorize - ([73a673e](https://github.com/yiyungent/PluginCore/commit/73a673e06d2a8d662a44323cf7401e91d814ae90)) - yiyun
- **(src/plugincore.aspnetcore)** 认证与授权: 优化,分离, PluginCoreStartupExtensions 优化 - ([ebab6d8](https://github.com/yiyungent/PluginCore/commit/ebab6d888a303f221e4a45ddaa28107ff4a012c4)) - yiyun
- **(src/plugincore.aspnetcore)** accountManager 部分方法静态化, 提供 HttpContext 传入方式, 相关引用处更新调用 - ([491f334](https://github.com/yiyungent/PluginCore/commit/491f334baabb28e1736b5cc1bd19a0081c54949c)) - yiyun
### Miscellaneous Chores
- **(src/plugincore.aspnetcore/extensions/plugincorestartupextensions.cs)** 代码缩进: 美化 - ([6a03628](https://github.com/yiyungent/PluginCore/commit/6a036289ba6f3ae1592b4e4ed58b4fb1bcdc1306)) - yiyun
### Build
- **(plugincore.aspnetcore.csproj)** <Version>1.3.4</Version> -> <Version>1.4.0</Version> - ([f786f5e](https://github.com/yiyungent/PluginCore/commit/f786f5eeb5e19558e3e73890a42f56ce812784cc)) - yiyun
---
## [PluginCore.AspNetCore-v1.3.4](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v1.3.3..PluginCore.AspNetCore-v1.3.4) - 2023-12-30
### Bug Fixes
- **(src/plugincore.aspnetcore)** 转向/适配 LogUtil - ([1a7c71d](https://github.com/yiyungent/PluginCore/commit/1a7c71d748fd228f1053de67572a04f466012ab8)) - yiyun
- **(src/plugincore.aspnetcore)** 适配: LogUtil - ([f150d07](https://github.com/yiyungent/PluginCore/commit/f150d07b55326133afdfe2c156464605483feb05)) - yiyun
- **(src/plugincore.aspnetcore)** 适配 LogUtil.Error - ([9c82b16](https://github.com/yiyungent/PluginCore/commit/9c82b161c5507ad1a7b78b1191dba208e8f4be45)) - yiyun
- **(src/plugincore.aspnetcore/middlewares/languagemiddleware.cs)** namespace: 语法降级 - ([a1f83fc](https://github.com/yiyungent/PluginCore/commit/a1f83fce8f1dcf2e5ead3b8a15eae006053af4de)) - yiyun
### Documentation
- **(src/plugincore.aspnetcore/readme.txt)** zh -> EN - ([f822b0b](https://github.com/yiyungent/PluginCore/commit/f822b0b79d1afca536bb7e10814226fb65988365)) - yiyun
- **(src/plugincore.aspnetcore/readme.txt)** update - ([0a3210b](https://github.com/yiyungent/PluginCore/commit/0a3210bc073252a1f18d0910dc80aabd1061a66f)) - yiyun
### Features
- **(src/**/*.cs)** // License: Apache-2.0 -> // License: GNU LGPLv3 - ([57366d3](https://github.com/yiyungent/PluginCore/commit/57366d3e2afdb8e20e94851aa8a09f1ee61b6d7e)) - yiyun
- **(src/**/*.cs)** // Project: https://moeci.com/PluginCore -> // Project: https://yiyungent.github.io/PluginCore - ([7420480](https://github.com/yiyungent/PluginCore/commit/742048065978c1b8597fab3d52f011db4247fbda)) - yiyun
- **(src/plugincore.aspnetcore)** use Constants - ([6cd128a](https://github.com/yiyungent/PluginCore/commit/6cd128a2ce6da83f8cfee46ae03a7af44380e791)) - yiyun
- **(src/plugincore.aspnetcore)** languageMiddleware: 当前 Language - ([b0d79e7](https://github.com/yiyungent/PluginCore/commit/b0d79e7a0ae469cc295d87ed8a7c97a355cbc7a1)) - yiyun
- **(src/plugincore.aspnetcore)** 适配: LogUtil - ([b09da39](https://github.com/yiyungent/PluginCore/commit/b09da39199d614f40ec533d7a472069c40121fae)) - yiyun
- **(src/plugincore.aspnetcore)** 认证与授权: 日志输出: 中文->英文 - ([1470a99](https://github.com/yiyungent/PluginCore/commit/1470a9913fff6e0df37495d1143f5a55eed995fd)) - yiyun
- **(src/plugincore.aspnetcore/controllers/pluginscontroller.cs)** 启用,禁用: Message: 使用 BasePlugin 源 - ([d6c8a33](https://github.com/yiyungent/PluginCore/commit/d6c8a3361a7573c68031ad6e8bd8aa35c65035d4)) - yiyun
- **(src/plugincore.aspnetcore/extensions/plugincorestartupextensions.cs)** use Constants - ([68952a1](https://github.com/yiyungent/PluginCore/commit/68952a13613d469cefe9fc9fd13ab7525dd93f78)) - yiyun
- **(src/plugincore.aspnetcore/extensions/plugincorestartupextensions.cs)** use constants - ([37798ef](https://github.com/yiyungent/PluginCore/commit/37798efc92f82e5e5f4d696b6de5466809a18d48)) - yiyun
- **(src/plugincore.aspnetcore/extensions/plugincorestartupextensions.cs)** 初始化: Logger - ([6e24a5a](https://github.com/yiyungent/PluginCore/commit/6e24a5a391af60f8ec0ba85e8ea86ea23c49f522)) - yiyun
- **(src/plugincore.aspnetcore/extensions/plugincorestartupextensions.cs)** log 输出 -> 英文 - ([e41b73a](https://github.com/yiyungent/PluginCore/commit/e41b73adda0ca5d9f1f60109ee8b6fe7de39deda)) - yiyun
### Build
- **(plugincore.aspnetcore.csproj)** 1.3.3 -> 1.3.4 - ([84550cc](https://github.com/yiyungent/PluginCore/commit/84550cc9159c387cedbc8beda282dfcfea9cdcb1)) - yiyun
---
## [PluginCore.AspNetCore-v1.3.3](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v1.3.2..PluginCore.AspNetCore-v1.3.3) - 2023-12-14
### Build
- **(src/plugincore.aspnetcore/plugincore.aspnetcore.csproj)** packageReference update, 1.3.2->1.3.3 - ([6fc3d1a](https://github.com/yiyungent/PluginCore/commit/6fc3d1ad6359a5f8690babfc6949e0821d5bd6c5)) - yiyun
---
## [PluginCore.AspNetCore-v1.3.2](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v1.3.1..PluginCore.AspNetCore-v1.3.2) - 2023-08-21
### Features
- **(src/plugincore.aspnetcore/extensions/plugincorestartupextensions.cs)** services: PluginFinder - ([144db05](https://github.com/yiyungent/PluginCore/commit/144db05576001a72f9cbd6beea0b3b5baa6a082c)) - yiyun
### Build
- **(src/plugincore.aspnetcore/plugincore.aspnetcore.csproj)** <Version>1.3.2</Version> - ([dfdd17d](https://github.com/yiyungent/PluginCore/commit/dfdd17dbcb2fc636cc102ab76497ff5768c64a4e)) - yiyun
---
## [PluginCore.AspNetCore-v1.3.1](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v1.3.0..PluginCore.AspNetCore-v1.3.1) - 2023-02-15
### Build
- **(plugincore.aspnetcore.csproj)** `<Version>1.3.1</Version>` - ([39e0d03](https://github.com/yiyungent/PluginCore/commit/39e0d037c6f3e0dcf4845b5ca8ae2aa41142a474)) - yiyun
---
## [PluginCore.AspNetCore-v1.3.0](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v1.2.0..PluginCore.AspNetCore-v1.3.0) - 2023-02-15
### Bug Fixes
- **(plugincore.aspnetcore,plugincore)** iList<string> EnabledPlugins->List<string>,IList不支持Remove - ([4d5d30e](https://github.com/yiyungent/PluginCore/commit/4d5d30e66c4c28998a7a6ac96bf3ffb25e4872b4)) - yiyun
### Features
- **(plugincore.aspnetcore,plugincore.iplugins,plugincore)** 仅保留已启用/已禁用 状态, IPlugin新方法 - ([e843a5b](https://github.com/yiyungent/PluginCore/commit/e843a5ba9fad4e88290c09bb3282b730c44c5a06)) - yiyun
### Build
- **(src/plugincore.aspnetcore/plugincore.aspnetcore.csproj)** `<Version>1.3.0</Version>` - ([64b9775](https://github.com/yiyungent/PluginCore/commit/64b977569312539aa2775235ae1f0fca5517ddcd)) - yiyun
---
## [PluginCore.AspNetCore-v1.2.0](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v1.1.0..PluginCore.AspNetCore-v1.2.0) - 2023-02-14
### Features
- **(src/plugincore.aspnetcore/controllers/debugcontroller.cs)** pluginContexts:PluginId - ([f82b7b1](https://github.com/yiyungent/PluginCore/commit/f82b7b199420b15fc6f38e8f26c8094ee4be1b88)) - yiyun
### Build
- **(src/plugincore.aspnetcore/plugincore.aspnetcore.csproj)** <Version>1.2.0</Version> - ([fe418e0](https://github.com/yiyungent/PluginCore/commit/fe418e0f67dd8ed1fd2d78dc3f4106c8dc1a396b)) - yiyun
---
## [PluginCore.AspNetCore-v1.1.0](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v1.0.4..PluginCore.AspNetCore-v1.1.0) - 2023-02-10
### Features
- **(src/plugincore.aspnetcore/)** update - ([52ee48f](https://github.com/yiyungent/PluginCore/commit/52ee48fe234040fd0cc0a4d21f3c2e1c9483735e)) - yiyun
- **(src/plugincore.aspnetcore/controllers/debugcontroller.cs)** add - ([196823d](https://github.com/yiyungent/PluginCore/commit/196823d6e3b39eb70d1d4bb55d7d4c8433ee64fc)) - yiyun
- **(src/plugincore.aspnetcore/controllers/debugcontroller.cs)** 完成 - ([2a2e213](https://github.com/yiyungent/PluginCore/commit/2a2e2131ee4b25912c16ca0d01ef408702cbfc39)) - yiyun
- **(src/plugincore.aspnetcore/controllers/debugcontroller.cs)** services - ([22edc22](https://github.com/yiyungent/PluginCore/commit/22edc229c1bedc4667a920ef62c11c8e002ba9d2)) - yiyun
### Build
- **(src/plugincore.aspnetcore/plugincore.aspnetcore.csproj)** <Version>1.1.0</Version> - ([3bdb176](https://github.com/yiyungent/PluginCore/commit/3bdb17602c3258386711b200917e567c82dc442b)) - yiyun
---
## [PluginCore.AspNetCore-v1.0.4](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v1.0.3..PluginCore.AspNetCore-v1.0.4) - 2023-01-12
### Build
- **(plugincore.aspnetcore.csproj)** <Version>1.0.4</Version> - ([46cf5dd](https://github.com/yiyungent/PluginCore/commit/46cf5dd5f955f6b648323e5216af97dd177e5a4b)) - yiyun
---
## [PluginCore.AspNetCore-v1.0.3](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v1.0.2..PluginCore.AspNetCore-v1.0.3) - 2022-06-03
### Bug Fixes
- **(backgroundservices/plugintimejobbackgroundservice.cs)** 定时任务:强制GC回收,抑制内存++ - ([434e824](https://github.com/yiyungent/PluginCore/commit/434e82403b6aa2050945eeaa1131ea7965bdbc5f)) - yiyun
### Build
- **(plugincore.aspnetcore.csproj)** <Version>1.0.3</Version> - ([7da5638](https://github.com/yiyungent/PluginCore/commit/7da5638672571ca0ef057ebf73493260886b903e)) - yiyun
---
## [PluginCore.AspNetCore-v1.0.2](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v1.0.1..PluginCore.AspNetCore-v1.0.2) - 2022-04-19
### Features
- **(plugincore.aspnetcore)** pluginsController: 移除: 插件上传大小限制 - ([90f8d67](https://github.com/yiyungent/PluginCore/commit/90f8d671a840ae8ca3688f4a5cbc22e568a6159e)) - yiyun
### Style
- add: copyright: *.cs - ([9643dce](https://github.com/yiyungent/PluginCore/commit/9643dce112861a440d63306cb555accbed3d5111)) - yiyun
### Build
- **(plugincore.aspnetcore.csproj)** <Version>1.0.2</Version> - ([b4eb1cc](https://github.com/yiyungent/PluginCore/commit/b4eb1cca9039ff95a516ba6d4000f10d156e03d0)) - yiyun
- **(plugincore.aspnetcore/package.json,package-lock.json)** "plugincore-admin-frontend": "0.3.2" - ([7a70b20](https://github.com/yiyungent/PluginCore/commit/7a70b2003128bf569b4fdd4b7eaa662a92da8ee8)) - yiyun
---
## [PluginCore.AspNetCore-v1.0.1](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v1.0.0..PluginCore.AspNetCore-v1.0.1) - 2022-04-17
### Build
- **(plugincore.aspnetcore.csproj)** <Version>1.0.1</Version> - ([ab348ce](https://github.com/yiyungent/PluginCore/commit/ab348ceca937f1b1f4ba8c83f4816b9a0ae7ea3e)) - yiyun
---
## [PluginCore.AspNetCore-v1.0.0](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v0.0.5..PluginCore.AspNetCore-v1.0.0) - 2022-04-17
### Bug Fixes
- **(plugincore)** 临时修复由于 PluginContextManager 单例失败 导致的插件信息丢失 - ([fa613b4](https://github.com/yiyungent/PluginCore/commit/fa613b4c46e41c906fe955eb8f62c3f4937795bc)) - yiyun
### Features
- **(plugincore,plugincore.aspnetcore)** aspNetCorePluginManagerBeta,PluginLoadContext,PluginFinder - ([9d65a59](https://github.com/yiyungent/PluginCore/commit/9d65a590e3e0850251f6d815c322c7c5d9c7cf3f)) - yiyun
- **(plugincore.aspnetcore)** add:DebugController.PluginContext - ([cd8de63](https://github.com/yiyungent/PluginCore/commit/cd8de636c8d7e2b125e11c6ce4091567cb97d4bc)) - yiyun
- **(plugincore.aspnetcore)** commonResponseModel -> BaseResponseModel - ([1a0e834](https://github.com/yiyungent/PluginCore/commit/1a0e834b7dfdb45c45770d42b1733f1cb449a6ca)) - yiyun
### Refactoring
- **(plugincore.aspnetcore,plugincore)** 未完成 - ([a151bcd](https://github.com/yiyungent/PluginCore/commit/a151bcda125cb7e9b5fe11d44e1389afa7a1db5e)) - yiyun
- **(plugincore.aspnetcore,plugincore)** 重构v2: 未测试 - ([53dde31](https://github.com/yiyungent/PluginCore/commit/53dde31116bd6455d33f7d7006b6fd1430f3694b)) - yiyun
- **(plugincore.aspnetcore,plugincore)** 变量名,属性名,类名规范化 - ([eaadabf](https://github.com/yiyungent/PluginCore/commit/eaadabfd759228da245af1d9bd5b86e557540d28)) - yiyun
### Build
- **(plugincore.aspnetcore.csproj)** <Version>1.0.0</Version> - ([9b5caf4](https://github.com/yiyungent/PluginCore/commit/9b5caf4c4cdc543025c5493d35275a836cae61a9)) - yiyun
---
## [PluginCore.AspNetCore-v0.0.5](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v0.0.4..PluginCore.AspNetCore-v0.0.5) - 2022-04-16
### Build
- **(plugincore.aspnetcore.csproj)** <Version>0.0.5</Version> - ([3e5cb09](https://github.com/yiyungent/PluginCore/commit/3e5cb0921a9faa33f93169c2d983011a1e8c5b5f)) - yiyun
---
## [PluginCore.AspNetCore-v0.0.4](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v0.0.3..PluginCore.AspNetCore-v0.0.4) - 2022-04-16
### Build
- **(plugincore.aspnetcore.csproj)** <Version>0.0.4</Version> - ([adffab3](https://github.com/yiyungent/PluginCore/commit/adffab3fecee6696709c1d325b8c11e5a5389031)) - yiyun
---
## [PluginCore.AspNetCore-v0.0.3](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v0.0.2..PluginCore.AspNetCore-v0.0.3) - 2022-04-16
### Bug Fixes
- **(plugincore.aspnetcore/extensions/plugincorestartupextensions.cs)** pluginFinder:TryAddTransient - ([03e9235](https://github.com/yiyungent/PluginCore/commit/03e9235c3c7fe68a3209cb1109792869e78aaa4e)) - yiyun
- **(plugincore.aspnetcore/extensions/plugincorestartupextensions.cs)** 注释错误: 在程序启动时加载所有 已安装并启用 的插件 - ([844826a](https://github.com/yiyungent/PluginCore/commit/844826a630f99c2d866a98e6556404d4b1857e52)) - yiyun
### Build
- **(plugincore.aspnetcore.csproj)** <Version>0.0.3</Version> - ([ee81a45](https://github.com/yiyungent/PluginCore/commit/ee81a4536af17a85bbebc07bc0c980af8c00ad63)) - yiyun
---
## [PluginCore.AspNetCore-v0.0.2](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v0.0.1..PluginCore.AspNetCore-v0.0.2) - 2022-04-16
### Refactoring
- 1.提取出 PluginCore.AspNetCore,PluginCore.IPlugins.AspNetCore 2.提取出更多接口,可自由替换 - ([fffd8d9](https://github.com/yiyungent/PluginCore/commit/fffd8d91c23fd6e4a4d09cbf91975beb3cf7acf0)) - yiyun
### Build
- **(plugincore.aspnetcore.csproj)** <Version>0.0.2</Version> - ([ba7b274](https://github.com/yiyungent/PluginCore/commit/ba7b27496a4ba784c271305489b9f2f8e6f74ad3)) - yiyun
---
## [PluginCore.AspNetCore-v0.0.1](https://github.com/yiyungent/PluginCore/compare/PluginCore-v0.9.3..PluginCore.AspNetCore-v0.0.1) - 2022-03-26
### Features
- **(plugincore.aspnetcore)** pluginCore.AspNetCore,PluginCore.AspNetCore-nuget-push.yml - ([491f63e](https://github.com/yiyungent/PluginCore/commit/491f63e3362129a2239d87b090aa04cc2e414e9a)) - yiyun
<!-- generated by git-cliff -->

View File

@ -0,0 +1,207 @@
//===================================================
// License: GNU LGPLv3
// Contributors: yiyungent@gmail.com
// Project: https://yiyungent.github.io/PluginCore
// GitHub: https://github.com/yiyungent/PluginCore
//===================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using PluginCore;
using PluginCore.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using PluginCore.AspNetCore.Authorization;
using PluginCore.AspNetCore.ResponseModel;
//using ResponseModel;
namespace PluginCore.AspNetCore.Controllers
{
/// <summary>
/// 应用中心
/// <para>插件</para>
/// </summary>
[Route("api/plugincore/admin/[controller]/[action]")]
[PluginCoreAdminAuthorize]
[ApiController]
public class AppCenterController : ControllerBase
{
#region Fields
private static Dictionary<string, Task> _pluginDownloadTasks;
#endregion
#region Ctor
static AppCenterController()
{
_pluginDownloadTasks = new Dictionary<string, Task>();
}
public AppCenterController()
{
}
#endregion
#region Actions
#region
/// <summary>
/// 插件
/// </summary>
/// <param name="query"></param>
/// <returns></returns>
[HttpGet, HttpPost]
public async Task<ActionResult<BaseResponseModel>> Plugins(string query = "")
{
BaseResponseModel responseDTO = new BaseResponseModel();
IList<PluginRegistryResponseModel> pluginRegistryModels = new List<PluginRegistryResponseModel>();
try
{
// 1. TODO: 从json文件中读取插件订阅源 registry url
string registryUrl = "";
// 2. TODO: 向订阅源发送 http get 获取插件列表信息 eg: http://rem-core-plugins-registry.moeci.com/?query=xxx
IList<string> remotePluginIds = new List<string>();
// 3. 根据本地已有 PluginId 插件情况 状态赋值
PluginConfigModel pluginConfigModel = PluginConfigModelFactory.Create();
// IList<string> localPluginIds = pluginConfigModel.EnabledPlugins.Concat(pluginConfigModel.DisabledPlugins).Concat(pluginConfigModel.UninstalledPlugins).ToList();
IList<string> localPluginIds = PluginPathProvider.AllPluginFolderName();
responseDTO.Code = 1;
responseDTO.Message = "获取远程插件数据成功";
responseDTO.Data = pluginRegistryModels;
}
catch (Exception ex)
{
responseDTO.Code = -1;
responseDTO.Message = "获取远程插件数据失败: " + ex.Message;
responseDTO.Data = pluginRegistryModels;
}
return await Task.FromResult(responseDTO);
}
#endregion
#region
[HttpGet, HttpPost]
public async Task<ActionResult<BaseResponseModel>> DownloadPlugin(string pluginDownloadUrl = "")
{
BaseResponseModel responseDTO = new BaseResponseModel();
#region
if (string.IsNullOrEmpty(pluginDownloadUrl))
{
responseDTO.Code = -1;
responseDTO.Message = "插件下载地址不正确";
return responseDTO;
}
// TODO: 效验是否本地已经存在相同pluginId的插件
#endregion
try
{
// 1.执行下载操作, TODO:存在问题,阻塞对性能不好,但不阻塞又不好通知用户插件下载进度,以及可能存在在插件下载过程中,用户再次点击下载
WebClient webClient = new WebClient();
// TODO: 插件下载文件路径
string pluginDownloadFilePath = "";
//webClient.DownloadFileAsync(new Uri(pluginDownloadFilePath), "");
Task task = webClient.DownloadFileTaskAsync(pluginDownloadUrl, pluginDownloadFilePath);
_pluginDownloadTasks.Add(pluginDownloadUrl, task);
webClient.DownloadFileCompleted += Plugin_DownloadFileCompleted;
webClient.DownloadProgressChanged += Plugin_DownloadProgressChanged;
webClient.Disposed += WebClient_Disposed;
responseDTO.Code = 1;
responseDTO.Message = "开始下载插件";
}
catch (Exception ex)
{
responseDTO.Code = -1;
responseDTO.Message = "下载插件失败: " + ex.Message;
}
return await Task.FromResult(responseDTO);
}
#endregion
#region
[HttpGet, HttpPost]
public async Task<ActionResult<BaseResponseModel>> DownloadPluginProgress()
{
BaseResponseModel responseDTO = new BaseResponseModel();
try
{
responseDTO.Data = new { };
responseDTO.Code = 1;
responseDTO.Message = "获取插件下载进度成功";
}
catch (Exception ex)
{
responseDTO.Code = -1;
responseDTO.Message = "获取插件下载进度失败: " + ex.Message;
}
return await Task.FromResult(responseDTO);
}
#endregion
#endregion
#region Helpers
/// <summary>
/// 插件下载完成
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Plugin_DownloadFileCompleted(object sender, System.ComponentModel.AsyncCompletedEventArgs e)
{
Utils.LogUtil.Info<AppCenterController>("插件下载完成");
// 1.从 _pluginDownloadTasks 中移除
//_pluginDownloadTasks.Remove();
// 2. 解压插件
}
private void Plugin_DownloadProgressChanged(object sender, DownloadProgressChangedEventArgs e)
{
Utils.LogUtil.Info<AppCenterController>($"插件下载进度改变: {e.ProgressPercentage}% {e.BytesReceived}/{e.TotalBytesToReceive}");
}
private void WebClient_Disposed(object sender, EventArgs e)
{
if (sender is WebClient webClient)
{
Utils.LogUtil.Info<AppCenterController>(webClient.BaseAddress);
}
Utils.LogUtil.Info<AppCenterController>(nameof(WebClient_Disposed) + ": " + sender.ToString());
}
#endregion
}
}

View File

@ -0,0 +1,280 @@
using System.Runtime.CompilerServices;
//===================================================
// License: GNU LGPLv3
// Contributors: yiyungent@gmail.com
// Project: https://yiyungent.github.io/PluginCore
// GitHub: https://github.com/yiyungent/PluginCore
//===================================================
using Microsoft.AspNetCore.Mvc;
using PluginCore.AspNetCore.Authorization;
using PluginCore.AspNetCore.ResponseModel;
using PluginCore.Interfaces;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using System.Linq;
using System.Runtime.Loader;
using Microsoft.Extensions.DependencyInjection;
using System.Collections.Concurrent;
using PluginCore.AspNetCore.Extensions;
namespace PluginCore.AspNetCore.Controllers
{
/// <summary>
/// [ASP.NET Core — 依赖注入\_啊晚的博客-CSDN博客\_asp.net core 依赖注入](https://blog.csdn.net/weixin_37648525/article/details/127942292)
/// [ASP.NET Core中的依赖注入3: 服务的注册与提供 - Artech - 博客园](https://www.cnblogs.com/artech/p/asp-net-core-di-register.html)
/// [ASP.NET Core中的依赖注入5: ServiceProvider实现揭秘 【总体设计 】 - Artech - 博客园](https://www.cnblogs.com/artech/p/asp-net-core-di-service-provider-1.html)
/// [dotnet/ServiceProvider.cs at main · dotnet/dotnet](https://github.com/dotnet/dotnet/blob/main/src/runtime/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceProvider.cs)
/// [Net6 DI源码分析Part2 Engine,ServiceProvider - 一身大膘 - 博客园](https://www.cnblogs.com/hts92/p/15800990.html)
/// [【特别的骚气】asp.net core运行时注入服务实现类库热插拔 - 四处观察 - 博客园](https://www.cnblogs.com/1996-Chinese-Chen/p/16154218.html)
///
/// ActivatorUtilities.CreateInstance<PluginCore.IPlugins.IPlugin>(serviceProvider, "test");
/// ActivatorUtilities.GetServiceOrCreateInstance<PluginCore.IPlugins.IPlugin>(serviceProvider);
///
/// </summary>
[Route("api/plugincore/admin/[controller]/[action]")]
[PluginCoreAdminAuthorize]
[ApiController]
public class DebugController : ControllerBase
{
#region Fields
private readonly IPluginContextManager _pluginContextManager;
#endregion
#region Ctor
public DebugController(IPluginContextManager pluginContextManager)
{
_pluginContextManager = pluginContextManager;
}
#endregion
#region Actions
[HttpGet, HttpPost]
public async Task<ActionResult<BaseResponseModel>> PluginContexts()
{
BaseResponseModel responseModel = new BaseResponseModel();
try
{
var pluginContextList = _pluginContextManager.All();
Dictionary<string, List<string>> keyValuePairs = new Dictionary<string, List<string>>();
foreach (var pluginContext in pluginContextList)
{
keyValuePairs.Add($"{pluginContext.GetType().ToString()} - {pluginContext.PluginId} - {pluginContext.GetHashCode()}", pluginContext.Assemblies.Select(m => m.FullName).ToList());
}
responseModel.Code = 1;
responseModel.Message = "success";
responseModel.Data = keyValuePairs;
}
catch (Exception ex)
{
responseModel.Code = -1;
responseModel.Message = "error";
responseModel.Data = ex.ToString();
}
return await Task.FromResult(responseModel);
}
[HttpGet, HttpPost]
public async Task<ActionResult<BaseResponseModel>> AssemblyLoadContexts()
{
BaseResponseModel responseModel = new BaseResponseModel();
try
{
var assemblyLoadContextDefault = AssemblyLoadContext.Default;
var assemblyLoadContextAll = AssemblyLoadContext.All;
var responseDataModel = new AssemblyLoadContextsResponseDataModel();
responseDataModel.Default = new AssemblyLoadContextsResponseDataModel.AssemblyLoadContextModel
{
Name = assemblyLoadContextDefault.Name,
Type = assemblyLoadContextDefault.GetType().ToString(),
Assemblies = assemblyLoadContextDefault.Assemblies.Select(m => new AssemblyModel { FullName = m.FullName, DefinedTypes = m.DefinedTypes.Select(m => m.FullName).ToList() }).ToList()
};
responseDataModel.All = assemblyLoadContextAll.Select(item => new AssemblyLoadContextsResponseDataModel.AssemblyLoadContextModel
{
Name = item.Name,
Type = item.GetType().ToString(),
Assemblies = item.Assemblies.Select(m => new AssemblyModel { FullName = m.FullName, DefinedTypes = m.DefinedTypes.Select(m => m.FullName).ToList() }).ToList()
}).ToList();
responseModel.Code = 1;
responseModel.Message = "success";
responseModel.Data = responseDataModel;
}
catch (Exception ex)
{
responseModel.Code = -1;
responseModel.Message = "error";
responseModel.Data = ex.ToString();
}
return await Task.FromResult(responseModel);
}
[HttpGet, HttpPost]
public async Task<ActionResult<BaseResponseModel>> Assemblies()
{
BaseResponseModel responseModel = new BaseResponseModel();
try
{
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
List<AssemblyModel> assemblyModels = new List<AssemblyModel>();
foreach (var item in assemblies)
{
assemblyModels.Add(new AssemblyModel
{
FullName = item.FullName,
DefinedTypes = item.DefinedTypes.Select(m => m.FullName).ToList()
});
}
responseModel.Code = 1;
responseModel.Message = "success";
responseModel.Data = assemblyModels;
}
catch (Exception ex)
{
responseModel.Code = -1;
responseModel.Message = "error";
responseModel.Data = ex.ToString();
}
return await Task.FromResult(responseModel);
}
[HttpGet, HttpPost]
public async Task<ActionResult<BaseResponseModel>> Services([FromServices] IServiceProvider serviceProvider)
{
BaseResponseModel responseModel = new BaseResponseModel();
try
{
//IServiceProvider serviceProvider = HttpContext.RequestServices;
//var provider = serviceProvider.GetType().GetProperty("RootProvider", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
//var serviceField = provider.GetType().GetField("_realizedServices", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
//var serviceValue = serviceField.GetValue(provider);
//var funcType = serviceField.FieldType.GetGenericArguments()[1].GetGenericArguments()[0];
//ConcurrentDictionary<Type, Func<ServiceProviderEngineScope, object?>> realizedServices = (ConcurrentDictionary<Type, Func<ServiceProviderEngineScope, object?>>)serviceValue;
// 获取所有已经注册的服务
var allService = serviceProvider.GetAllServiceDescriptors();
List<ServiceModel> serviceModels = new List<ServiceModel>();
foreach (var item in allService)
{
serviceModels.Add(new ServiceModel
{
Type = item.Key.ToString(),
ImplementationType = item.Value.ImplementationType?.ToString() ?? "",
Lifetime = item.Value.Lifetime.ToString(),
TypeAssembly = new AssemblyModel
{
FullName = item.Key.Assembly.FullName,
},
ImplementationTypeAssembly = new AssemblyModel
{
FullName = item.Value.ImplementationType?.Assembly?.FullName ?? ""
}
});
}
responseModel.Code = 1;
responseModel.Message = "success";
responseModel.Data = serviceModels;
}
catch (Exception ex)
{
responseModel.Code = -1;
responseModel.Message = "error";
responseModel.Data = ex.ToString();
}
return await Task.FromResult(responseModel);
}
#endregion
public sealed class AssemblyLoadContextsResponseDataModel
{
public AssemblyLoadContextModel Default
{
get; set;
}
public List<AssemblyLoadContextModel> All
{
get; set;
}
public sealed class AssemblyLoadContextModel
{
public string Name
{
get; set;
}
public string Type
{
get; set;
}
public List<AssemblyModel> Assemblies
{
get; set;
}
}
}
public sealed class AssembliesResponseDataModel
{
}
public sealed class ServiceModel
{
public string Type
{
get; set;
}
public string ImplementationType
{
get; set;
}
public string Lifetime
{
get; set;
}
public AssemblyModel TypeAssembly
{
get; set;
}
public AssemblyModel ImplementationTypeAssembly
{
get; set;
}
}
public sealed class AssemblyModel
{
public string FullName
{
get; set;
}
public List<string> DefinedTypes
{
get; set;
}
}
}
}

View File

@ -0,0 +1,73 @@
//===================================================
// License: GNU LGPLv3
// Contributors: yiyungent@gmail.com
// Project: https://yiyungent.github.io/PluginCore
// GitHub: https://github.com/yiyungent/PluginCore
//===================================================
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
namespace PluginCore.AspNetCore.Controllers
{
[Controller]
public class HomeController : Controller
{
#region Old
//private readonly IWebHostEnvironment _webHostEnvironment;
//public bool IsLocalFronted
//{
// get
// {
// return PluginCore.Config.PluginCoreConfigFactory.Create().IsLocalFrontend;
// }
//}
//public string RemoteFronted
//{
// get
// {
// return PluginCore.Config.PluginCoreConfigFactory.Create().RemoteFrontend;
// }
//}
//public HomeController(IWebHostEnvironment webHostEnvironment)
//{
// this._webHostEnvironment = webHostEnvironment;
//}
//[Route("PluginCore/Admin")]
//public async Task<ActionResult> Home()
//{
// if (this.IsLocalFronted)
// {
// var localIndexFilePath = Path.Combine(
// this._webHostEnvironment.ContentRootPath, "PluginCoreAdmin", "index.html");
// return PhysicalFile(localIndexFilePath, "text/html");
// }
// else
// {
// string htmlStr = string.Empty;
// HttpClient httpClient = new HttpClient();
// htmlStr = await httpClient.GetStringAsync(this.RemoteFronted + "/index.html");
// return Content(htmlStr, "text/html", Encoding.UTF8);
// }
//}
#endregion
}
}

View File

@ -0,0 +1,103 @@
//===================================================
// License: GNU LGPLv3
// Contributors: yiyungent@gmail.com
// Project: https://yiyungent.github.io/PluginCore
// GitHub: https://github.com/yiyungent/PluginCore
//===================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using PluginCore;
using PluginCore.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using PluginCore.AspNetCore.Authorization;
using PluginCore.AspNetCore.ResponseModel;
using PluginCore.IPlugins;
using System.Text;
using PluginCore.Interfaces;
namespace PluginCore.AspNetCore.Controllers
{
[Route("api/plugincore/[controller]/[action]")]
[ApiController]
public class PluginWidgetController : ControllerBase
{
#region Fields
private readonly IPluginFinder _pluginFinder;
#endregion
#region Ctor
public PluginWidgetController(IPluginFinder pluginFinder)
{
_pluginFinder = pluginFinder;
}
#endregion
#region Actions
#region Widget
/// <summary>
/// Widget
/// </summary>
/// <returns></returns>
[HttpGet, HttpPost]
//public async Task<ActionResult<CommonResponseModel>> Widget(string widgetKey, string extraPars = "")
public async Task<ActionResult> Widget(string widgetKey, string extraPars = "")
{
BaseResponseModel responseModel = new ResponseModel.BaseResponseModel();
string responseData = "";
widgetKey = widgetKey.Trim('"', '\'');
string[] extraParsArr = null;
if (!string.IsNullOrEmpty(extraPars))
{
extraParsArr = extraPars.Split(",", StringSplitOptions.RemoveEmptyEntries);
extraParsArr = extraParsArr.Select(m => m.Trim('"', '\'')).ToArray();
}
StringBuilder sb = new StringBuilder();
sb.AppendLine($"<!-- start:PluginCore.IPlugins.IWidgetPlugin.Widget({widgetKey},{extraPars}) -->");
try
{
List<IWidgetPlugin> plugins = this._pluginFinder.EnablePlugins<IWidgetPlugin>().ToList();
foreach (var item in plugins)
{
string widgetStr = await item.Widget(widgetKey, extraParsArr);
if (!string.IsNullOrEmpty(widgetStr))
{
// TODO: 配合 PluginCoreConfig.PluginWidgetDebug
// TODO: PluginCoreConfig 改为 Options 模式, 避免手动反复读取文件 效率低
//sb.AppendLine($"<!-- {item.GetType().ToString()}: -->");
sb.AppendLine(widgetStr);
}
}
}
catch (Exception ex)
{
Utils.LogUtil.Error<PluginWidgetController>(ex, ex.Message);
sb.AppendLine($"<!-- Exception: {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}, Details: Console -->");
}
sb.AppendLine($"<!-- end:PluginCore.IPlugins.IWidgetPlugin.Widget({widgetKey},{extraPars}) -->");
responseData = sb.ToString();
responseModel.Code = 1;
responseModel.Message = "Load Widget Success";
responseModel.Data = responseData;
//return await Task.FromResult(responseModel);
return Content(responseData, "text/html;charset=utf-8");
}
#endregion
#endregion
}
}

View File

@ -0,0 +1,688 @@
//===================================================
// License: GNU LGPLv3
// Contributors: yiyungent@gmail.com
// Project: https://yiyungent.github.io/PluginCore
// GitHub: https://github.com/yiyungent/PluginCore
//===================================================
using System;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
//using Core.Common;
//using Framework.Authorization;
using PluginCore;
using PluginCore.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using PluginCore.AspNetCore.Authorization;
using PluginCore.Infrastructure;
using PluginCore.IPlugins;
using PluginCore.AspNetCore.ResponseModel;
using PluginCore.Interfaces;
using PluginCore.AspNetCore.Interfaces;
//using ResponseModel;
namespace PluginCore.AspNetCore.Controllers
{
[Route("api/plugincore/admin/[controller]/[action]")]
[PluginCoreAdminAuthorize]
[ApiController]
public class PluginsController : ControllerBase
{
#region Fields
private readonly IPluginManager _pluginManager;
private readonly IPluginFinder _pluginFinder;
private readonly IPluginApplicationBuilderManager _pluginApplicationBuilderManager;
#endregion
#region Ctor
public PluginsController(IPluginManager pluginManager, IPluginFinder pluginFinder, IPluginApplicationBuilderManager pluginApplicationBuilderManager)
{
_pluginManager = pluginManager;
_pluginFinder = pluginFinder;
_pluginApplicationBuilderManager = pluginApplicationBuilderManager;
}
#endregion
#region Actions
#region
/// <summary>
/// 加载插件列表
/// </summary>
/// <param name="status">插件状态</param>
/// <returns></returns>
[HttpGet, HttpPost]
public async Task<ActionResult<BaseResponseModel>> List(string status = "all")
{
BaseResponseModel responseData = new ResponseModel.BaseResponseModel();
var pluginConfigModel = PluginConfigModelFactory.Create();
// 获取所有插件信息
IList<PluginInfoModel> pluginInfoModels = PluginInfoModelFactory.CreateAll();
IList<PluginInfoResponseModel> responseModels = new List<PluginInfoResponseModel>();
string[] enablePluginIds = _pluginFinder.EnablePluginIds().ToArray();
// 添加插件状态
responseModels = PluginInfoModelToResponseModel(pluginInfoModels, pluginConfigModel, enablePluginIds);
#region
switch (status.ToLower())
{
case "all":
break;
case "enabled":
responseModels = responseModels.Where(m => m.Status == PluginStatus.Enabled).ToList();
break;
case "disabled":
responseModels = responseModels.Where(m => m.Status == PluginStatus.Disabled).ToList();
break;
default:
break;
}
#endregion
responseData.Code = 1;
responseData.Message = "加载插件列表成功";
responseData.Data = responseModels;
return await Task.FromResult(responseData);
}
#endregion
#region
[HttpGet, HttpPost]
public async Task<ActionResult<BaseResponseModel>> Uninstall(string pluginId)
{
BaseResponseModel responseData = new BaseResponseModel();
pluginId = pluginId.Trim();
var pluginConfigModel = PluginConfigModelFactory.Create();
// 卸载插件 必须 先禁用插件
#region
if (pluginConfigModel.EnabledPlugins.Contains(pluginId))
{
responseData.Code = -1;
responseData.Message = "卸载失败: 请先禁用此插件";
return await Task.FromResult(responseData);
}
string pluginDirStr= Path.Combine(PluginPathProvider.PluginsRootPath(), pluginId);
string pluginWwwrootDirStr = Path.Combine(PluginPathProvider.PluginsWwwRootDir(), pluginId);
if (!Directory.Exists(pluginDirStr) && !Directory.Exists(pluginWwwrootDirStr))
{
responseData.Code = -2;
responseData.Message = "卸载失败: 此插件不存在";
return await Task.FromResult(responseData);
}
#endregion
try
{
// PS:卸载插件必须先禁用插件所以此时插件LoadContext已被移除释放(插件Assemblies已被释放), 此处不需移除LoadContext
// 1.删除物理文件
var pluginDir = new DirectoryInfo(pluginDirStr);
if (pluginDir.Exists) {
pluginDir.Delete(true);
}
// 虽然 已禁用 时 pluginWwwrootDirStr/pluginId 已删除, 但为确保, 还是再删除一次
var pluginWwwrootDir = new DirectoryInfo(pluginWwwrootDirStr);
if (pluginWwwrootDir.Exists) {
pluginWwwrootDir.Delete(true);
}
responseData.Code = 1;
responseData.Message = "卸载成功";
}
catch (Exception ex)
{
responseData.Code = -1;
responseData.Message = "卸载失败: " + ex.Message;
}
return await Task.FromResult(responseData);
}
#endregion
#region
[HttpGet, HttpPost]
public async Task<ActionResult<BaseResponseModel>> Enable(string pluginId)
{
BaseResponseModel responseData = new BaseResponseModel();
var pluginConfigModel = PluginConfigModelFactory.Create();
// 效验是否存在于 已禁用插件列表
#region
pluginId = pluginId.Trim();
var pluginDir = new DirectoryInfo(Path.Combine(PluginPathProvider.PluginsRootPath(), pluginId));
if (pluginDir != null && !pluginDir.Exists)
{
responseData.Code = -1;
responseData.Message = "启用失败: 此插件不存在";
return await Task.FromResult(responseData);
}
string[] enablePluginIds = _pluginFinder.EnablePluginIds().ToArray();
if (enablePluginIds.Contains(pluginId)) {
responseData.Code = -2;
responseData.Message = "启用失败: 此插件已启用";
return await Task.FromResult(responseData);
}
#endregion
try
{
// 1. 创建插件程序集加载上下文, 添加到 PluginsLoadContexts
_pluginManager.LoadPlugin(pluginId);
// 2. 添加到 pluginConfigModel.EnabledPlugins
pluginConfigModel.EnabledPlugins.Add(pluginId);
// 4.保存到 plugin.config.json
PluginConfigModelFactory.Save(pluginConfigModel);
// 5. 找到此插件实例
IPlugin plugin = _pluginFinder.Plugin(pluginId);
if (plugin == null)
{
// 7.启用不成功, 回滚插件状态: (1)释放插件上下文 (2)更新 plugin.config.json
try
{
_pluginManager.UnloadPlugin(pluginId);
}
catch (Exception ex)
{ }
// 从 pluginConfigModel.EnabledPlugins 移除
pluginConfigModel.EnabledPlugins.Remove(pluginId);
// 保存到 plugin.config.json
PluginConfigModelFactory.Save(pluginConfigModel);
responseData.Code = -1;
responseData.Message = "启用失败: 此插件不存在";
return await Task.FromResult(responseData);
}
// 6.调取插件的 AfterEnable(), 插件开发者可在此回收资源
var pluginEnableResult = plugin.AfterEnable();
if (!pluginEnableResult.IsSuccess)
{
// 7.启用不成功, 回滚插件状态: (1)释放插件上下文 (2)更新 plugin.config.json
try
{
_pluginManager.UnloadPlugin(pluginId);
}
catch (Exception ex)
{ }
// 从 pluginConfigModel.EnabledPlugins 移除
pluginConfigModel.EnabledPlugins.Remove(pluginId);
// 保存到 plugin.config.json
PluginConfigModelFactory.Save(pluginConfigModel);
responseData.Code = -1;
responseData.Message = "启用失败: 来自插件的错误信息: " + pluginEnableResult.Message;
return await Task.FromResult(responseData);
}
// 7. ReBuild
this._pluginApplicationBuilderManager.ReBuild();
// 8. 尝试复制 插件下的 wwwroot 到 Plugins_wwwroot
string wwwRootDir = PluginPathProvider.WwwRootDir(pluginId);
if (Directory.Exists(wwwRootDir))
{
string targetDir = PluginPathProvider.PluginWwwRootDir(pluginId);
Utils.FileUtil.CopyFolder(wwwRootDir, targetDir);
}
responseData.Code = 1;
// responseData.Message = "启用成功";
responseData.Message = pluginEnableResult.Message;
}
catch (Exception ex)
{
responseData.Code = -2;
responseData.Message = "启用失败: " + ex.Message;
Utils.LogUtil.Error<PluginsController>(ex, ex.Message);
}
return await Task.FromResult(responseData);
}
#endregion
#region
[HttpGet, HttpPost]
public async Task<ActionResult<BaseResponseModel>> Disable(string pluginId)
{
BaseResponseModel responseData = new BaseResponseModel();
#region
pluginId = pluginId.Trim();
var pluginConfigModel = PluginConfigModelFactory.Create();
// string[] enablePluginIds = _pluginFinder.EnablePluginIds().ToArray();
// // 效验是否存在于 已启用插件列表
// if (!enablePluginIds.Contains(pluginId))
// {
// responseData.Code = -1;
// responseData.Message = "禁用失败: 此插件不存在, 或未启用";
// return await Task.FromResult(responseData);
// }
#endregion
try
{
// 1. 找到此插件实例
IPlugin plugin = _pluginFinder.Plugin(pluginId);
if (plugin == null)
{
responseData.Code = -1;
responseData.Message = "禁用失败: 此插件不存在, 或未启用";
return await Task.FromResult(responseData);
}
string pluginDisableResultMessage = "";
try
{
// 2.调取插件的 BeforeDisable(), 插件开发者可在此回收资源
var pluginDisableResult = plugin.BeforeDisable();
pluginDisableResultMessage = pluginDisableResult.Message;
if (!pluginDisableResult.IsSuccess)
{
responseData.Code = -1;
responseData.Message = "禁用失败: 来自插件的错误信息: " + pluginDisableResult.Message;
return await Task.FromResult(responseData);
}
// 3.移除插件对应的程序集加载上下文
_pluginManager.UnloadPlugin(pluginId);
// 3.1. ReBuild
this._pluginApplicationBuilderManager.ReBuild();
if (pluginConfigModel.EnabledPlugins.Contains(pluginId)) {
// 4.从 pluginConfigModel.EnabledPlugins 移除
pluginConfigModel.EnabledPlugins.Remove(pluginId);
// 5.保存到 plugin.config.json
PluginConfigModelFactory.Save(pluginConfigModel);
}
}
catch (Exception ex)
{
Utils.LogUtil.Error<PluginsController>(ex, ex.Message);
responseData.Code = -1;
responseData.Message = "禁用失败: 此插件不存在, 或未启用";
return await Task.FromResult(responseData);
}
// 7. 尝试移除 Plugins_wwwroot/PluginId
string pluginWwwRootDir = PluginPathProvider.PluginWwwRootDir(pluginId);
if (Directory.Exists(pluginWwwRootDir))
{
Directory.Delete(pluginWwwRootDir, true);
}
responseData.Code = 1;
// responseData.Message = "禁用成功";
responseData.Message = pluginDisableResultMessage;
}
catch (Exception ex)
{
responseData.Code = -2;
responseData.Message = "禁用失败: " + ex.Message;
Utils.LogUtil.Error<PluginsController>(ex, ex.Message);
}
return await Task.FromResult(responseData);
}
#endregion
#region
/// <summary>
/// 上传插件
/// </summary>
/// <param name="file">注意: 参数名一定为 file 对应前端传过来时以 file 为名</param>
/// <returns></returns>
[HttpGet, HttpPost]
public async Task<ActionResult<BaseResponseModel>> Upload(IFormFile file)
{
BaseResponseModel responseData = new BaseResponseModel();
#region
if (file == null)
{
responseData.Code = -1;
responseData.Message = "上传的文件不能为空";
return responseData;
}
//文件后缀
string fileExtension = Path.GetExtension(file.FileName);//获取文件格式,拓展名
// 类型标记
UploadFileType uploadFileType = UploadFileType.NoAllowedType;
switch (fileExtension)
{
case ".zip":
uploadFileType = UploadFileType.Zip;
break;
case ".nupkg":
uploadFileType = UploadFileType.Nupkg;
break;
}
if (fileExtension != ".zip" && fileExtension != ".nupkg")
{
responseData.Code = -1;
// nupkg 其实就是 zip
responseData.Message = "只能上传 zip 或 nupkg 格式文件";
return responseData;
}
// PluginCore.AspNetCore-v1.0.2 起 不再限制插件上传大小
//判断文件大小
//var fileSize = file.Length;
//if (fileSize > 1024 * 1024 * 5) // 5M
//{
// responseData.Code = -1;
// responseData.Message = "上传的文件不能大于5MB";
// return responseData;
//}
#endregion
try
{
// 1.先上传到 临时插件上传目录, 用Guid.zip作为保存文件名
string tempZipFilePath = Path.Combine(PluginPathProvider.TempPluginUploadDir(), Guid.NewGuid() + ".zip");
using (var fs = System.IO.File.Create(tempZipFilePath))
{
file.CopyTo(fs); //将上传的文件文件流复制到fs中
fs.Flush();//清空文件流
}
// 2.解压
bool isDecomparessSuccess = false;
if (uploadFileType == UploadFileType.Zip)
{
isDecomparessSuccess = Utils.ZipHelper.DecomparessFile(tempZipFilePath, tempZipFilePath.Replace(".zip", ""));
}
else if (uploadFileType == UploadFileType.Nupkg)
{
isDecomparessSuccess = NupkgService.DecomparessFile(tempZipFilePath, tempZipFilePath.Replace(".zip", ""));
}
// 3.删除原压缩包
System.IO.File.Delete(tempZipFilePath);
if (!isDecomparessSuccess)
{
responseData.Code = -1;
responseData.Message = "解压插件压缩包失败";
return responseData;
}
// 4.读取其中的info.json, 获取 PluginId 值
PluginInfoModel pluginInfoModel = PluginInfoModelFactory.ReadPluginDir(tempZipFilePath.Replace(".zip", ""));
if (pluginInfoModel == null || string.IsNullOrEmpty(pluginInfoModel.PluginId))
{
// 记得删除已不再需要的临时插件文件夹
Directory.Delete(tempZipFilePath.Replace(".zip", ""), true);
responseData.Code = -1;
responseData.Message = "不合法的插件";
return responseData;
}
string pluginId = pluginInfoModel.PluginId;
// 5.检索 此 PluginId 是否本地插件已存在
var pluginConfigModel = PluginConfigModelFactory.Create();
// 本地已经存在的 PluginId
IList<string> localExistPluginIds = PluginPathProvider.AllPluginFolderName();
if (localExistPluginIds.Contains(pluginId))
{
// 记得删除已不再需要的临时插件文件夹
Directory.Delete(tempZipFilePath.Replace(".zip", ""), true);
responseData.Code = -1;
responseData.Message = $"本地已有此插件 (PluginId: {pluginId}), 请前往插件列表删除后, 再上传";
return responseData;
}
// 6.本地无此插件 -> 移动插件文件夹到 Plugins 下, 并以 PluginId 为插件文件夹名
string pluginsRootPath = PluginPathProvider.PluginsRootPath();
string newPluginDir = Path.Combine(pluginsRootPath, pluginId);
Directory.Move(tempZipFilePath.Replace(".zip", ""), newPluginDir);
// 7. 放入 Plugins 中, 默认为 已禁用
responseData.Code = 1;
responseData.Message = $"上传插件成功 (PluginId: {pluginId})";
}
catch (Exception ex)
{
responseData.Code = -1;
responseData.Message = "上传插件失败: " + ex.Message;
ex = ex.InnerException;
while (ex != null)
{
responseData.Message += " - " + ex.InnerException.Message;
ex = ex.InnerException;
}
Utils.LogUtil.Error<PluginsController>(ex, ex.Message);
}
return await Task.FromResult(responseData);
}
#endregion
#region
[HttpGet, HttpPost]
public async Task<ActionResult<BaseResponseModel>> Details(string pluginId)
{
BaseResponseModel responseData = new BaseResponseModel();
try
{
#region
pluginId = pluginId.Trim();
var pluginConfigModel = PluginConfigModelFactory.Create();
string[] localPluginIds = PluginPathProvider.AllPluginFolderName().ToArray();
if (!localPluginIds.Contains(pluginId))
{
responseData.Code = -1;
responseData.Message = $"查看详细失败: 不存在 {pluginId} 插件";
return await Task.FromResult(responseData);
}
#endregion
PluginInfoModel pluginInfoModel = PluginInfoModelFactory.Create(pluginId);
string[] enablePluginIds = _pluginFinder.EnablePluginIds().ToArray();
PluginInfoResponseModel pluginInfoResponseModel = PluginInfoModelToResponseModel(new List<PluginInfoModel>() { pluginInfoModel }, pluginConfigModel, enablePluginIds).FirstOrDefault();
responseData.Code = 1;
responseData.Message = "查看详细成功";
responseData.Data = pluginInfoResponseModel;
}
catch (Exception ex)
{
responseData.Code = -1;
responseData.Message = "查看详细失败: " + ex.Message;
}
return await Task.FromResult(responseData);
}
#endregion
#region
[HttpGet, HttpPost]
public async Task<ActionResult<BaseResponseModel>> Readme(string pluginId)
{
BaseResponseModel responseData = new BaseResponseModel();
try
{
#region
pluginId = pluginId.Trim();
// var pluginConfigModel = PluginConfigModelFactory.Create();
string[] localPluginIds = PluginPathProvider.AllPluginFolderName().ToArray();
if (!localPluginIds.Contains(pluginId))
{
responseData.Code = -1;
responseData.Message = $"查看文档失败: 不存在 {pluginId} 插件";
return await Task.FromResult(responseData);
}
#endregion
PluginReadmeModel readmeModel = PluginReadmeModelFactory.Create(pluginId);
PluginReadmeResponseModel readmeResponseModel = new PluginReadmeResponseModel();
readmeResponseModel.Content = readmeModel?.Content ?? "";
readmeResponseModel.PluginId = pluginId;
responseData.Code = 1;
responseData.Message = "查看文档成功";
responseData.Data = readmeResponseModel;
}
catch (Exception ex)
{
responseData.Code = -1;
responseData.Message = "查看文档失败: " + ex.Message;
}
return await Task.FromResult(responseData);
}
#endregion
#region
[HttpGet]
public async Task<ActionResult<BaseResponseModel>> Settings(string pluginId)
{
BaseResponseModel responseData = new BaseResponseModel();
try
{
#region
pluginId = pluginId.Trim();
// var pluginConfigModel = PluginConfigModelFactory.Create();
string[] localPluginIds = PluginPathProvider.AllPluginFolderName().ToArray();
if (!localPluginIds.Contains(pluginId))
{
responseData.Code = -1;
responseData.Message = $"查看设置失败: 不存在 {pluginId} 插件";
return await Task.FromResult(responseData);
}
#endregion
string settingsJsonStr = PluginSettingsModelFactory.Create(pluginId);
responseData.Code = 1;
responseData.Message = "查看设置成功";
responseData.Data = settingsJsonStr ?? "无设置项";
}
catch (Exception ex)
{
responseData.Code = -1;
responseData.Message = "查看设置失败: " + ex.Message;
}
return await Task.FromResult(responseData);
}
[HttpPost]
public async Task<ActionResult<BaseResponseModel>> Settings(PluginSettingsInputModel inputModel)
{
BaseResponseModel responseData = new BaseResponseModel();
try
{
#region
inputModel.PluginId = inputModel.PluginId.Trim();
// var pluginConfigModel = PluginConfigModelFactory.Create();
string[] localPluginIds = PluginPathProvider.AllPluginFolderName().ToArray();
if (!localPluginIds.Contains(inputModel.PluginId))
{
responseData.Code = -1;
responseData.Message = $"设置失败: 不存在 {inputModel.PluginId} 插件";
return await Task.FromResult(responseData);
}
#endregion
inputModel.Data = inputModel.Data ?? "";
PluginSettingsModelFactory.Save(pluginSettingsJsonStr: inputModel.Data, pluginId: inputModel.PluginId);
responseData.Code = 1;
responseData.Message = "设置成功";
responseData.Data = inputModel.Data;
}
catch (Exception ex)
{
responseData.Code = -1;
responseData.Message = "设置失败: " + ex.Message;
}
return await Task.FromResult(responseData);
}
#endregion
#endregion
#region Helpers
[NonAction]
private IList<PluginInfoResponseModel> PluginInfoModelToResponseModel(IList<PluginInfoModel> pluginInfoModels, PluginConfigModel pluginConfigModel, string[] enablePluginIds)
{
// 获取 Plugins 下所有插件
// DirectoryInfo pluginsDir = new DirectoryInfo(PluginPathProvider.PluginsRootPath());
// List<string> pluginIds = pluginsDir?.GetDirectories()?.Select(m => m.Name)?.ToList() ?? new List<string>();
IList<PluginInfoResponseModel> responseModels = new List<PluginInfoResponseModel>();
#region
foreach (var model in pluginInfoModels)
{
PluginInfoResponseModel responseModel = new PluginInfoResponseModel();
responseModel.Author = model.Author;
responseModel.Description = model.Description;
responseModel.DisplayName = model.DisplayName;
responseModel.PluginId = model.PluginId;
responseModel.SupportedVersions = model.SupportedVersions;
responseModel.Version = model.Version;
responseModel.DependPlugins = model.DependPlugins;
if (pluginConfigModel.EnabledPlugins.Contains(model.PluginId) && !enablePluginIds.Contains(model.PluginId)) {
// 错误情况: 配置 标识 已启用, 但实际没有启用成功
pluginConfigModel.EnabledPlugins.Remove(model.PluginId);
PluginConfigModelFactory.Save(pluginConfigModel);
responseModel.Status = PluginStatus.Disabled;
} else if(!pluginConfigModel.EnabledPlugins.Contains(model.PluginId) && enablePluginIds.Contains(model.PluginId))
{
// 错误情况: 配置没有标识 已启用, 但实际 已启用
pluginConfigModel.EnabledPlugins.Add(model.PluginId);
PluginConfigModelFactory.Save(pluginConfigModel);
responseModel.Status = PluginStatus.Enabled;
} else if (pluginConfigModel.EnabledPlugins.Contains(model.PluginId) && enablePluginIds.Contains(model.PluginId))
{
responseModel.Status = PluginStatus.Enabled;
}
else
{
responseModel.Status = PluginStatus.Disabled;
}
responseModels.Add(responseModel);
}
#endregion
return responseModels;
}
public enum UploadFileType
{
NoAllowedType = 0,
Zip = 1,
Nupkg = 2
}
#endregion
}
}

View File

@ -0,0 +1,143 @@
//===================================================
// License: GNU LGPLv3
// Contributors: yiyungent@gmail.com
// Project: https://yiyungent.github.io/PluginCore
// GitHub: https://github.com/yiyungent/PluginCore
//===================================================
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using PluginCore.AspNetCore.Authorization;
using PluginCore.Config;
using PluginCore.AspNetCore.RequestModel.User;
using PluginCore.AspNetCore.ResponseModel;
namespace PluginCore.AspNetCore.Controllers
{
[Route("api/plugincore/admin/[controller]/[action]")]
[ApiController]
public class UserController : ControllerBase
{
private readonly AccountManager _accountManager;
public string RemoteFronted
{
get
{
return PluginCore.Config.PluginCoreConfigFactory.Create().RemoteFrontend;
}
}
public UserController(AccountManager accountManager)
{
_accountManager = accountManager;
}
[HttpGet, HttpPost]
public async Task<ActionResult<BaseResponseModel>> Login([FromBody] LoginRequestModel requestModel)
{
BaseResponseModel responseModel = new BaseResponseModel();
try
{
string token = AccountManager.CreateToken(requestModel.UserName, requestModel.Password);
bool isAdmin = AccountManager.IsAdminToken(token);
if (!isAdmin)
{
responseModel.Code = -1;
responseModel.Message = "用户名或密码不正确";
return await Task.FromResult(responseModel);
}
responseModel.Code = 1;
responseModel.Message = "登录成功";
responseModel.Data = new
{
token = token,
userName = requestModel.UserName
};
}
catch (Exception ex)
{
responseModel.Code = -1;
responseModel.Message = "失败: " + ex.Message;
}
return await Task.FromResult(responseModel);
}
[HttpGet, HttpPost]
public async Task<ActionResult<BaseResponseModel>> Logout()
{
BaseResponseModel responseModel = new BaseResponseModel()
{
Code = 1,
Message = "退出登录成功"
};
return await Task.FromResult(responseModel);
}
[PluginCoreAdminAuthorize]
[HttpGet, HttpPost]
public async Task<ActionResult<BaseResponseModel>> Info()
{
BaseResponseModel responseModel = new BaseResponseModel();
try
{
string adminUserName = PluginCoreConfigFactory.Create().Admin.UserName;
responseModel.Code = 1;
responseModel.Message = "成功";
responseModel.Data = new
{
name = adminUserName,
//avatar = this.RemoteFronted + "/images/avatar.gif"
avatar = ""
};
}
catch (Exception ex)
{
responseModel.Code = -1;
responseModel.Message = "失败: " + ex.Message;
}
return await Task.FromResult(responseModel);
}
[PluginCoreAdminAuthorize]
[HttpGet, HttpPost]
public async Task<ActionResult<BaseResponseModel>> Update([FromBody] UpdateRequestModel requestModel)
{
BaseResponseModel responseModel = new BaseResponseModel();
try
{
PluginCoreConfig pluginCoreConfig = PluginCoreConfigFactory.Create();
pluginCoreConfig.Admin.UserName = requestModel.UserName;
pluginCoreConfig.Admin.Password = requestModel.Password;
PluginCoreConfigFactory.Save(pluginCoreConfig);
responseModel.Code = 1;
responseModel.Message = "修改成功, 需要重新登录";
}
catch (Exception ex)
{
responseModel.Code = -1;
responseModel.Message = "失败: " + ex.Message;
}
return await Task.FromResult(responseModel);
}
}
}

View File

@ -0,0 +1,152 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
using Microsoft.Extensions.DependencyInjection;
namespace PluginCore.AspNetCore.Extensions
{
public static class IServiceProviderExtensions
{
/// <summary>
/// Get all registered <see cref="ServiceDescriptor"/>
/// </summary>
/// <param name="provider"></param>
/// <returns></returns>
public static Dictionary<Type, ServiceDescriptor> GetAllServiceDescriptors(this IServiceProvider provider)
{
var serviceProvider = provider.GetPropertyValue("RootProvider");
var result = new Dictionary<Type, ServiceDescriptor>();
var engine = serviceProvider.GetFieldValue("_engine");
// var callSiteFactory = engine.GetPropertyValue("CallSiteFactory");
var callSiteFactory = serviceProvider.GetPropertyValue("CallSiteFactory");
var descriptorLookup = callSiteFactory.GetFieldValue("_descriptorLookup");
if (descriptorLookup is IDictionary dictionary)
{
foreach (DictionaryEntry entry in dictionary)
{
result.Add((Type)entry.Key, (ServiceDescriptor)entry.Value.GetPropertyValue("Last"));
}
}
return result;
#region Old
//if (provider is ServiceProvider serviceProvider)
//{
// var result = new Dictionary<Type, ServiceDescriptor>();
// var engine = serviceProvider.GetFieldValue("_engine");
// var callSiteFactory = engine.GetPropertyValue("CallSiteFactory");
// var descriptorLookup = callSiteFactory.GetFieldValue("_descriptorLookup");
// if (descriptorLookup is IDictionary dictionary)
// {
// foreach (DictionaryEntry entry in dictionary)
// {
// result.Add((Type)entry.Key, (ServiceDescriptor)entry.Value.GetPropertyValue("Last"));
// }
// }
// return result;
//}
//throw new NotSupportedException($"Type '{provider.GetType()}' is not supported!");
#endregion
}
}
public static class ReflectionHelper
{
// ##########################################################################################
// Get / Set Field
// ##########################################################################################
#region Get / Set Field
public static object GetFieldValue(this object obj, string fieldName)
{
if (obj == null)
throw new ArgumentNullException(nameof(obj));
Type objType = obj.GetType();
var fieldInfo = GetFieldInfo(objType, fieldName);
if (fieldInfo == null)
throw new ArgumentOutOfRangeException(fieldName,
$"Couldn't find field {fieldName} in type {objType.FullName}");
return fieldInfo.GetValue(obj);
}
public static void SetFieldValue(this object obj, string fieldName, object val)
{
if (obj == null)
throw new ArgumentNullException(nameof(obj));
Type objType = obj.GetType();
var fieldInfo = GetFieldInfo(objType, fieldName);
if (fieldInfo == null)
throw new ArgumentOutOfRangeException(fieldName,
$"Couldn't find field {fieldName} in type {objType.FullName}");
fieldInfo.SetValue(obj, val);
}
private static FieldInfo GetFieldInfo(Type type, string fieldName)
{
FieldInfo fieldInfo = null;
do
{
fieldInfo = type.GetField(fieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
type = type.BaseType;
} while (fieldInfo == null && type != null);
return fieldInfo;
}
#endregion
// ##########################################################################################
// Get / Set Property
// ##########################################################################################
#region Get / Set Property
public static object GetPropertyValue(this object obj, string propertyName)
{
if (obj == null)
throw new ArgumentNullException(nameof(obj));
Type objType = obj.GetType();
var propertyInfo = GetPropertyInfo(objType, propertyName);
if (propertyInfo == null)
throw new ArgumentOutOfRangeException(propertyName,
$"Couldn't find property {propertyName} in type {objType.FullName}");
return propertyInfo.GetValue(obj, null);
}
public static void SetPropertyValue(this object obj, string propertyName, object val)
{
if (obj == null)
throw new ArgumentNullException(nameof(obj));
Type objType = obj.GetType();
var propertyInfo = GetPropertyInfo(objType, propertyName);
if (propertyInfo == null)
throw new ArgumentOutOfRangeException(propertyName,
$"Couldn't find property {propertyName} in type {objType.FullName}");
propertyInfo.SetValue(obj, val, null);
}
private static PropertyInfo GetPropertyInfo(Type type, string propertyName)
{
PropertyInfo propertyInfo = null;
do
{
propertyInfo = type.GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
type = type.BaseType;
} while (propertyInfo == null && type != null);
return propertyInfo;
}
#endregion
}
}

View File

@ -0,0 +1,416 @@
//===================================================
// License: GNU LGPLv3
// Contributors: yiyungent@gmail.com
// Project: https://yiyungent.github.io/PluginCore
// GitHub: https://github.com/yiyungent/PluginCore
//===================================================
using PluginCore.Models;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.FileProviders;
using PluginCore.AspNetCore.Authorization;
using PluginCore.AspNetCore.AdminUI;
using PluginCore.Infrastructure;
using PluginCore.Interfaces;
using PluginCore.AspNetCore.Middlewares;
using PluginCore.AspNetCore.BackgroundServices;
using PluginCore.IPlugins;
using PluginCore.AspNetCore.Interfaces;
using PluginCore.lmplements;
using PluginCore.AspNetCore.lmplements;
namespace PluginCore.AspNetCore.Extensions
{
/// <summary>
///
/// </summary>
public static class PluginCoreStartupExtensions
{
private static IWebHostEnvironment _webHostEnvironment;
private static IServiceCollection _services;
private static IServiceProvider _serviceProvider;
/// <summary>
/// 若需要替换默认实现, 请在 <see cref="AddPluginCore(IServiceCollection)"/> 之前, 若在之后, 则无法影响 主程序启动时默认行为
/// </summary>
/// <param name="services"></param>
public static void AddPluginCore(this IServiceCollection services)
{
services.AddPluginCoreServices();
services.AddPluginCoreLog();
services.AddPluginCorePlugins();
#region PluginCore Admin
// 1. 先 Authentication (我是谁) 2. 再 Authorization (我能做什么)
// Authentication
//注销内置认证 by tomny
// services.AddPluginCoreAuthentication();
//注销内置授权 by tomny
// Authorization
//services.AddPluginCoreAuthorization();
#endregion
services.AddPluginCoreStartupPlugin();
// AddBackgroundServices
services.AddBackgroundServices();
// 一定要在最后
_services = services;
_serviceProvider = services.BuildServiceProvider();
}
public static IApplicationBuilder UsePluginCore(this IApplicationBuilder app)
{
// 一定在 PluginCore 添加的中间件中 第一个
app.UseMiddleware<PluginHttpStartFilterMiddleware>();
app.UsePluginCoreLanguageMiddleware();
//注销内置管理页面 by tomny
// app.UsePluginCoreAdminUI();
app.UsePluginCoreStaticFiles();
// 发现 UseAuthentication 认证中间件重复添加, 也只会执行一次 认证
// 但 UseAuthorization 重复添加2次, 则会执行 2次 授权
//注销内置认证授权 by tomny
//app.UseAuthentication();
//app.UseAuthorization();
#region Plugin Middleware
// Plugin Middleware
//app.UseMiddleware<PluginContentFilterMiddleware>();
// 一定在 PluginCore 添加的中间件中 最后一个
app.UseMiddleware<PluginHttpEndFilterMiddleware>();
#endregion
app.UsePluginCoreStartupPlugin();
app.UsePluginCoreAppStart();
// Log
app.UsePluginCoreLog();
return app;
}
public static void AddPluginCoreServices(this IServiceCollection services)
{
#region
#region ASP.NET Core
// start: 仅用于 ASP.NET Core
// 用于添加插件Controller 时通知Controller.Action发生变化
services.AddSingleton<IActionDescriptorChangeProvider>(PluginActionDescriptorChangeProvider.Instance);
services.AddSingleton(PluginActionDescriptorChangeProvider.Instance);
services.TryAddTransient<PluginControllerManager>();
services.TryAddTransient<IPluginControllerManager, PluginControllerManager>();
services.TryAddTransient<PluginApplicationBuilderManager>();
services.TryAddTransient<IPluginApplicationBuilderManager, PluginApplicationBuilderManager>();
// end: 仅用于 ASP.NET Core
#endregion
#region
// v1 旧版
//services.TryAddTransient<PluginContextPackV1>();
//services.TryAddTransient<IPluginContextPack, PluginContextPackV1>();
//services.TryAddTransient<AspNetCorePluginManagerV1>();
//services.TryAddTransient<IPluginManager, AspNetCorePluginManagerV1>();
services.TryAddTransient<PluginContextPack>();
services.TryAddTransient<IPluginContextPack, PluginContextPack>();
//注销内置插件管理 by tomny
services.TryAddTransient<AspNetCorePluginManager>();
services.TryAddTransient<IPluginManager, AspNetCorePluginManager>();
services.TryAddTransient<PluginFinderV1>();
services.TryAddTransient<PluginFinderV2>();
services.TryAddTransient<PluginFinder>();
services.TryAddTransient<IPluginFinder, PluginFinder>();
// 注意: 它必须单例
// TODO: 不知道原因, 单例失败, 每次 获取 IPluginFinder 都会获取新的 IPluginContextManager
services.TryAddSingleton<PluginContextManager>();
services.TryAddSingleton<IPluginContextManager, PluginContextManager>();
#endregion
#endregion
// ************************* 注意: IServiceCollection 是服务列表, 但由 IServiceProvider 来负责解析, AClass 单例 仅在 AServiceProvider 范围内
_serviceProvider = services.BuildServiceProvider();
}
public static void AddPluginCorePlugins(this IServiceCollection services)
{
#region ASP.NET Core
//IPluginManager pluginManager = services.BuildServiceProvider().GetService<IPluginManager>();
IPluginManager pluginManager = _serviceProvider.GetService<IPluginManager>();
// 初始化 PluginCore 相关目录
PluginPathProvider.PluginsRootPath();
// 在程序启动时加载所有 已安装并启用 的插件
// 获取 PluginConfigModel
#region PluginConfigModel
PluginConfigModel pluginConfigModel = PluginConfigModelFactory.Create();
#endregion
// 已启用的插件
#region Assemblies
IList<string> enabledPluginIds = pluginConfigModel.EnabledPlugins;
foreach (var pluginId in enabledPluginIds)
{
pluginManager.LoadPlugin(pluginId);
}
#endregion
//注销内置Controller的注册 by tomny
// 将本 Assembly 内的 Controller 添加
// var ass = Assembly.GetExecutingAssembly();
//IPluginControllerManager pluginControllerManager = services.BuildServiceProvider().GetService<IPluginControllerManager>();
// IPluginControllerManager pluginControllerManager = _serviceProvider.GetService<IPluginControllerManager>();
//pluginControllerManager.AddControllers(ass);
// IWebHostEnvironment
_webHostEnvironment = services.BuildServiceProvider().GetService<IWebHostEnvironment>();
#endregion
}
public static void AddPluginCoreAuthentication(this IServiceCollection services)
{
// fixed: https://github.com/yiyungent/PluginCore/issues/4
// System.InvalidOperationException: No authenticationScheme was specified, and there was no DefaultChallengeScheme found. The default schemes can be set using either AddAuthentication(string defaultScheme) or AddAuthentication(Action<AuthenticationOptions> configureOptions).
#region Authentication
// 没通过 Authentication: 401 Unauthorized
// services.AddAuthentication("PluginCore.Authentication")
// .AddScheme<Authentication.PluginCoreAuthenticationSchemeOptions,
// Authentication.PluginCoreAuthenticationHandler>("PluginCore.Authentication", "PluginCore.Authentication",
// options => { });
// 注意: 不要设置 默认 认证名: Constants.AspNetCoreAuthenticationScheme
// services.AddAuthentication(Constants.AspNetCoreAuthenticationScheme)
// 默认认证名: 默认
services.AddAuthentication()
// 添加一个新的认证方案
.AddScheme<Authentication.PluginCoreAuthenticationSchemeOptions, Authentication.PluginCoreAuthenticationHandler>(
authenticationScheme: Constants.AspNetCoreAuthenticationScheme,
displayName: Constants.AspNetCoreAuthenticationScheme,
options => { });
#endregion
}
public static void AddPluginCoreAuthorization(this IServiceCollection services)
{
#region - [PluginCoreAdminAuthorize]
// Only Once, Not recommend
//services.AddSingleton<IAuthorizationHandler, PluginCoreAdminAuthorizationHandler>();
services.AddAuthorization(options =>
{
// options.AddPolicy("PluginCore.Admin", policy =>
options.AddPolicy(name: Constants.AspNetCoreAuthorizationPolicyName, policy =>
{
// 无法满足 下方任何一项HTTP 403 错误
// 3.需要 检查是否拥有当前请求资源的权限
//policy.Requirements.Add(new PluginCoreAdminRequirement());
policy.AuthenticationSchemes = new string[] {
Constants.AspNetCoreAuthenticationScheme
};
policy.RequireAuthenticatedUser();
policy.RequireClaim(claimType: Constants.AspNetCoreAuthenticationClaimType);
// 必须重启才能使得更改密码生效
string token = AccountManager.CreateToken();
policy.RequireClaim(claimType: Constants.AspNetCoreAuthenticationClaimType, allowedValues: new string[] {
token
});
//policy.RequireAssertion(context =>
//{
// return true;
//});
});
});
#endregion
// AccountManager
services.AddTransient<AccountManager>();
// HttpContext.Current
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
//services.AddHttpContextAccessor();
}
public static void AddPluginCoreStartupPlugin(this IServiceCollection services)
{
//IPluginFinder pluginFinder = services.BuildServiceProvider().GetService<IPluginFinder>();
IPluginFinder pluginFinder = _serviceProvider.GetService<IPluginFinder>();
#region IStartupPlugin
var plugins = pluginFinder.EnablePlugins<IStartupPlugin>()?.OrderBy(m => m.ConfigureServicesOrder)?.ToList();
foreach (var item in plugins)
{
// 调用
Utils.LogUtil.Info(
categoryName: $"{nameof(PluginCoreStartupExtensions)}.{nameof(AddPluginCoreStartupPlugin)}",
message: $"{item.GetType().ToString()}: {nameof(IStartupPlugin)}.{nameof(IStartupPlugin.ConfigureServices)}"
);
item?.ConfigureServices(services);
}
#endregion
}
public static void AddPluginCoreLog(this IServiceCollection services)
{
#region Logger
IServiceScopeFactory serviceScopeFactory = _serviceProvider.GetService<IServiceScopeFactory>();
Utils.LogUtil.Initialize(serviceScopeFactory);
#endregion
}
public static IApplicationBuilder UsePluginCoreStaticFiles(this IApplicationBuilder app)
{
// TODO: 其实由于目前已实现运行时动态新增/删除 HTTP Middleware, 其实可以不用再像下方这么复制插件 wwwroot 目录到 Plugins_wwwroot/{PluginId}
// 而是在运行时配置, 直接指向 `Plugins/{PluginId}/wwwroot`, 而无需复制/删除
// 注意:`UseDefaultFiles`必须在`UseStaticFiles`之前进行调用。因为`DefaultFilesMiddleware`仅仅负责重写Url实际上默认页文件仍然是通过`StaticFilesMiddleware`来提供的。
string pluginwwwrootDir = PluginPathProvider.PluginsWwwRootDir();
#region wwwroot
// 设置目录的默认页
var defaultFilesOptions = new DefaultFilesOptions();
defaultFilesOptions.DefaultFileNames.Clear();
// 指定默认页名称
defaultFilesOptions.DefaultFileNames.Add("index.html");
// 指定请求路径
defaultFilesOptions.RequestPath = "/Plugins";
// 指定默认页所在的目录
defaultFilesOptions.FileProvider = new PhysicalFileProvider(pluginwwwrootDir);
app.UseDefaultFiles(defaultFilesOptions);
#endregion
#region wwwroot
// 由于没办法在运行时, 动态 UseStaticFiles(), 因此不再为每一个插件都 UseStaticFiles(),
// 而是统一在一个文件夹下, 插件启用时, 将插件的wwwroot复制到 Plugins_wwwroot/{PluginId}, 禁用时, 再删除
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(
pluginwwwrootDir),
RequestPath = "/Plugins"
});
#endregion
return app;
}
public static IApplicationBuilder UsePluginCoreAppStart(this IApplicationBuilder app)
{
//IPluginFinder pluginFinder = _services.BuildServiceProvider().GetService<IPluginFinder>();
IPluginFinder pluginFinder = _serviceProvider.GetService<IPluginFinder>();
#region AppStart
var plugins = pluginFinder.EnablePluginsFull()?.ToList();
var dependencySorter = new PluginCore.Utils.DependencySorter<IPlugin>();
dependencySorter.AddObjects(plugins.Select(m => m.PluginInstance).ToArray());
foreach (var item in plugins)
{
var dependPlugins = plugins.Where(m => item.PluginInstance.AppStartOrderDependPlugins.Contains(m.PluginId)).Select(m => m.PluginInstance).ToArray();
dependencySorter.SetDependencies(obj: item.PluginInstance, dependsOnObjects: dependPlugins);
}
var sortedPlugins = dependencySorter.Sort();
foreach (var item in sortedPlugins)
{
// 调用
//Utils.LogUtil.PluginBehavior(item, typeof(IPlugin), nameof(IPlugin.AppStart));
Utils.LogUtil.Info(
categoryName: $"{nameof(PluginCoreStartupExtensions)}.{nameof(UsePluginCoreAppStart)}",
message: $"{item.GetType().ToString()}: {nameof(IPlugin)}.{nameof(IPlugin.AppStart)}"
);
item?.AppStart();
}
#endregion
return app;
}
public static IApplicationBuilder UsePluginCoreStartupPlugin(this IApplicationBuilder app)
{
//IPluginFinder pluginFinder = _services.BuildServiceProvider().GetService<IPluginFinder>();
IPluginFinder pluginFinder = _serviceProvider.GetService<IPluginFinder>();
#region IStartupPlugin
var startupPlugins = pluginFinder.EnablePlugins<IStartupPlugin>()?.OrderBy(m => m.ConfigureOrder)?.ToList();
foreach (var item in startupPlugins)
{
// 调用
//Utils.LogUtil.PluginBehavior(item, typeof(IStartupPlugin), nameof(IStartupPlugin.Configure));
Utils.LogUtil.Info(
categoryName: $"{nameof(PluginCoreStartupExtensions)}.{nameof(UsePluginCoreStartupPlugin)}",
message: $"{item.GetType().ToString()}: {nameof(IStartupPlugin)}.{nameof(IStartupPlugin.Configure)}"
);
item?.Configure(app);
}
#endregion
app.UseMiddleware<PluginStartupXMiddleware>();
return app;
}
public static IApplicationBuilder UsePluginCoreLog(this IApplicationBuilder app)
{
#region Log
Config.PluginCoreConfig pluginCoreConfig = Config.PluginCoreConfigFactory.Create();
Utils.LogUtil.Info(categoryName: $"{nameof(PluginCoreStartupExtensions)}.{nameof(UsePluginCoreLog)}", $"{nameof(PluginCore.AspNetCore)}");
Utils.LogUtil.Info(categoryName: $"{nameof(PluginCoreStartupExtensions)}.{nameof(UsePluginCoreLog)}", "Started successfully:");
Utils.LogUtil.Info(categoryName: $"{nameof(PluginCoreStartupExtensions)}.{nameof(UsePluginCoreLog)}", $"Front-end mode: {pluginCoreConfig.FrontendMode}");
Utils.LogUtil.Info(categoryName: $"{nameof(PluginCoreStartupExtensions)}.{nameof(UsePluginCoreLog)}", $"Notice: Updating the front-end mode requires restarting the site");
#endregion
return app;
}
}
}

View File

@ -0,0 +1,350 @@
//===================================================
// License: GNU LGPLv3
// Contributors: yiyungent@gmail.com
// Project: https://yiyungent.github.io/PluginCore
// GitHub: https://github.com/yiyungent/PluginCore
//===================================================
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Internal;
namespace PluginCore.AspNetCore.Infrastructure
{
/// <summary>
/// Default implementation for <see cref="IApplicationBuilder"/>.
///
/// https://github.com/dotnet/aspnetcore/blob/main/src/Http/Http/src/Builder/ApplicationBuilder.cs
/// </summary>
public class PluginApplicationBuilder : IApplicationBuilder
{
private const string ServerFeaturesKey = "server.Features";
private const string ApplicationServicesKey = "application.Services";
private readonly List<Func<RequestDelegate, RequestDelegate>> _components = new List<Func<RequestDelegate, RequestDelegate>>();
private Action _reachEndAction;
public Action ReachEndAction
{
get
{
return this._reachEndAction;
}
set
{
this._reachEndAction = value;
}
}
public PluginApplicationBuilder()
{
}
/// <summary>
/// Initializes a new instance of <see cref="ApplicationBuilder"/>.
/// </summary>
/// <param name="serviceProvider">The <see cref="IServiceProvider"/> for application services.</param>
public PluginApplicationBuilder(IServiceProvider serviceProvider, Action reachEndAction)
{
Properties = new Dictionary<string, object?>(StringComparer.Ordinal);
ApplicationServices = serviceProvider;
_reachEndAction = reachEndAction;
}
/// <summary>
/// Initializes a new instance of <see cref="ApplicationBuilder"/>.
/// </summary>
/// <param name="serviceProvider">The <see cref="IServiceProvider"/> for application services.</param>
/// <param name="server">The server instance that hosts the application.</param>
public PluginApplicationBuilder(IServiceProvider serviceProvider, Action reachEndAction, object server)
: this(serviceProvider, reachEndAction)
{
SetProperty(ServerFeaturesKey, server);
}
private PluginApplicationBuilder(PluginApplicationBuilder builder)
{
Properties = new CopyOnWriteDictionary<string, object?>(builder.Properties, StringComparer.Ordinal);
}
/// <summary>
/// Gets the <see cref="IServiceProvider"/> for application services.
/// </summary>
public IServiceProvider ApplicationServices
{
get
{
//return GetProperty<IServiceProvider>(ApplicationServicesKey)!;
return null;
}
set
{
SetProperty<IServiceProvider>(ApplicationServicesKey, value);
}
}
/// <summary>
/// Gets the <see cref="IFeatureCollection"/> for server features.
/// </summary>
public IFeatureCollection ServerFeatures
{
get
{
// ! null 包容运算符C# 参考)
// https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/operators/null-forgiving
// TODO: 报错
//var result = GetProperty<IFeatureCollection>(ServerFeaturesKey)!;
IFeatureCollection rtn = null;
return rtn;
}
}
/// <summary>
/// Gets a set of properties for <see cref="ApplicationBuilder"/>.
/// </summary>
public IDictionary<string, object?> Properties { get; }
//private T? GetProperty<T>(string key)
//{
// return Properties.TryGetValue(key, out var value) ? (T?)value : default(T);
//}
private void SetProperty<T>(string key, T value)
{
Properties[key] = value;
}
/// <summary>
/// Adds the middleware to the application request pipeline.
/// </summary>
/// <param name="middleware">The middleware.</param>
/// <returns>An instance of <see cref="IApplicationBuilder"/> after the operation has completed.</returns>
public IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware)
{
_components.Add(middleware);
return this;
}
/// <summary>
/// Creates a copy of this application builder.
/// <para>
/// The created clone has the same properties as the current instance, but does not copy
/// the request pipeline.
/// </para>
/// </summary>
/// <returns>The cloned instance.</returns>
public IApplicationBuilder New()
{
return new PluginApplicationBuilder(this);
}
/// <summary>
/// Produces a <see cref="RequestDelegate"/> that executes added middlewares.
/// </summary>
/// <returns>The <see cref="RequestDelegate"/>.</returns>
public RequestDelegate Build()
{
RequestDelegate app = context =>
{
#region Old
// If we reach the end of the pipeline, but we have an endpoint, then something unexpected has happened.
// This could happen if user code sets an endpoint, but they forgot to add the UseEndpoint middleware.
//var endpoint = context.GetEndpoint();
//var endpointRequestDelegate = endpoint?.RequestDelegate;
//if (endpointRequestDelegate != null)
//{
// var message =
// $"The request reached the end of the pipeline without executing the endpoint: '{endpoint!.DisplayName}'. " +
// $"Please register the EndpointMiddleware using '{nameof(IApplicationBuilder)}.UseEndpoints(...)' if using " +
// $"routing.";
// throw new InvalidOperationException(message);
//}
#endregion
this._reachEndAction();
//context.Response.StatusCode = StatusCodes.Status404NotFound;
return Task.CompletedTask;
};
for (var c = _components.Count - 1; c >= 0; c--)
{
app = _components[c](app);
}
return app;
}
}
/// <summary>
/// https://github.com/dotnet/aspnetcore/blob/8b30d862de6c9146f466061d51aa3f1414ee2337/src/Shared/CopyOnWriteDictionary/CopyOnWriteDictionary.cs
/// </summary>
/// <typeparam name="TKey"></typeparam>
/// <typeparam name="TValue"></typeparam>
internal class CopyOnWriteDictionary<TKey, TValue> : IDictionary<TKey, TValue> where TKey : notnull
{
private readonly IDictionary<TKey, TValue> _sourceDictionary;
private readonly IEqualityComparer<TKey> _comparer;
private IDictionary<TKey, TValue>? _innerDictionary;
public CopyOnWriteDictionary(
IDictionary<TKey, TValue> sourceDictionary,
IEqualityComparer<TKey> comparer)
{
if (sourceDictionary == null)
{
throw new ArgumentNullException(nameof(sourceDictionary));
}
if (comparer == null)
{
throw new ArgumentNullException(nameof(comparer));
}
_sourceDictionary = sourceDictionary;
_comparer = comparer;
}
private IDictionary<TKey, TValue> ReadDictionary
{
get
{
return _innerDictionary ?? _sourceDictionary;
}
}
private IDictionary<TKey, TValue> WriteDictionary
{
get
{
if (_innerDictionary == null)
{
_innerDictionary = new Dictionary<TKey, TValue>(_sourceDictionary,
_comparer);
}
return _innerDictionary;
}
}
public virtual ICollection<TKey> Keys
{
get
{
return ReadDictionary.Keys;
}
}
public virtual ICollection<TValue> Values
{
get
{
return ReadDictionary.Values;
}
}
public virtual int Count
{
get
{
return ReadDictionary.Count;
}
}
public virtual bool IsReadOnly
{
get
{
return false;
}
}
public virtual TValue this[TKey key]
{
get
{
return ReadDictionary[key];
}
set
{
WriteDictionary[key] = value;
}
}
public virtual bool ContainsKey(TKey key)
{
return ReadDictionary.ContainsKey(key);
}
public virtual void Add(TKey key, TValue value)
{
WriteDictionary.Add(key, value);
}
public virtual bool Remove(TKey key)
{
return WriteDictionary.Remove(key);
}
public virtual bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value)
{
return ReadDictionary.TryGetValue(key, out value);
}
public virtual void Add(KeyValuePair<TKey, TValue> item)
{
WriteDictionary.Add(item);
}
public virtual void Clear()
{
WriteDictionary.Clear();
}
public virtual bool Contains(KeyValuePair<TKey, TValue> item)
{
return ReadDictionary.Contains(item);
}
public virtual void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex)
{
ReadDictionary.CopyTo(array, arrayIndex);
}
public bool Remove(KeyValuePair<TKey, TValue> item)
{
return WriteDictionary.Remove(item);
}
public virtual IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
{
return ReadDictionary.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}

View File

@ -0,0 +1,62 @@
//===================================================
// License: GNU LGPLv3
// Contributors: yiyungent@gmail.com
// Project: https://yiyungent.github.io/PluginCore
// GitHub: https://github.com/yiyungent/PluginCore
//===================================================
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using PluginCore.AspNetCore.Extensions;
[assembly: HostingStartup(typeof(PluginCore.AspNetCore.Infrastructure.PluginCoreHostingStartup))]
namespace PluginCore.AspNetCore.Infrastructure
{
/// <summary>
/// https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/host/platform-specific-configuration?view=aspnetcore-5.0
/// </summary>
public class PluginCoreHostingStartup : IHostingStartup
{
public PluginCoreHostingStartup()
{
}
public void Configure(IWebHostBuilder builder)
{
//builder.ConfigureAppConfiguration(config =>
//{
//});
// 注意: 无论是通过 Program.cs 中 webBuilder.UseSetting(WebHostDefaults.HostingStartupAssembliesKey, "PluginCore");
// 还是 通过环境变量 指定承载启动程序集, 都是先执行 外部的承载启动程序集, 再执行主程序的 Startup.cs, 因此在这时, 有些 service 还没有注册
// TODO: 不知道, 重复 Add, Use 会导致什么, 没有做防止重复
builder.ConfigureServices(services =>
{
// fixed: https://github.com/yiyungent/PluginCore/issues/1
// System.InvalidOperationException: 'Unable to resolve service for type 'Microsoft.AspNetCore.Mvc.ApplicationParts.ApplicationPartManager'
// TODO: 不确定, 这样是否可行, 事实上之后主程序还会 Add 一次, 不知道是否会导致存在多个 实例
// 失败: 不是一个实例, 导致无法改变 Controller
//Microsoft.AspNetCore.Mvc.ApplicationParts.ApplicationPartManager applicationPartManager =
// new Microsoft.AspNetCore.Mvc.ApplicationParts.ApplicationPartManager();
//services.AddSingleton<Microsoft.AspNetCore.Mvc.ApplicationParts.ApplicationPartManager>(applicationPartManager);
services.AddPluginCore();
});
builder.Configure(app =>
{
app.UsePluginCore();
});
}
}
}

View File

@ -0,0 +1,25 @@
//===================================================
// License: GNU LGPLv3
// Contributors: yiyungent@gmail.com
// Project: https://yiyungent.github.io/PluginCore
// GitHub: https://github.com/yiyungent/PluginCore
//===================================================
using Microsoft.AspNetCore.Http;
using System;
using System.Collections.Generic;
using System.Text;
namespace PluginCore.AspNetCore.Interfaces
{
public interface IPluginApplicationBuilderManager
{
void ReBuild();
RequestDelegate GetBuildResult();
}
}

View File

@ -0,0 +1,25 @@
//===================================================
// License: GNU LGPLv3
// Contributors: yiyungent@gmail.com
// Project: https://yiyungent.github.io/PluginCore
// GitHub: https://github.com/yiyungent/PluginCore
//===================================================
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
namespace PluginCore.AspNetCore.Interfaces
{
public interface IPluginControllerManager
{
void AddControllers(Assembly assembly);
void RemoveControllers(string pluginId);
}
}

View File

@ -0,0 +1,47 @@
//===================================================
// License: GNU LGPLv3
// Contributors: yiyungent@gmail.com
// Project: https://yiyungent.github.io/PluginCore
// GitHub: https://github.com/yiyungent/PluginCore
//===================================================
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using PluginCore.IPlugins;
namespace PluginCore.AspNetCore.Middlewares
{
public class LanguageMiddleware
{
private readonly RequestDelegate _next;
public LanguageMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext httpContext)
{
// 从 Cookie 中获取语言标识
string language = httpContext.Request.Cookies[Constants.AspNetCoreLanguageCookieName];
// 存储当前语言到 HttpContext.Items
httpContext.Items[Constants.AspNetCoreLanguageKey] = language;
// 调用下一个中间件
await _next(httpContext);
}
}
public static class LanguageMiddlewareExtensions
{
public static IApplicationBuilder UsePluginCoreLanguageMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<LanguageMiddleware>();
}
}
}

View File

@ -0,0 +1,150 @@
//===================================================
// License: GNU LGPLv3
// Contributors: yiyungent@gmail.com
// Project: https://yiyungent.github.io/PluginCore
// GitHub: https://github.com/yiyungent/PluginCore
//===================================================
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using PluginCore.Interfaces;
namespace PluginCore.AspNetCore.Middlewares
{
/// <summary>
/// TODO: 未测试
/// </summary>
public class PluginContentFilterMiddleware
{
private readonly RequestDelegate _next;
private readonly IPluginFinder _pluginFinder;
public PluginContentFilterMiddleware(
RequestDelegate next,
IWebHostEnvironment hostingEnv,
ILoggerFactory loggerFactory,
IPluginFinder pluginFinder)
{
_next = next;
_pluginFinder = pluginFinder;
}
public async Task InvokeAsync(HttpContext httpContext)
{
var httpMethod = httpContext.Request.Method;
var path = httpContext.Request.Path.Value;
// 在请求下一个 middleware 前过滤
await RequestBodyFilter(httpContext);
// Call the next delegate/middleware in the pipeline
await _next(httpContext);
if (httpMethod == "GET" && path.EndsWith(".css") || path.EndsWith(".js") || path.EndsWith(".html"))
{
// middleware 回退时 过滤
await ResponseBodyFilter(httpContext);
}
}
private async Task RequestBodyFilter(HttpContext httpContext)
{
string content = string.Empty;
using (var memoryStream = new MemoryStream())
{
// Response.Body
await httpContext.Request.Body.CopyToAsync(memoryStream);
long pos = httpContext.Request.Body.Position;
using (var reader = new StreamReader(memoryStream, Encoding.UTF8))
{
content = await reader.ReadToEndAsync();
}
}
var plugins = this._pluginFinder.EnablePlugins<PluginCore.IPlugins.IContentFilterPlugin>().ToList();
foreach (var item in plugins)
{
// 调用
content = await item?.RequestBodyFilter(httpContext.Request.Path.Value, content);
}
// 更新 Request.Body
#region 1
//var requestStream = new MemoryStream();
//using (StreamWriter writer = new StreamWriter(requestStream, Encoding.UTF8))
//{
// await writer.WriteAsync(content);
//}
//httpContext.Request.Body = requestStream;
#endregion
#region 2
httpContext.Request.Body.Seek(0, SeekOrigin.Begin);
byte[] buffer = Encoding.UTF8.GetBytes(content);
await httpContext.Request.Body.WriteAsync(buffer, 0, buffer.Length);
#endregion
}
private async Task ResponseBodyFilter(HttpContext httpContext)
{
string content = string.Empty;
using (var memoryStream = new MemoryStream())
{
// Response.Body
await httpContext.Response.Body.CopyToAsync(memoryStream);
long pos = httpContext.Response.Body.Position;
using (var reader = new StreamReader(memoryStream, Encoding.UTF8))
{
content = await reader.ReadToEndAsync();
}
}
var plugins = this._pluginFinder.EnablePlugins<PluginCore.IPlugins.IContentFilterPlugin>().ToList();
foreach (var item in plugins)
{
// 调用
content = await item?.ReponseBodyFilter(httpContext.Request.Path.Value, content);
}
// 更新 ResponseBody
#region 1
//var responseStream = new MemoryStream();
//using (StreamWriter writer = new StreamWriter(responseStream, Encoding.UTF8))
//{
// await writer.WriteAsync(content);
//}
//httpContext.Response.Body = responseStream;
#endregion
#region 2
httpContext.Response.Body.Seek(0, SeekOrigin.Begin);
byte[] buffer = Encoding.UTF8.GetBytes(content);
await httpContext.Response.Body.WriteAsync(buffer, 0, buffer.Length);
#endregion
}
}
}

View File

@ -0,0 +1,72 @@
//===================================================
// License: GNU LGPLv3
// Contributors: yiyungent@gmail.com
// Project: https://yiyungent.github.io/PluginCore
// GitHub: https://github.com/yiyungent/PluginCore
//===================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using PluginCore.Interfaces;
namespace PluginCore.AspNetCore.Middlewares
{
/// <summary>
/// 一定在 PluginCore 添加的中间件中 最后一个
/// </summary>
public class PluginHttpEndFilterMiddleware
{
private readonly RequestDelegate _next;
private readonly IPluginFinder _pluginFinder;
public PluginHttpEndFilterMiddleware(
RequestDelegate next,
IWebHostEnvironment hostingEnv,
ILoggerFactory loggerFactory,
IPluginFinder pluginFinder)
{
_next = next;
_pluginFinder = pluginFinder;
}
public async Task InvokeAsync(HttpContext httpContext)
{
var httpMethod = httpContext.Request.Method;
var path = httpContext.Request.Path.Value;
// 在请求下一个 middleware 前过滤
// Call the next delegate/middleware in the pipeline
await _next(httpContext);
// middleware 回退时 过滤
await Filter(httpContext);
}
private async Task Filter(HttpContext httpContext)
{
var plugins = this._pluginFinder.EnablePlugins<PluginCore.IPlugins.IHttpFilterPlugin>().ToList();
foreach (var item in plugins)
{
// 调用
await item?.HttpEndFilter(httpContext);
}
}
}
}

View File

@ -0,0 +1,72 @@
//===================================================
// License: GNU LGPLv3
// Contributors: yiyungent@gmail.com
// Project: https://yiyungent.github.io/PluginCore
// GitHub: https://github.com/yiyungent/PluginCore
//===================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using PluginCore.Interfaces;
namespace PluginCore.AspNetCore.Middlewares
{
/// <summary>
/// 一定在 PluginCore 添加的中间件中 第一个
/// </summary>
public class PluginHttpStartFilterMiddleware
{
private readonly RequestDelegate _next;
private readonly IPluginFinder _pluginFinder;
public PluginHttpStartFilterMiddleware(
RequestDelegate next,
IWebHostEnvironment hostingEnv,
ILoggerFactory loggerFactory,
IPluginFinder pluginFinder)
{
_next = next;
_pluginFinder = pluginFinder;
}
public async Task InvokeAsync(HttpContext httpContext)
{
var httpMethod = httpContext.Request.Method;
var path = httpContext.Request.Path.Value;
// 在请求下一个 middleware 前过滤
await Filter(httpContext);
// Call the next delegate/middleware in the pipeline
await _next(httpContext);
// middleware 回退时 过滤
}
private async Task Filter(HttpContext httpContext)
{
var plugins = this._pluginFinder.EnablePlugins<PluginCore.IPlugins.IHttpFilterPlugin>().ToList();
foreach (var item in plugins)
{
// 调用
await item?.HttpStartFilter(httpContext);
}
}
}
}

View File

@ -0,0 +1,72 @@
//===================================================
// License: GNU LGPLv3
// Contributors: yiyungent@gmail.com
// Project: https://yiyungent.github.io/PluginCore
// GitHub: https://github.com/yiyungent/PluginCore
//===================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using PluginCore.AspNetCore.Interfaces;
using PluginCore.Infrastructure;
namespace PluginCore.AspNetCore.Middlewares
{
public class PluginStartupXMiddleware
{
private readonly RequestDelegate _next;
public PluginStartupXMiddleware(RequestDelegate next)
{
_next = next;
}
public static Action ReachedEndAction { get; set; } = () => { _isReachedEnd = true; };
private static bool _isReachedEnd;
public async Task InvokeAsync(HttpContext httpContext, IPluginApplicationBuilderManager pluginApplicationBuilderManager)
{
//bool isReachedEnd = false;
_isReachedEnd = false;
try
{
RequestDelegate requestDelegate = pluginApplicationBuilderManager.GetBuildResult();
await requestDelegate(httpContext);
}
catch (Exception ex)
{
// InvalidOperationException: The request reached the end of the pipeline without executing the endpoint: 'AspNetCore3_1.Controllers.WeatherForecastController.Get (AspNetCore3_1)'. Please register the EndpointMiddleware using 'IApplicationBuilder.UseEndpoints(...)' if using routing.
Utils.LogUtil.Error<PluginStartupXMiddleware>(ex, ex.Message);
if (ex.InnerException != null)
{
Utils.LogUtil.Error<PluginStartupXMiddleware>(ex.InnerException, ex.InnerException.Message);
}
}
if (_isReachedEnd)
{
// Call the next delegate/middleware in the pipeline
await _next(httpContext);
}
else
{
// 没有抵达 End, 说明在插件的 middleware 中已堵塞, 准备返回 响应
}
}
}
}

View File

@ -0,0 +1,107 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
<PackageId>PluginCore.AspNetCore</PackageId>
<Version>1.4.3</Version>
<Company>yiyun</Company>
<Authors>yiyun</Authors>
<Description>ASP.NET Core lightweight plugin framework</Description>
<Copyright>Copyright (c) 2021-present yiyun</Copyright>
<RepositoryUrl>https://github.com/yiyungent/PluginCore</RepositoryUrl>
<PackageLicenseUrl>https://github.com/yiyungent/PluginCore/blob/main/LICENSE</PackageLicenseUrl>
<PackageTags>PluginCore PluginCore.AspNetCore</PackageTags>
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<!-- 方便开发debug,与发布到nuget -->
<ItemGroup Condition="'$(Configuration)' == 'Release'">
<PackageReference Include="PluginCore" Version="2.2.5" />
</ItemGroup>
<ItemGroup Condition="'$(Configuration)' == 'Debug'">
<ProjectReference Include="..\PluginCore\PluginCore.csproj" />
</ItemGroup>
<ItemGroup Condition="'$(Configuration)' == 'Release'">
<PackageReference Include="PluginCore.IPlugins.AspNetCore" Version="0.1.1" />
</ItemGroup>
<ItemGroup Condition="'$(Configuration)' == 'Debug'">
<ProjectReference Include="..\PluginCore.IPlugins.AspNetCore\PluginCore.IPlugins.AspNetCore.csproj" />
</ItemGroup>
<!--<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" />
<PackageReference Include="System.Text.Json" Version="5.0.2" />
</ItemGroup>-->
<!-- 生成注释xml -->
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|netcoreapp3.1|AnyCPU'">
<DocumentationFile>bin\Release\netcoreapp3.1\PluginCore.AspNetCore.xml</DocumentationFile>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net5.0|AnyCPU'">
<DocumentationFile>bin\Release\net5.0\PluginCore.AspNetCore.xml</DocumentationFile>
</PropertyGroup>
<!-- 0.4.0 : 支持嵌入式 前端 (打包进dll) -->
<ItemGroup>
<EmbeddedResource Include="node_modules/plugincore-admin-frontend/dist/**/**/*" />
</ItemGroup>
<ItemGroup>
<Compile Remove="node_modules\**" />
<EmbeddedResource Remove="node_modules\**" />
<None Remove="node_modules\**" />
</ItemGroup>
<ItemGroup>
<Compile Remove="Controllers忽略\AppCenterController.cs" />
<Compile Remove="Controllers忽略\DebugController.cs" />
<Compile Remove="Controllers忽略\HomeController.cs" />
<Compile Remove="Controllers忽略\PluginsController.cs" />
<Compile Remove="Controllers忽略\PluginWidgetController.cs" />
<Compile Remove="Controllers忽略\UserController.cs" />
</ItemGroup>
<ItemGroup>
<None Remove="package-lock.json" />
</ItemGroup>
<ItemGroup>
<Folder Include="Controllers忽略\" />
</ItemGroup>
<!-- 将 PluginCoreAdmin\*\* 与 readme.txt 复制到build后文件夹且加入 nupkg -->
<!-- 从 0.2.0 开始, 支持远程前端,本地前端不再必要 -->
<!--<ItemGroup>
<Content Include="PluginCoreAdmin\*">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<CopyToPublishDirectory>Always</CopyToPublishDirectory>
</Content>
<Content Include="PluginCoreAdmin\*\*">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<CopyToPublishDirectory>Always</CopyToPublishDirectory>
</Content>
<Content Include="PluginCoreAdmin\*\*\*">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<CopyToPublishDirectory>Always</CopyToPublishDirectory>
</Content>
<Content Include="readme.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>-->
<!-- Use Visual Studio npm package if it is installed. -->
<PropertyGroup Condition="Exists('$(VsInstallRoot)\Web\External\npm.cmd')">
<Path>$(Path)$(VsInstallRoot)\Web\External\;</Path>
</PropertyGroup>
<Target Name="NpmInstall" BeforeTargets="Build">
<Exec Command="npm install" EnvironmentVariables="PATH=$(Path.Replace(';', '%3B'))" ContinueOnError="true">
<Output TaskParameter="ExitCode" PropertyName="ErrorCode" />
</Exec>
<Error Condition="'$(ErrorCode)' != '0'" Text="Node.js/npm is required to build this project. To continue, please install Node.js from https://nodejs.org/ or Visual Studio Installer, and then restart your command prompt or IDE." />
</Target>
</Project>

View File

@ -0,0 +1,24 @@
//===================================================
// License: GNU LGPLv3
// Contributors: yiyungent@gmail.com
// Project: https://yiyungent.github.io/PluginCore
// GitHub: https://github.com/yiyungent/PluginCore
//===================================================
using System;
using System.Collections.Generic;
using System.Text;
namespace PluginCore.AspNetCore.RequestModel.User
{
public class LoginRequestModel
{
public string UserName { get; set; }
public string Password { get; set; }
}
}

View File

@ -0,0 +1,24 @@
//===================================================
// License: GNU LGPLv3
// Contributors: yiyungent@gmail.com
// Project: https://yiyungent.github.io/PluginCore
// GitHub: https://github.com/yiyungent/PluginCore
//===================================================
using System;
using System.Collections.Generic;
using System.Text;
namespace PluginCore.AspNetCore.RequestModel.User
{
public class UpdateRequestModel
{
public string UserName { get; set; }
public string Password { get; set; }
}
}

View File

@ -0,0 +1,22 @@
//===================================================
// License: GNU LGPLv3
// Contributors: yiyungent@gmail.com
// Project: https://yiyungent.github.io/PluginCore
// GitHub: https://github.com/yiyungent/PluginCore
//===================================================
namespace PluginCore.AspNetCore.ResponseModel
{
public class BaseResponseModel
{
public int Code { get; set; }
public string Message { get; set; }
public object Data { get; set; }
}
}

View File

@ -0,0 +1,82 @@
# git-cliff ~ configuration file
# https://git-cliff.org/docs/configuration
[changelog]
# template for the changelog header
header = """
# Changelog\n
All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines.\n
"""
# template for the changelog body
# https://keats.github.io/tera/docs/#introduction
body = """
---
{% if version %}\
{% if previous.version %}\
## [{{ version | trim_start_matches(pat="v") }}]($REPO/compare/{{ previous.version }}..{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
{% endif %}\
{% else %}\
## [unreleased]
{% endif %}\
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | striptags | trim | upper_first }}
{% for commit in commits
| filter(attribute="scope")
| sort(attribute="scope") %}
- **({{commit.scope}})**{% if commit.breaking %} [**breaking**]{% endif %} \
{{ commit.message }} - ([{{ commit.id | truncate(length=7, end="") }}]($REPO/commit/{{ commit.id }})) - {{ commit.author.name }}
{%- endfor -%}
{% raw %}\n{% endraw %}\
{%- for commit in commits %}
{%- if commit.scope -%}
{% else -%}
- {% if commit.breaking %} [**breaking**]{% endif %}\
{{ commit.message }} - ([{{ commit.id | truncate(length=7, end="") }}]($REPO/commit/{{ commit.id }})) - {{ commit.author.name }}
{% endif -%}
{% endfor -%}
{% endfor %}\n
"""
# template for the changelog footer
footer = """
<!-- generated by git-cliff -->
"""
# remove the leading and trailing whitespace from the templates
trim = true
# postprocessors
postprocessors = [
{ pattern = '\$REPO', replace = "https://github.com/yiyungent/PluginCore" }, # replace repository URL
]
[git]
# parse the commits based on https://www.conventionalcommits.org
conventional_commits = true
# filter out the commits that are not conventional
filter_unconventional = true
# process each line of a commit as an individual commit
split_commits = false
# regex for preprocessing the commit messages
commit_preprocessors = [
# { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/orhun/git-cliff/issues/${2}))"}, # replace issue numbers
]
# regex for parsing and grouping commits
commit_parsers = [
{ message = "^feat", group = "Features" },
{ message = "^fix", group = "Bug Fixes" },
{ message = "^doc", group = "Documentation" },
{ message = "^perf", group = "Performance" },
{ message = "^refactor", group = "Refactoring" },
{ message = "^style", group = "Style" },
{ message = "^revert", group = "Revert" },
{ message = "^test", group = "Tests" },
{ message = "^chore\\(version\\):", skip = true },
{ message = "^chore", group = "Miscellaneous Chores" },
{ body = ".*security", group = "Security" },
]
# filter out the commits that are not matched by commit parsers
filter_commits = false
# sort the tags topologically
topo_order = false
# sort the commits inside sections by oldest/newest order
sort_commits = "oldest"

View File

@ -0,0 +1,82 @@
//===================================================
// License: GNU LGPLv3
// Contributors: yiyungent@gmail.com
// Project: https://yiyungent.github.io/PluginCore
// GitHub: https://github.com/yiyungent/PluginCore
//===================================================
using PluginCore.AspNetCore.Interfaces;
using PluginCore.Interfaces;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using System.Text;
using PluginCore.lmplements;
using PluginCore.Infrastructure;
namespace PluginCore.AspNetCore.lmplements
{
/// <summary>
/// 一个插件的所有dll由 一个 <see cref="IPluginContext"/> 管理
/// <see cref="PluginContextManager"/> 记录管理了所有 插件的<see cref="IPluginContext"/>
/// <see cref="AspNetCorePluginManager"/> 是对 <see cref="PluginContextManager"/>的封装, 使其更好管理插件加载释放的行为
/// </summary>
public class AspNetCorePluginManager : IPluginManager
{
private readonly IPluginControllerManager _pluginControllerManager;
public IPluginContextManager PluginContextManager { get; set; }
public IPluginContextPack PluginContextPack { get; set; }
public AspNetCorePluginManager(IPluginContextManager pluginContextManager, IPluginContextPack pluginContextPack, IPluginControllerManager pluginControllerManager)
{
this.PluginContextManager = pluginContextManager;
this.PluginContextPack = pluginContextPack;
_pluginControllerManager = pluginControllerManager;
}
/// <summary>
/// 加载插件程序集
/// </summary>
/// <param name="pluginId"></param>
public void LoadPlugin(string pluginId)
{
IPluginContext context = this.PluginContextPack.Pack(pluginId);
Assembly pluginMainAssembly = context.LoadFromAssemblyName(new AssemblyName(pluginId));
//注销内置Controller的注册 by tomny
// 加载其中的控制器
//_pluginControllerManager.AddControllers(pluginMainAssembly);
// 这个插件加载上下文 放入 集合中
this.PluginContextManager.Add(pluginId, context);
}
public void UnloadPlugin(string pluginId)
{
this.PluginContextManager.Remove(pluginId);
// 移除其中的控制器
_pluginControllerManager.RemoveControllers(pluginId);
}
/// <summary>
/// 加载插件程序集
/// </summary>
/// <param name="pluginId"></param>
public Assembly GetPluginAssembly(string pluginId)
{
IPluginContext context = this.PluginContextPack.Pack(pluginId);
Assembly pluginMainAssembly = context.LoadFromAssemblyName(new AssemblyName(pluginId));
return pluginMainAssembly;
}
}
}

View File

@ -0,0 +1,84 @@
//===================================================
// License: GNU LGPLv3
// Contributors: yiyungent@gmail.com
// Project: https://yiyungent.github.io/PluginCore
// GitHub: https://github.com/yiyungent/PluginCore
//===================================================
using PluginCore.AspNetCore.Interfaces;
using PluginCore.Interfaces;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using System.Text;
using PluginCore.lmplements;
using PluginCore.Infrastructure;
namespace PluginCore.AspNetCore.lmplements
{
// ********* 虽然 看上去和 AspNetCorePluginManager 一样, 但特别保留, 防止需要不一样的处理, 后续更新维护方便
/// <summary>
/// 一个插件的所有dll由 一个 <see cref="IPluginContext"/> 管理
/// <see cref="PluginContextManager"/> 记录管理了所有 插件的<see cref="CollectibleAssemblyLoadContext"/>
/// <see cref="AspNetCorePluginManagerV1"/> 是对 <see cref="PluginContextManager"/>的封装, 使其更好管理插件加载释放的行为
/// </summary>
public class AspNetCorePluginManagerV1 : IPluginManager
{
private readonly IPluginControllerManager _pluginControllerManager;
public IPluginContextManager PluginContextManager { get; set; }
public IPluginContextPack PluginContextPack { get; set; }
public AspNetCorePluginManagerV1(IPluginContextManager pluginContextManager, IPluginContextPack pluginContextPack, IPluginControllerManager pluginControllerManager)
{
this.PluginContextManager = pluginContextManager;
this.PluginContextPack = pluginContextPack;
_pluginControllerManager = pluginControllerManager;
}
/// <summary>
/// 加载插件程序集
/// </summary>
/// <param name="pluginId"></param>
public void LoadPlugin(string pluginId)
{
// 此插件的 加载上下文
IPluginContext context = this.PluginContextPack.Pack(pluginId);
// TODO: 注意: 未测试: 不清除 对于 旧版加载 dll 方式, 再结合 LoadFromAssemblyName 是否有效
Assembly pluginMainAssembly = context.LoadFromAssemblyName(new AssemblyName(pluginId));
//注销内置Controller的注册 by tomny
// 加载其中的控制器
//_pluginControllerManager.AddControllers(pluginMainAssembly);
// 这个插件加载上下文 放入 集合中
this.PluginContextManager.Add(pluginId, context);
}
public void UnloadPlugin(string pluginId)
{
this.PluginContextManager.Remove(pluginId);
// 移除其中的控制器
_pluginControllerManager.RemoveControllers(pluginId);
}
public Assembly GetPluginAssembly(string pluginId)
{
IPluginContext context = this.PluginContextPack.Pack(pluginId);
Assembly pluginMainAssembly = context.LoadFromAssemblyName(new AssemblyName(pluginId));
return pluginMainAssembly;
}
}
}

View File

@ -0,0 +1,40 @@
//===================================================
// License: GNU LGPLv3
// Contributors: yiyungent@gmail.com
// Project: https://yiyungent.github.io/PluginCore
// GitHub: https://github.com/yiyungent/PluginCore
//===================================================
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.Primitives;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
namespace PluginCore.AspNetCore.lmplements
{
/// <summary>
/// 目前采用的第一种方案
/// 方案一: https://www.codetd.com/article/461093
/// 方案二: https://www.cnblogs.com/artech/p/dynamic-controllers.html
/// </summary>
public class PluginActionDescriptorChangeProvider : IActionDescriptorChangeProvider
{
public static PluginActionDescriptorChangeProvider Instance { get; } = new PluginActionDescriptorChangeProvider();
public CancellationTokenSource TokenSource { get; private set; }
public bool HasChanged { get; set; }
public IChangeToken GetChangeToken()
{
TokenSource = new CancellationTokenSource();
return new CancellationChangeToken(TokenSource.Token);
}
}
}

View File

@ -0,0 +1,78 @@
//===================================================
// License: GNU LGPLv3
// Contributors: yiyungent@gmail.com
// Project: https://yiyungent.github.io/PluginCore
// GitHub: https://github.com/yiyungent/PluginCore
//===================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using PluginCore.AspNetCore.Interfaces;
using PluginCore.Interfaces;
using PluginCore.IPlugins;
using PluginCore.AspNetCore.Middlewares;
using PluginCore.AspNetCore.Infrastructure;
namespace PluginCore.AspNetCore.lmplements
{
public class PluginApplicationBuilderManager : PluginApplicationBuilderManager<PluginApplicationBuilder>
{
public PluginApplicationBuilderManager(IPluginFinder pluginFinder) : base(pluginFinder)
{
}
}
public class PluginApplicationBuilderManager<TPluginApplicationBuilder> : IPluginApplicationBuilderManager
where TPluginApplicationBuilder : PluginApplicationBuilder, new()
{
private readonly IPluginFinder _pluginFinder;
public PluginApplicationBuilderManager(IPluginFinder pluginFinder)
{
_pluginFinder = pluginFinder;
}
public static RequestDelegate RequestDelegateResult { get; set; }
/// <summary>
/// 插件 启用, 禁用 时: 重新 Build
/// </summary>
public void ReBuild()
{
TPluginApplicationBuilder applicationBuilder = new TPluginApplicationBuilder();
applicationBuilder.ReachEndAction = PluginStartupXMiddleware.ReachedEndAction;
var plugins = this._pluginFinder.EnablePlugins<IStartupXPlugin>()?.OrderBy(m => m.ConfigureOrder)?.ToList();
foreach (var item in plugins)
{
// 调用
Utils.LogUtil.Info<PluginApplicationBuilderManager>($"{item.GetType().ToString()} {nameof(IStartupXPlugin)}.{nameof(IStartupXPlugin.Configure)}");
item.Configure(applicationBuilder);
}
RequestDelegateResult = applicationBuilder.Build();
}
public RequestDelegate GetBuildResult()
{
if (RequestDelegateResult == null)
{
ReBuild();
}
return RequestDelegateResult;
}
}
}

View File

@ -0,0 +1,66 @@
//===================================================
// License: GNU LGPLv3
// Contributors: yiyungent@gmail.com
// Project: https://yiyungent.github.io/PluginCore
// GitHub: https://github.com/yiyungent/PluginCore
//===================================================
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using PluginCore.AspNetCore.Interfaces;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
namespace PluginCore.AspNetCore.lmplements
{
public class PluginControllerManager : IPluginControllerManager
{
private readonly ApplicationPartManager _applicationPartManager;
public PluginControllerManager(ApplicationPartManager applicationPartManager)
{
_applicationPartManager = applicationPartManager;
}
/// <summary>
/// 从指定<see cref="Assembly"/> 中获取 Controller, 并添加进来
/// </summary>
/// <param name="assembly"></param>
public void AddControllers(Assembly assembly)
{
AssemblyPart assemblyPart = new AssemblyPart(assembly);
_applicationPartManager.ApplicationParts.Add(assemblyPart);
ResetControllActions();
}
public void RemoveControllers(string pluginId)
{
ApplicationPart last = _applicationPartManager.ApplicationParts.First(m => m.Name == pluginId);
_applicationPartManager.ApplicationParts.Remove(last);
ResetControllActions();
}
/// <summary>
/// 通知应用(主程序)Controller.Action 已发生变化
/// </summary>
private void ResetControllActions()
{
PluginActionDescriptorChangeProvider.Instance.HasChanged = true;
// TokenSource 为 null
// 注意: 在程序刚启动时, 未抵达 Controller 时不会触发 IActionDescriptorChangeProvider.GetChangeToken(), 也就会导致 TokenSource 为 null, 在启动时同时在启动时插件Controller.Action和主程序一起被添加此时无需通知改变
if (PluginActionDescriptorChangeProvider.Instance.TokenSource != null)
{
// 只有在插件列表控制启用,禁用时才需通知改变
PluginActionDescriptorChangeProvider.Instance.TokenSource.Cancel();
}
}
}
}

View File

@ -0,0 +1 @@
dotnet pack -c Release

View File

@ -0,0 +1,307 @@
{
"name": "PluginCore",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "PluginCore",
"version": "1.0.0",
"dependencies": {
"plugincore-admin-frontend": "0.3.2"
}
},
"node_modules/ansi-styles": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-1.0.0.tgz",
"integrity": "sha512-3iF4FIKdxaVYT3JqQuY3Wat/T2t7TRbbQ94Fu50ZUCbLy4TFbTzr90NOHQodQkNqmeEGCw8WbeP78WNi6SKYUA==",
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/async-validator": {
"version": "1.8.5",
"resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-1.8.5.tgz",
"integrity": "sha512-tXBM+1m056MAX0E8TL2iCjg8WvSyXu0Zc8LNtYqrVeyoL3+esHRZ4SieE9fKQyyU09uONjnMEjrNBMqT0mbvmA==",
"dependencies": {
"babel-runtime": "6.x"
}
},
"node_modules/axios": {
"version": "0.18.1",
"resolved": "https://registry.npmmirror.com/axios/-/axios-0.18.1.tgz",
"integrity": "sha512-0BfJq4NSfQXd+SkFdrvFbG7addhYSBA2mQwISr46pD6E5iqkWg02RAs8vyTT/j0RTnoYmeXauBuSv1qKwR179g==",
"deprecated": "Critical security vulnerability fixed in v0.21.1. For more information, see https://github.com/axios/axios/pull/3410",
"dependencies": {
"follow-redirects": "1.5.10",
"is-buffer": "^2.0.2"
}
},
"node_modules/babel-helper-vue-jsx-merge-props": {
"version": "2.0.3",
"resolved": "https://registry.npmmirror.com/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-2.0.3.tgz",
"integrity": "sha512-gsLiKK7Qrb7zYJNgiXKpXblxbV5ffSwR0f5whkPAaBAR4fhi6bwRZxX9wBlIc5M/v8CCkXUbXZL4N/nSE97cqg=="
},
"node_modules/babel-runtime": {
"version": "6.26.0",
"resolved": "https://registry.npmmirror.com/babel-runtime/-/babel-runtime-6.26.0.tgz",
"integrity": "sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==",
"dependencies": {
"core-js": "^2.4.0",
"regenerator-runtime": "^0.11.0"
}
},
"node_modules/babel-runtime/node_modules/core-js": {
"version": "2.6.12",
"resolved": "https://registry.npmmirror.com/core-js/-/core-js-2.6.12.tgz",
"integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==",
"deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.",
"hasInstallScript": true
},
"node_modules/chalk": {
"version": "0.4.0",
"resolved": "https://registry.npmmirror.com/chalk/-/chalk-0.4.0.tgz",
"integrity": "sha512-sQfYDlfv2DGVtjdoQqxS0cEZDroyG8h6TamA6rvxwlrU5BaSLDx9xhatBYl2pxZ7gmpNaPFVwBtdGdu5rQ+tYQ==",
"dependencies": {
"ansi-styles": "~1.0.0",
"has-color": "~0.1.0",
"strip-ansi": "~0.1.0"
},
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/codemirror": {
"version": "5.65.2",
"resolved": "https://registry.npmmirror.com/codemirror/-/codemirror-5.65.2.tgz",
"integrity": "sha512-SZM4Zq7XEC8Fhroqe3LxbEEX1zUPWH1wMr5zxiBuiUF64iYOUH/JI88v4tBag8MiBS8B8gRv8O1pPXGYXQ4ErA=="
},
"node_modules/core-js": {
"version": "3.6.5",
"resolved": "https://registry.npmmirror.com/core-js/-/core-js-3.6.5.tgz",
"integrity": "sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==",
"deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.",
"hasInstallScript": true
},
"node_modules/debug": {
"version": "3.1.0",
"resolved": "https://registry.npmmirror.com/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/deepmerge": {
"version": "1.5.2",
"resolved": "https://registry.npmmirror.com/deepmerge/-/deepmerge-1.5.2.tgz",
"integrity": "sha512-95k0GDqvBjZavkuvzx/YqVLv/6YYa17fz6ILMSf7neqQITCPbnfEnQvEgMPNjH4kgobe7+WIL0yJEHku+H3qtQ==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/element-ui": {
"version": "2.13.2",
"resolved": "https://registry.npmmirror.com/element-ui/-/element-ui-2.13.2.tgz",
"integrity": "sha512-r761DRPssMPKDiJZWFlG+4e4vr0cRG/atKr3Eqr8Xi0tQMNbtmYU1QXvFnKiFPFFGkgJ6zS6ASkG+sellcoHlQ==",
"dependencies": {
"async-validator": "~1.8.1",
"babel-helper-vue-jsx-merge-props": "^2.0.0",
"deepmerge": "^1.2.0",
"normalize-wheel": "^1.0.1",
"resize-observer-polyfill": "^1.5.0",
"throttle-debounce": "^1.0.1"
},
"peerDependencies": {
"vue": "^2.5.17"
}
},
"node_modules/follow-redirects": {
"version": "1.5.10",
"resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.5.10.tgz",
"integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
"dependencies": {
"debug": "=3.1.0"
},
"engines": {
"node": ">=4.0"
}
},
"node_modules/has-color": {
"version": "0.1.7",
"resolved": "https://registry.npmmirror.com/has-color/-/has-color-0.1.7.tgz",
"integrity": "sha512-kaNz5OTAYYmt646Hkqw50/qyxP2vFnTVu5AQ1Zmk22Kk5+4Qx6BpO8+u7IKsML5fOsFk0ZT0AcCJNYwcvaLBvw==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-buffer": {
"version": "2.0.5",
"resolved": "https://registry.npmmirror.com/is-buffer/-/is-buffer-2.0.5.tgz",
"integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==",
"engines": {
"node": ">=4"
}
},
"node_modules/js-cookie": {
"version": "2.2.0",
"resolved": "https://registry.npmmirror.com/js-cookie/-/js-cookie-2.2.0.tgz",
"integrity": "sha512-7YAJP/LPE/MhDjHIdfIiT665HUSumCwPN2hAmO6OJZ8V3o1mtz2HeQ8BKetEjkh+3nqGxYaq1vPMViUR8kaOXw=="
},
"node_modules/jsonlint": {
"version": "1.6.3",
"resolved": "https://registry.npmmirror.com/jsonlint/-/jsonlint-1.6.3.tgz",
"integrity": "sha512-jMVTMzP+7gU/IyC6hvKyWpUU8tmTkK5b3BPNuMI9U8Sit+YAWLlZwB6Y6YrdCxfg2kNz05p3XY3Bmm4m26Nv3A==",
"dependencies": {
"JSV": "^4.0.x",
"nomnom": "^1.5.x"
},
"bin": {
"jsonlint": "lib/cli.js"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/JSV": {
"version": "4.0.2",
"resolved": "https://registry.npmmirror.com/JSV/-/JSV-4.0.2.tgz",
"integrity": "sha512-ZJ6wx9xaKJ3yFUhq5/sk82PJMuUyLk277I8mQeyDgCTjGdjWJIvPfaU5LIXaMuaN2UO1X3kZH4+lgphublZUHw==",
"engines": {
"node": "*"
}
},
"node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
"node_modules/nomnom": {
"version": "1.8.1",
"resolved": "https://registry.npmmirror.com/nomnom/-/nomnom-1.8.1.tgz",
"integrity": "sha512-5s0JxqhDx9/rksG2BTMVN1enjWSvPidpoSgViZU4ZXULyTe+7jxcCRLB6f42Z0l1xYJpleCBtSyY6Lwg3uu5CQ==",
"deprecated": "Package no longer supported. Contact support@npmjs.com for more info.",
"dependencies": {
"chalk": "~0.4.0",
"underscore": "~1.6.0"
}
},
"node_modules/normalize-wheel": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/normalize-wheel/-/normalize-wheel-1.0.1.tgz",
"integrity": "sha512-1OnlAPZ3zgrk8B91HyRj+eVv+kS5u+Z0SCsak6Xil/kmgEia50ga7zfkumayonZrImffAxPU/5WcyGhzetHNPA=="
},
"node_modules/normalize.css": {
"version": "7.0.0",
"resolved": "https://registry.npmmirror.com/normalize.css/-/normalize.css-7.0.0.tgz",
"integrity": "sha512-LYaFZxj2Q1Q9e1VJ0f6laG46Rt5s9URhKyckNaA2vZnL/0gwQHWhM7ALQkp3WBQKM5sXRLQ5Ehrfkp+E/ZiCRg=="
},
"node_modules/nprogress": {
"version": "0.2.0",
"resolved": "https://registry.npmmirror.com/nprogress/-/nprogress-0.2.0.tgz",
"integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA=="
},
"node_modules/path-to-regexp": {
"version": "2.4.0",
"resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-2.4.0.tgz",
"integrity": "sha512-G6zHoVqC6GGTQkZwF4lkuEyMbVOjoBKAEybQUypI1WTkqinCOrq2x6U2+phkJ1XsEMTy4LjtwPI7HW+NVrRR2w=="
},
"node_modules/plugincore-admin-frontend": {
"version": "0.3.2",
"resolved": "https://registry.npmmirror.com/plugincore-admin-frontend/-/plugincore-admin-frontend-0.3.2.tgz",
"integrity": "sha512-w8t6J+92Ns/gan/3uLW9Uns6vbAEkOzYBzueCJY5b7i+F7wyvmh0TcOHQscm6VmJQW+d73eaTk4psIcUcHbOng==",
"dependencies": {
"axios": "0.18.1",
"codemirror": "^5.57.0",
"core-js": "3.6.5",
"element-ui": "2.13.2",
"js-cookie": "2.2.0",
"jsonlint": "^1.6.3",
"normalize.css": "7.0.0",
"nprogress": "0.2.0",
"path-to-regexp": "2.4.0",
"script-loader": "^0.7.2",
"vue": "2.6.10",
"vue-i18n": "^8.26.7",
"vue-meditor": "^2.1.1",
"vue-router": "3.0.6",
"vuex": "3.1.0"
},
"engines": {
"node": ">=8.9",
"npm": ">= 3.0.0"
}
},
"node_modules/raw-loader": {
"version": "0.5.1",
"resolved": "https://registry.npmmirror.com/raw-loader/-/raw-loader-0.5.1.tgz",
"integrity": "sha512-sf7oGoLuaYAScB4VGr0tzetsYlS8EJH6qnTCfQ/WVEa89hALQ4RQfCKt5xCyPQKPDUbVUAIP1QsxAwfAjlDp7Q=="
},
"node_modules/regenerator-runtime": {
"version": "0.11.1",
"resolved": "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz",
"integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg=="
},
"node_modules/resize-observer-polyfill": {
"version": "1.5.1",
"resolved": "https://registry.npmmirror.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
"integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg=="
},
"node_modules/script-loader": {
"version": "0.7.2",
"resolved": "https://registry.npmmirror.com/script-loader/-/script-loader-0.7.2.tgz",
"integrity": "sha512-UMNLEvgOAQuzK8ji8qIscM3GIrRCWN6MmMXGD4SD5l6cSycgGsCo0tX5xRnfQcoghqct0tjHjcykgI1PyBE2aA==",
"dependencies": {
"raw-loader": "~0.5.1"
}
},
"node_modules/strip-ansi": {
"version": "0.1.1",
"resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-0.1.1.tgz",
"integrity": "sha512-behete+3uqxecWlDAm5lmskaSaISA+ThQ4oNNBDTBJt0x2ppR6IPqfZNuj6BLaLJ/Sji4TPZlcRyOis8wXQTLg==",
"bin": {
"strip-ansi": "cli.js"
},
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/throttle-debounce": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/throttle-debounce/-/throttle-debounce-1.1.0.tgz",
"integrity": "sha512-XH8UiPCQcWNuk2LYePibW/4qL97+ZQ1AN3FNXwZRBNPPowo/NRU5fAlDCSNBJIYCKbioZfuYtMhG4quqoJhVzg==",
"engines": {
"node": ">=4"
}
},
"node_modules/underscore": {
"version": "1.6.0",
"resolved": "https://registry.npmmirror.com/underscore/-/underscore-1.6.0.tgz",
"integrity": "sha512-z4o1fvKUojIWh9XuaVLUDdf86RQiq13AC1dmHbTpoyuu+bquHms76v16CjycCbec87J7z0k//SiQVk0sMdFmpQ=="
},
"node_modules/vue": {
"version": "2.6.10",
"resolved": "https://registry.npmmirror.com/vue/-/vue-2.6.10.tgz",
"integrity": "sha512-ImThpeNU9HbdZL3utgMCq0oiMzAkt1mcgy3/E6zWC/G6AaQoeuFdsl9nDhTDU3X1R6FK7nsIUuRACVcjI+A2GQ=="
},
"node_modules/vue-i18n": {
"version": "8.27.1",
"resolved": "https://registry.npmmirror.com/vue-i18n/-/vue-i18n-8.27.1.tgz",
"integrity": "sha512-lWrGm4F25qReJ7XxSnFVb2h3PfW54ldnM4C+YLBGGJ75+Myt/kj4hHSTKqsyDLamvNYpvINMicSOdW+7yuqgIQ=="
},
"node_modules/vue-meditor": {
"version": "2.1.1",
"resolved": "https://registry.npmmirror.com/vue-meditor/-/vue-meditor-2.1.1.tgz",
"integrity": "sha512-xvLqqvS4LzEJ7TqW2mDSzvueMey/WM92o0jfmqZWBTks1/8EAmn0ehqZDlcq/W+1goQpUmWE4m3yJjzQnoZhQQ=="
},
"node_modules/vue-router": {
"version": "3.0.6",
"resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-3.0.6.tgz",
"integrity": "sha512-Ox0ciFLswtSGRTHYhGvx2L44sVbTPNS+uD2kRISuo8B39Y79rOo0Kw0hzupTmiVtftQYCZl87mwldhh2L9Aquw=="
},
"node_modules/vuex": {
"version": "3.1.0",
"resolved": "https://registry.npmmirror.com/vuex/-/vuex-3.1.0.tgz",
"integrity": "sha512-mdHeHT/7u4BncpUZMlxNaIdcN/HIt1GsGG5LKByArvYG/v6DvHcOxvDCts+7SRdCoIRGllK8IMZvQtQXLppDYg=="
}
}
}

View File

@ -0,0 +1,8 @@
{
"name": "PluginCore",
"version": "1.0.0",
"private": true,
"dependencies": {
"plugincore-admin-frontend": "0.3.2"
}
}

View File

@ -0,0 +1,11 @@

`PluginCoreAdmin` is the web frontend for the `PluginCore management center (PluginCore Admin)` and should be released together.
https://github.com/yiyungent/PluginCore

View File

@ -0,0 +1,54 @@
# Changelog
All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines.
---
## [unreleased]
### Documentation
- **(CHANGELOG.md)** update - ([0bbfc89](https://github.com/yiyungent/PluginCore/commit/0bbfc8955b7f6338db2125c78ec250e9eeeadcce)) - github-actions[bot]
- **(CHANGELOG.md)** update - ([4f6b47b](https://github.com/yiyungent/PluginCore/commit/4f6b47b3f86bfce4a8f660166837a7322c568d78)) - github-actions[bot]
### Miscellaneous Chores
- **(cliff.toml)** add - ([5614ef0](https://github.com/yiyungent/PluginCore/commit/5614ef024d644349095e19a0016bb23d989b0c90)) - yiyun
---
## [PluginCore.IPlugins.AspNetCore-v0.1.1](https://github.com/yiyungent/PluginCore/compare/PluginCore.IPlugins.AspNetCore-v0.1.0..PluginCore.IPlugins.AspNetCore-v0.1.1) - 2023-12-30
### Features
- **(src/**/*.cs)** // License: Apache-2.0 -> // License: GNU LGPLv3 - ([57366d3](https://github.com/yiyungent/PluginCore/commit/57366d3e2afdb8e20e94851aa8a09f1ee61b6d7e)) - yiyun
- **(src/**/*.cs)** // Project: https://moeci.com/PluginCore -> // Project: https://yiyungent.github.io/PluginCore - ([7420480](https://github.com/yiyungent/PluginCore/commit/742048065978c1b8597fab3d52f011db4247fbda)) - yiyun
### Build
- **(plugincore.iplugins.aspnetcore.csproj)** 0.1.0 -> 0.1.1 - ([338e919](https://github.com/yiyungent/PluginCore/commit/338e919c74dcff4199b82c6046a62847cc68beb3)) - yiyun
---
## [PluginCore.IPlugins.AspNetCore-v0.1.0](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v1.0.2..PluginCore.IPlugins.AspNetCore-v0.1.0) - 2023-02-15
### Build
- **(plugincore.iplugins.aspnetcore.csproj)** `<Version>0.1.0</Version>` - ([162d304](https://github.com/yiyungent/PluginCore/commit/162d304f6b74941701b5b799228a3061ef4ea6c2)) - yiyun
---
## [PluginCore.AspNetCore-v1.0.2](https://github.com/yiyungent/PluginCore/compare/PluginCore.IPlugins.AspNetCore-v0.0.1..PluginCore.AspNetCore-v1.0.2) - 2022-04-19
### Style
- add: copyright: *.cs - ([9643dce](https://github.com/yiyungent/PluginCore/commit/9643dce112861a440d63306cb555accbed3d5111)) - yiyun
---
## [PluginCore.IPlugins.AspNetCore-v0.0.1](https://github.com/yiyungent/PluginCore/compare/PluginCore-v1.0.0..PluginCore.IPlugins.AspNetCore-v0.0.1) - 2022-04-16
### Refactoring
- 1.提取出 PluginCore.AspNetCore,PluginCore.IPlugins.AspNetCore 2.提取出更多接口,可自由替换 - ([fffd8d9](https://github.com/yiyungent/PluginCore/commit/fffd8d91c23fd6e4a4d09cbf91975beb3cf7acf0)) - yiyun
### Build
- **(plugincore.iplugins.aspnetcore.csproj)** <Version>0.0.1</Version> - ([b907263](https://github.com/yiyungent/PluginCore/commit/b9072639d894904add2faf46216e29f902ddf32b)) - yiyun
<!-- generated by git-cliff -->

View File

@ -0,0 +1,29 @@
//===================================================
// License: GNU LGPLv3
// Contributors: yiyungent@gmail.com
// Project: https://yiyungent.github.io/PluginCore
// GitHub: https://github.com/yiyungent/PluginCore
//===================================================
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace PluginCore.IPlugins
{
/// <summary>
/// 目前未有效化,占坑
/// </summary>
public interface IContentFilterPlugin : IPlugin
{
Task<string> RequestBodyFilter(string urlPath, string content);
Task<string> ReponseBodyFilter(string urlPath, string content);
}
}

View File

@ -0,0 +1,29 @@
//===================================================
// License: GNU LGPLv3
// Contributors: yiyungent@gmail.com
// Project: https://yiyungent.github.io/PluginCore
// GitHub: https://github.com/yiyungent/PluginCore
//===================================================
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
namespace PluginCore.IPlugins
{
/// <summary>
/// 实验性: 不确定有效, 发现问题,请 new issue
/// </summary>
public interface IHttpFilterPlugin : IPlugin
{
Task HttpEndFilter(HttpContext httpContext);
Task HttpStartFilter(HttpContext httpContext);
}
}

View File

@ -0,0 +1,40 @@
//===================================================
// License: GNU LGPLv3
// Contributors: yiyungent@gmail.com
// Project: https://yiyungent.github.io/PluginCore
// GitHub: https://github.com/yiyungent/PluginCore
//===================================================
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
namespace PluginCore.IPlugins
{
/// <summary>
/// 实验阶段
/// <para>无法热插拔: 需要启用插件后, 再 重启站点</para>
/// </summary>
public interface IStartupPlugin : IPlugin
{
/// <summary>
/// This method gets called by the runtime. Use this method to add services to the container.
/// </summary>
/// <param name="services"></param>
void ConfigureServices(IServiceCollection services);
int ConfigureServicesOrder { get; }
/// <summary>
/// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
/// </summary>
/// <param name="app"></param>
/// <param name="env"></param>
void Configure(IApplicationBuilder app);
int ConfigureOrder { get; }
}
}

View File

@ -0,0 +1,45 @@
//===================================================
// License: GNU LGPLv3
// Contributors: yiyungent@gmail.com
// Project: https://yiyungent.github.io/PluginCore
// GitHub: https://github.com/yiyungent/PluginCore
//===================================================
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
namespace PluginCore.IPlugins
{
/// <summary>
/// 实验阶段
/// <para>热插拔: 已有效化</para>
/// </summary>
public interface IStartupXPlugin : IPlugin
{
/// <summary>
/// This method gets called by the runtime. Use this method to add services to the container.
/// <para>未有效化</para>
/// </summary>
/// <param name="services"></param>
void ConfigureServices(IServiceCollection services);
int ConfigureServicesOrder { get; }
/// <summary>
/// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
/// </summary>
/// <param name="app"></param>
/// <param name="env"></param>
void Configure(IApplicationBuilder app);
int ConfigureOrder { get; }
}
}

View File

@ -0,0 +1,40 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
<PackageId>PluginCore.IPlugins.AspNetCore</PackageId>
<Version>0.1.1</Version>
<Company>yiyun</Company>
<Authors>yiyun</Authors>
<Description>PluginCore.AspNetCore Plugin Sdk</Description>
<Copyright>Copyright (c) 2021-present yiyun</Copyright>
<RepositoryUrl>https://github.com/yiyungent/PluginCore</RepositoryUrl>
<PackageLicenseUrl>https://github.com/yiyungent/PluginCore/blob/main/LICENSE</PackageLicenseUrl>
<PackageTags>PluginCore PluginCore.IPlugins.AspNetCore</PackageTags>
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
</PropertyGroup>
<!-- 方便开发debug,与发布到nuget -->
<ItemGroup Condition="'$(Configuration)' == 'Release'">
<PackageReference Include="PluginCore.IPlugins" Version="0.9.1" />
</ItemGroup>
<ItemGroup Condition="'$(Configuration)' == 'Debug'">
<ProjectReference Include="..\PluginCore.IPlugins\PluginCore.IPlugins.csproj" />
</ItemGroup>
<!-- 生成注释xml -->
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|netstandard2.0|AnyCPU'">
<DocumentationFile>bin\Release\netstandard2.0\PluginCore.IPlugins.AspNetCore.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<Folder Include="Infrastructure\" />
<Folder Include="Interfaces\" />
<Folder Include="Models\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.3.0" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,82 @@
# git-cliff ~ configuration file
# https://git-cliff.org/docs/configuration
[changelog]
# template for the changelog header
header = """
# Changelog\n
All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines.\n
"""
# template for the changelog body
# https://keats.github.io/tera/docs/#introduction
body = """
---
{% if version %}\
{% if previous.version %}\
## [{{ version | trim_start_matches(pat="v") }}]($REPO/compare/{{ previous.version }}..{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
{% endif %}\
{% else %}\
## [unreleased]
{% endif %}\
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | striptags | trim | upper_first }}
{% for commit in commits
| filter(attribute="scope")
| sort(attribute="scope") %}
- **({{commit.scope}})**{% if commit.breaking %} [**breaking**]{% endif %} \
{{ commit.message }} - ([{{ commit.id | truncate(length=7, end="") }}]($REPO/commit/{{ commit.id }})) - {{ commit.author.name }}
{%- endfor -%}
{% raw %}\n{% endraw %}\
{%- for commit in commits %}
{%- if commit.scope -%}
{% else -%}
- {% if commit.breaking %} [**breaking**]{% endif %}\
{{ commit.message }} - ([{{ commit.id | truncate(length=7, end="") }}]($REPO/commit/{{ commit.id }})) - {{ commit.author.name }}
{% endif -%}
{% endfor -%}
{% endfor %}\n
"""
# template for the changelog footer
footer = """
<!-- generated by git-cliff -->
"""
# remove the leading and trailing whitespace from the templates
trim = true
# postprocessors
postprocessors = [
{ pattern = '\$REPO', replace = "https://github.com/yiyungent/PluginCore" }, # replace repository URL
]
[git]
# parse the commits based on https://www.conventionalcommits.org
conventional_commits = true
# filter out the commits that are not conventional
filter_unconventional = true
# process each line of a commit as an individual commit
split_commits = false
# regex for preprocessing the commit messages
commit_preprocessors = [
# { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/orhun/git-cliff/issues/${2}))"}, # replace issue numbers
]
# regex for parsing and grouping commits
commit_parsers = [
{ message = "^feat", group = "Features" },
{ message = "^fix", group = "Bug Fixes" },
{ message = "^doc", group = "Documentation" },
{ message = "^perf", group = "Performance" },
{ message = "^refactor", group = "Refactoring" },
{ message = "^style", group = "Style" },
{ message = "^revert", group = "Revert" },
{ message = "^test", group = "Tests" },
{ message = "^chore\\(version\\):", skip = true },
{ message = "^chore", group = "Miscellaneous Chores" },
{ body = ".*security", group = "Security" },
]
# filter out the commits that are not matched by commit parsers
filter_commits = false
# sort the tags topologically
topo_order = false
# sort the commits inside sections by oldest/newest order
sort_commits = "oldest"

View File

@ -0,0 +1 @@
dotnet pack -c Release

View File

@ -0,0 +1,67 @@
//===================================================
// License: GNU LGPLv3
// Contributors: yiyungent@gmail.com
// Project: https://yiyungent.github.io/PluginCore
// GitHub: https://github.com/yiyungent/PluginCore
//===================================================
using System;
using System.Collections.Generic;
using System.Text;
namespace PluginCore.IPlugins
{
public abstract class BasePlugin : IPlugin
{
/// <summary>
/// 在启用插件之后: 这时插件Assemblies已被加载(插件上下文已load)
/// </summary>
/// <returns>
/// <para>当 IsSuccess 为 False, 主程序之后会回滚插件状态: (1)unload插件上下文 (2)更新plugin.config.json,标记为禁用状态</para>
/// </returns>
public virtual (bool IsSuccess, string Message) AfterEnable()
{
return (true, "启用成功");
}
/// <summary>
/// 在禁用插件之前: 这时插件Assemblies还未被释放(插件上下文未Unload)
/// </summary>
/// <returns>
/// <para>只有当 IsSuccess 为 True, 主程序之后才会释放插件上下文, 并标记为已禁用</para>
/// <para>当 IsSuccess 为 False, 主程序不会释放插件上下文,也不会标记为禁用, 插件维持原状态</para>
/// </returns>
public virtual (bool IsSuccess, string Message) BeforeDisable()
{
return (true, "禁用成功");
}
/// <summary>
/// TODO: 更新未做
/// </summary>
/// <param name="currentVersion"></param>
/// <param name="targetVersion"></param>
/// <returns></returns>
public virtual (bool IsSuccess, string Message) Update(string currentVersion, string targetVersion)
{
return (true, "更新成功");
}
public virtual void AppStart()
{
}
public virtual List<string> AppStartOrderDependPlugins
{
get
{
return new List<string>();
}
}
}
}

View File

@ -0,0 +1,141 @@
# Changelog
All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines.
---
## [unreleased]
### Documentation
- **(CHANGELOG.md)** update - ([0bbfc89](https://github.com/yiyungent/PluginCore/commit/0bbfc8955b7f6338db2125c78ec250e9eeeadcce)) - github-actions[bot]
- **(CHANGELOG.md)** update - ([4f6b47b](https://github.com/yiyungent/PluginCore/commit/4f6b47b3f86bfce4a8f660166837a7322c568d78)) - github-actions[bot]
### Miscellaneous Chores
- **(cliff.toml)** add - ([5614ef0](https://github.com/yiyungent/PluginCore/commit/5614ef024d644349095e19a0016bb23d989b0c90)) - yiyun
---
## [PluginCore.IPlugins-v0.9.1](https://github.com/yiyungent/PluginCore/compare/PluginCore.IPlugins-v0.9.0..PluginCore.IPlugins-v0.9.1) - 2023-12-30
### Features
- **(src/**/*.cs)** // License: Apache-2.0 -> // License: GNU LGPLv3 - ([57366d3](https://github.com/yiyungent/PluginCore/commit/57366d3e2afdb8e20e94851aa8a09f1ee61b6d7e)) - yiyun
- **(src/**/*.cs)** // Project: https://moeci.com/PluginCore -> // Project: https://yiyungent.github.io/PluginCore - ([7420480](https://github.com/yiyungent/PluginCore/commit/742048065978c1b8597fab3d52f011db4247fbda)) - yiyun
- **(src/plugincore.iplugins/constants.cs)** add - ([1278d0f](https://github.com/yiyungent/PluginCore/commit/1278d0f4acaa201869e0eb014156e14c6575cd00)) - yiyun
- **(src/plugincore.iplugins/constants.cs)** add: AspNetCoreAuthenticationScheme - ([697fe42](https://github.com/yiyungent/PluginCore/commit/697fe422408eec364075689c60aa9771113e1bd2)) - yiyun
- **(src/plugincore.iplugins/constants.cs)** add: AspNetCoreLanguageCookieName = "language" - ([e3f1196](https://github.com/yiyungent/PluginCore/commit/e3f119655739a510a6804101c4e5d7067719ff86)) - yiyun
- **(src/plugincore.iplugins/constants.cs)** add: AspNetCoreLanguageKey = "PluginCore.Admin.Language" - ([b50fa81](https://github.com/yiyungent/PluginCore/commit/b50fa81fb9efa87ae8048ab1925d3f79ec7c869c)) - yiyun
### Miscellaneous Chores
- **(src/plugincore.iplugins/plugincore.iplugins.csproj)** 0.9.0 -> 0.9.1 - ([d842de1](https://github.com/yiyungent/PluginCore/commit/d842de15552e19f7ba8f75e5ef89c68713ef31a5)) - yiyun
---
## [PluginCore.IPlugins-v0.9.0](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v1.0.2..PluginCore.IPlugins-v0.9.0) - 2023-02-15
### Features
- **(plugincore.aspnetcore,plugincore.iplugins,plugincore)** 仅保留已启用/已禁用 状态, IPlugin新方法 - ([e843a5b](https://github.com/yiyungent/PluginCore/commit/e843a5ba9fad4e88290c09bb3282b730c44c5a06)) - yiyun
### Build
- **(src/plugincore.iplugins/plugincore.iplugins.csproj)** <Version>0.9.0</Version> - ([4f07e99](https://github.com/yiyungent/PluginCore/commit/4f07e99d176421853e276c2a83e84433592f5112)) - yiyun
---
## [PluginCore.AspNetCore-v1.0.2](https://github.com/yiyungent/PluginCore/compare/PluginCore.IPlugins-v0.8.0..PluginCore.AspNetCore-v1.0.2) - 2022-04-19
### Style
- add: copyright: *.cs - ([9643dce](https://github.com/yiyungent/PluginCore/commit/9643dce112861a440d63306cb555accbed3d5111)) - yiyun
---
## [PluginCore.IPlugins-v0.8.0](https://github.com/yiyungent/PluginCore/compare/PluginCore.IPlugins-v0.7.0..PluginCore.IPlugins-v0.8.0) - 2022-04-16
### Miscellaneous Chores
- **(plugincore.iplugins.csproj)** <Version>0.8.0</Version> - ([853e638](https://github.com/yiyungent/PluginCore/commit/853e63850940aeecc0492bb12da54c548321e408)) - yiyun
### Refactoring
- 1.提取出 PluginCore.AspNetCore,PluginCore.IPlugins.AspNetCore 2.提取出更多接口,可自由替换 - ([fffd8d9](https://github.com/yiyungent/PluginCore/commit/fffd8d91c23fd6e4a4d09cbf91975beb3cf7acf0)) - yiyun
---
## [PluginCore.IPlugins-v0.7.0](https://github.com/yiyungent/PluginCore/compare/PluginCore.IPlugins-v0.6.1..PluginCore.IPlugins-v0.7.0) - 2022-02-09
### Features
- **(helloworldplugin.cs,iwidgetplugin.cs,plugincore)** add: Plugin Widget - ([0f010e9](https://github.com/yiyungent/PluginCore/commit/0f010e9cb9b11c4ccda51c40656dc5fd82a16a01)) - yiyun
### Miscellaneous Chores
- **(plugincore.iplugins.csproj)** 0.7.0 - ([87eda42](https://github.com/yiyungent/PluginCore/commit/87eda427bae83181559de92abaa8241f6e94199a)) - yiyun
---
## [PluginCore.IPlugins-v0.6.1](https://github.com/yiyungent/PluginCore/compare/PluginCore.IPlugins-v0.6.0..PluginCore.IPlugins-v0.6.1) - 2021-09-01
### Features
- **(pluginsettingsmodelfactory.cs,plugincore.iplugins.csproj)** remove: Newtonsoft.Json - ([84db1d3](https://github.com/yiyungent/PluginCore/commit/84db1d3f2bf9bae71320883b4c92f7e0f565bf15)) - yiyun
### Miscellaneous Chores
- **(plugincore.iplugins.csproj)** 0.6.1 - ([97e88ed](https://github.com/yiyungent/PluginCore/commit/97e88edeacd7b2526f5899db67d66165eb3f4dc9)) - yiyun
---
## [PluginCore.IPlugins-v0.6.0](https://github.com/yiyungent/PluginCore/compare/PluginCore.IPlugins-v0.5.0..PluginCore.IPlugins-v0.6.0) - 2021-08-21
### Features
- **(testtimejobplugin,plugincore.iplugins,plugincore)** timeJobPlugin 相关 - ([55d4f4c](https://github.com/yiyungent/PluginCore/commit/55d4f4ca7ddd9738216b9434ad1c30ef75f06471)) - yiyun
### Miscellaneous Chores
- **(plugincore.iplugins.csproj)** 0.6.0 - ([402abb3](https://github.com/yiyungent/PluginCore/commit/402abb38d25c8677b671e8e4ac3aa3f08fb33f51)) - yiyun
---
## [PluginCore.IPlugins-v0.5.0](https://github.com/yiyungent/PluginCore/compare/PluginCore.IPlugins-v0.4.0..PluginCore.IPlugins-v0.5.0) - 2021-08-21
### Features
- **(plugins,plugincore.iplugins,plugincore)** add: order, add: PluginApplicationBuilderManager - ([5e4a5f4](https://github.com/yiyungent/PluginCore/commit/5e4a5f46a4eb3aaca5d978fc1e695d0849e11e5c)) - yiyun
### Miscellaneous Chores
- **(plugincore.iplugins.csproj)** 0.5.0 - ([60eaa08](https://github.com/yiyungent/PluginCore/commit/60eaa08c68e46668d9a6d83b2b7664c6843fadd3)) - yiyun
---
## [PluginCore.IPlugins-v0.4.0](https://github.com/yiyungent/PluginCore/compare/PluginCore.IPlugins-v0.2.0..PluginCore.IPlugins-v0.4.0) - 2021-08-20
### Features
- **(istartupplugin.cs,istartupxplugin.cs)** 添加注释 - ([d4519b5](https://github.com/yiyungent/PluginCore/commit/d4519b54e9df931c6e75d9ca59742edc5f3185ac)) - yiyun
- **(plugincore)** pluginContentFilterMiddleware, IContentFilterPlugin - ([2597e9c](https://github.com/yiyungent/PluginCore/commit/2597e9c054bde134f9f250071347990be59e8d37)) - yiyun
- **(plugincore,/plugincore.iplugins)** pluginHttpEndFilter - ([c0cd458](https://github.com/yiyungent/PluginCore/commit/c0cd4581df72cdb9f4f678a531e7f04980c9695d)) - yiyun
- **(plugincore,plugincore.iplugins,helloworldplugin)** iStartupXPlugin: 运行时 Configure(app) - ([0d18a6f](https://github.com/yiyungent/PluginCore/commit/0d18a6f9949faa1e92f1d20da35689e8e153bac1)) - yiyun
- **(plugincore.iplugins)** iStartupPlugin.cs, PluginCore.IPlugins.csproj - ([4459fbe](https://github.com/yiyungent/PluginCore/commit/4459fbe5e2cbe369519b7010a7b7d6d4600738cf)) - yiyun
- **(plugincore.iplugins.csproj)** 0.3.0 - ([c8000be](https://github.com/yiyungent/PluginCore/commit/c8000bec4800826afa5db37edfb095a945231591)) - yiyun
- 生成注释xml: PluginCore.IPlugins,PluginCore - ([5878148](https://github.com/yiyungent/PluginCore/commit/5878148244344f412e75fe9446824dd99ca2de47)) - yiyun
### Miscellaneous Chores
- **(plugincore.iplugins)** 注释 - ([4fe65ce](https://github.com/yiyungent/PluginCore/commit/4fe65ce4e731e1a67d35f2c202239f062fe45adc)) - yiyun
- **(plugincore.iplugins.csproj)** 0.4.0 - ([a6e8851](https://github.com/yiyungent/PluginCore/commit/a6e8851b75dabb8ca68d8e14124a1332e7c13ad7)) - yiyun
---
## [PluginCore.IPlugins-v0.2.0](https://github.com/yiyungent/PluginCore/compare/PluginCore.IPlugins-v0.1.0..PluginCore.IPlugins-v0.2.0) - 2021-08-10
### Features
- **(plugincore.iplugins.csproj)** 0.2.0 - ([d7fb02f](https://github.com/yiyungent/PluginCore/commit/d7fb02fe481e1b2d20a7f7b34f0fa50e95240059)) - yiyun
- plugin 支持加载插件 wwwroot 文件夹下的 html前端等 - ([273f9a4](https://github.com/yiyungent/PluginCore/commit/273f9a44c8727675f60d364fcf59a373958b3575)) - yiyun
---
## [PluginCore.IPlugins-v0.1.0] - 2021-08-08
### Features
- pluginCore.IPlugins, plugins: HelloWorldPlugin - ([1e81de2](https://github.com/yiyungent/PluginCore/commit/1e81de2107394f527a94ec5d4c2ae6853d2d5526)) - yiyun
- pluginCore, plugins/HelloWorldPlugin - ([5141afd](https://github.com/yiyungent/PluginCore/commit/5141afded8feba94af581d6132fccb87aafa516c)) - yiyun
- nuget config, v0.1.0 - ([fffc419](https://github.com/yiyungent/PluginCore/commit/fffc419480481b632340eb4e42a0b608c5fff144)) - yiyun
<!-- generated by git-cliff -->

View File

@ -0,0 +1,26 @@
using System;
namespace PluginCore.IPlugins
{
public class Constants
{
public const string AspNetCoreAuthenticationClaimType = "PluginCore.Admin.Token";
public const string AspNetCoreAuthenticationScheme = "PluginCore.Admin.Authentication";
public const string AspNetCoreAuthorizationPolicyName = "PluginCore.Admin";
public const string AspNetCoreAuthorizationTokenCookieName = "PluginCore.Admin.Token";
/// <summary>
/// zh
/// en
/// </summary>
public const string AspNetCoreLanguageCookieName = "language";
/// <summary>
/// httpContext.Items[Constants.AspNetCoreLanguageKey]
/// </summary>
public const string AspNetCoreLanguageKey = "PluginCore.Admin.Language";
}
}

View File

@ -0,0 +1,57 @@
//===================================================
// License: GNU LGPLv3
// Contributors: yiyungent@gmail.com
// Project: https://yiyungent.github.io/PluginCore
// GitHub: https://github.com/yiyungent/PluginCore
//===================================================
using System;
using System.Collections.Generic;
using System.Text;
namespace PluginCore.IPlugins
{
public interface IPlugin
{
/// <summary>
/// 在启用插件之后: 这时插件Assemblies已被加载(插件上下文已load)
/// </summary>
/// <returns>
/// <para>当 IsSuccess 为 False, 主程序之后会回滚插件状态: (1)unload插件上下文 (2)更新plugin.config.json,标记为禁用状态</para>
/// </returns>
(bool IsSuccess, string Message) AfterEnable();
/// <summary>
/// 在禁用插件之前: 这时插件Assemblies还未被释放(插件上下文未Unload)
/// </summary>
/// <returns>
/// <para>只有当 IsSuccess 为 True, 主程序之后才会释放插件上下文, 并标记为已禁用</para>
/// <para>当 IsSuccess 为 False, 主程序不会释放插件上下文,也不会标记为禁用, 插件维持原状态</para>
/// </returns>
(bool IsSuccess, string Message) BeforeDisable();
/// <summary>
/// TODO: 更新未做
/// </summary>
/// <param name="currentVersion"></param>
/// <param name="targetVersion"></param>
/// <returns></returns>
(bool IsSuccess, string Message) Update(string currentVersion, string targetVersion);
/// <summary>
/// 主应用程序启动时
/// </summary>
void AppStart();
/// <summary>
/// 启动顺序: 此插件 所依赖的前置插件
/// </summary>
/// <value></value>
List<string> AppStartOrderDependPlugins { get; }
}
}

View File

@ -0,0 +1,31 @@
//===================================================
// License: GNU LGPLv3
// Contributors: yiyungent@gmail.com
// Project: https://yiyungent.github.io/PluginCore
// GitHub: https://github.com/yiyungent/PluginCore
//===================================================
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace PluginCore.IPlugins
{
/// <summary>
/// 定时任务
/// </summary>
public interface ITimeJobPlugin : IPlugin
{
/// <summary>
/// 间隔秒数
/// </summary>
long SecondsPeriod { get; }
Task ExecuteAsync();
}
}

View File

@ -0,0 +1,26 @@
//===================================================
// License: GNU LGPLv3
// Contributors: yiyungent@gmail.com
// Project: https://yiyungent.github.io/PluginCore
// GitHub: https://github.com/yiyungent/PluginCore
//===================================================
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace PluginCore.IPlugins
{
public interface IWidgetPlugin : IPlugin
{
// 这种方式限定: 不合适, 一个插件, 可能需要注入多个地方
//string WidgetKey { get; }
Task<string> Widget(string widgetKey, params string[] extraPars);
}
}

View File

@ -0,0 +1,216 @@
//===================================================
// License: GNU LGPLv3
// Contributors: yiyungent@gmail.com
// Project: https://yiyungent.github.io/PluginCore
// GitHub: https://github.com/yiyungent/PluginCore
//===================================================
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
namespace PluginCore
{
public class PluginPathProvider
{
static PluginPathProvider()
{
// 初始化插件目录
string appDataDir = Path.Combine(Directory.GetCurrentDirectory(), "App_Data");
if (!Directory.Exists(appDataDir))
{
Directory.CreateDirectory(appDataDir);
}
string pluginConfigJsonFilePath = Path.Combine(Directory.GetCurrentDirectory(), "App_Data", "plugin.config.json");
string pluginConfigJson = "{\"EnabledPlugins\":[],\"DisabledPlugins\":[],\"UninstalledPlugins\":[]}";
if (!File.Exists(pluginConfigJsonFilePath))
{
File.WriteAllText(pluginConfigJsonFilePath, pluginConfigJson, System.Text.Encoding.UTF8);
}
string tempPluginUploadDir = TempPluginUploadDir();
if (!Directory.Exists(tempPluginUploadDir))
{
Directory.CreateDirectory(tempPluginUploadDir);
}
string pluginsDir = PluginsRootPath();
if (!Directory.Exists(pluginsDir))
{
Directory.CreateDirectory(pluginsDir);
}
string pluginsWwwRootDir = PluginsWwwRootDir();
if (!Directory.Exists(pluginsWwwRootDir))
{
Directory.CreateDirectory(pluginsWwwRootDir);
}
string pluginCoreAdminDir = PluginCoreAdminDir();
if (!Directory.Exists(pluginCoreAdminDir))
{
Directory.CreateDirectory(pluginCoreAdminDir);
}
}
/// <summary>
/// 临时插件上传目录路径
/// eg: F:\Com\me\Repos\Remember.Core\src\Presentation\WebApi\App_Data\TempPluginUpload
/// </summary>
/// <returns></returns>
public static string TempPluginUploadDir()
{
string tempPluginUploadDir = Path.Combine(Directory.GetCurrentDirectory(), "App_Data", "TempPluginUpload");
return tempPluginUploadDir;
}
/// <summary>
/// 获取 Plugins 的路径
/// </summary>
/// <returns></returns>
public static string PluginsRootPath()
{
string pluginRootPath = Path.Combine(Directory.GetCurrentDirectory(), "Plugins");
return pluginRootPath;
}
/// <summary>
/// 获取目标插件文件夹名
/// </summary>
/// <param name="pluginDir">目标插件完整目录路径</param>
/// <returns></returns>
public static string GetPluginFolderNameByDir(string pluginDir)
{
string pluginRootPath = PluginsRootPath();
string pluginFolderName = pluginDir.Replace(pluginRootPath + Path.DirectorySeparatorChar, "");
return pluginFolderName;
}
/// <summary>
/// 所有插件的完整目录路径
/// </summary>
/// <returns></returns>
public static IList<string> AllPluginDir()
{
string pluginRootPath = PluginsRootPath();
string[] pluginDirs = Directory.GetDirectories(pluginRootPath, "*");
return pluginDirs;
}
/// <summary>
/// 所有插件的文件夹名
/// </summary>
/// <returns></returns>
public static IList<string> AllPluginFolderName()
{
IList<string> pluginFolderNames = new List<string>();
IList<string> pluginDirs = AllPluginDir();
foreach (var dir in pluginDirs)
{
string pluginFolderName = GetPluginFolderNameByDir(dir);
pluginFolderNames.Add(pluginFolderName);
}
return pluginFolderNames;
}
/// <summary>
/// Plugins/{pluginId}/wwwroot
/// </summary>
/// <returns></returns>
public static string WwwRootDir(string pluginId)
{
string wwwrootDir = Path.Combine(PluginsRootPath(), pluginId, "wwwroot");
return wwwrootDir;
}
/// <summary>
/// Plugins/{currentPluginId}/wwwroot
/// </summary>
/// <returns></returns>
//public static string CurrentWwwRootDir()
//{
// string wwwrootDir = Path.Combine(PluginsRootPath(), CurrentPluginId(), "wwwroot");
// return wwwrootDir;
//}
/// <summary>
/// Plugins_wwwroot
/// </summary>
/// <returns></returns>
public static string PluginsWwwRootDir()
{
string pluginsWwwRootDir = Path.Combine(Directory.GetCurrentDirectory(), "Plugins_wwwroot");
return pluginsWwwRootDir;
}
/// <summary>
/// Plugins_wwwroot/pluginId
/// </summary>
/// <param name="pluginId"></param>
/// <returns></returns>
public static string PluginWwwRootDir(string pluginId)
{
string pluginWwwRootDir = Path.Combine(PluginsWwwRootDir(), pluginId);
return pluginWwwRootDir;
}
/// <summary>
/// Plugins_wwwroot/currentPluginId
/// </summary>
/// <returns></returns>
//public static string CurrentPluginWwwRootDir()
//{
// string pluginId = CurrentPluginId();
// string pluginWwwRootDir = PluginWwwRootDir(pluginId);
// return pluginWwwRootDir;
//}
public static string PluginCoreAdminDir()
{
string pluginCoreAdminDir = Path.Combine(Directory.GetCurrentDirectory(), "PluginCoreAdmin");
return pluginCoreAdminDir;
}
/// <summary>
/// 规范:
/// PluginId = 插件程序集名 (PluginId.dll)
///
/// TODO: 无法获取当前 PluginId
/// </summary>
/// <returns></returns>
//public static string CurrentPluginId()
//{
// //var ass = Assembly.GetExecutingAssembly(); // 错误: PluginCore.IPlugins
// //var ass = Assembly.GetCallingAssembly(); // 错误: PluginCore.IPlugins
// //var ass = Assembly.GetEntryAssembly(); // 错误: AspNetCore3_1
// string pluginId = ass.GetName().Name;
// return pluginId;
//}
}
}

View File

@ -0,0 +1,165 @@
//===================================================
// License: GNU LGPLv3
// Contributors: yiyungent@gmail.com
// Project: https://yiyungent.github.io/PluginCore
// GitHub: https://github.com/yiyungent/PluginCore
//===================================================
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using PluginCore.Models;
namespace PluginCore
{
public class PluginSettingsModelFactory
{
// TODO: Linux 文件名下区分大小写, windows不区分, 目前必须为 README.md
private const string SettingsFile = "settings.json";
#region
public static T Create<T>
(string pluginId)
where T : PluginSettingsModel
{
PluginSettingsModel rtnModel = new PluginSettingsModel();
string pluginDir = Path.Combine(PluginPathProvider.PluginsRootPath(), pluginId);
string pluginSettingsFilePath = Path.Combine(pluginDir, SettingsFile);
if (!File.Exists(pluginSettingsFilePath))
{
return null;
}
try
{
string settingsStr = File.ReadAllText(pluginSettingsFilePath, Encoding.UTF8);
rtnModel = System.Text.Json.JsonSerializer.Deserialize<T>(settingsStr);
}
catch
{
rtnModel = null;
}
return rtnModel as T;
}
public static string Create
(string pluginId)
{
string rtnStr = string.Empty;
string pluginDir = Path.Combine(PluginPathProvider.PluginsRootPath(), pluginId);
string pluginSettingsFilePath = Path.Combine(pluginDir, SettingsFile);
if (!File.Exists(pluginSettingsFilePath))
{
return null;
}
try
{
rtnStr = File.ReadAllText(pluginSettingsFilePath, Encoding.UTF8);
}
catch
{
rtnStr = null;
}
return rtnStr;
}
#endregion
#region
public static void Save<T>(T pluginSettingsModel, string pluginId)
where T : PluginSettingsModel
{
if (pluginSettingsModel == null)
{
throw new ArgumentNullException(nameof(pluginSettingsModel));
}
try
{
string pluginSettingsJsonStr = System.Text.Json.JsonSerializer.Serialize<T>(pluginSettingsModel);
string pluginSettingsFilePath = Path.Combine(PluginPathProvider.PluginsRootPath(), pluginId, SettingsFile);
//File.WriteAllText(pluginSettingsFilePath, pluginSettingsJsonStr, Encoding.UTF8);
// 写的时候加缩进
File.WriteAllText(pluginSettingsFilePath, ConvertJsonString(pluginSettingsJsonStr), Encoding.UTF8);
}
catch
{ }
}
public static void Save(string pluginSettingsJsonStr, string pluginId)
{
if (pluginSettingsJsonStr == null)
{
throw new ArgumentNullException(nameof(pluginSettingsJsonStr));
}
try
{
string pluginSettingsFilePath = Path.Combine(PluginPathProvider.PluginsRootPath(), pluginId, SettingsFile);
//File.WriteAllText(pluginSettingsFilePath, pluginSettingsJsonStr, Encoding.UTF8);
// 写的时候加缩进
File.WriteAllText(pluginSettingsFilePath, ConvertJsonString(pluginSettingsJsonStr), Encoding.UTF8);
}
catch
{ }
}
#endregion
#region JSON字符串
private static string ConvertJsonString(string str)
{
//格式化json字符串
#region 使Newtonsoft.Json格式化 JSON字符串
//Newtonsoft.Json.JsonSerializer serializer = new Newtonsoft.Json.JsonSerializer();
//TextReader tr = new StringReader(str);
//Newtonsoft.Json.JsonTextReader jtr = new Newtonsoft.Json.JsonTextReader(tr);
//object obj = serializer.Deserialize(jtr);
//if (obj != null)
//{
// StringWriter textWriter = new StringWriter();
// Newtonsoft.Json.JsonTextWriter jsonWriter = new Newtonsoft.Json.JsonTextWriter(textWriter)
// {
// Formatting = Newtonsoft.Json.Formatting.Indented,
// Indentation = 4,
// IndentChar = ' '
// };
// serializer.Serialize(jsonWriter, obj);
// return textWriter.ToString();
//}
//else
//{
// return str;
//}
#endregion
#region 使 System.Text.Json JSON字符串
// https://blog.csdn.net/essity/article/details/84644510
System.Text.Json.JsonSerializerOptions options = new System.Text.Json.JsonSerializerOptions();
// 设置支持中文的unicode编码: 这样就不会自动转码,而是原样展现
options.Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping;
// 启用缩进设置
options.WriteIndented = true;
// 注意: object 不会丢失json数据, 但不能使用 dynamic, 会报编译错误
object jsonObj = System.Text.Json.JsonSerializer.Deserialize<object>(str);
// Error CS0656 Missing compiler required member 'Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo.Create'
// dynamic jsonObj = System.Text.Json.JsonSerializer.Deserialize<dynamic>(str);
string rtnStr = System.Text.Json.JsonSerializer.Serialize(jsonObj, options);
return rtnStr;
#endregion
}
#endregion
}
}

View File

@ -0,0 +1,79 @@
//===================================================
// License: GNU LGPLv3
// Contributors: yiyungent@gmail.com
// Project: https://yiyungent.github.io/PluginCore
// GitHub: https://github.com/yiyungent/PluginCore
//===================================================
using System;
using System.Collections.Generic;
using System.Text;
using PluginCore.IPlugins;
namespace PluginCore.Interfaces
{
public interface IPluginFinder
{
#region
/// <summary>
/// 实现了指定接口或类型 的启用插件
/// </summary>
/// <typeparam name="TPlugin">可以是一个接口,一个抽象类,一个普通实现类, 只要实现了 <see cref="IPlugin"/>即可</typeparam>
/// <returns></returns>
IEnumerable<TPlugin> EnablePlugins<TPlugin>()
where TPlugin : IPlugin; // BasePlugin
/// <summary>
/// 实现了指定接口或类型 的启用插件
/// </summary>
/// <typeparam name="TPlugin">可以是一个接口,一个抽象类,一个普通实现类, 只要实现了 <see cref="IPlugin"/>即可</typeparam>
/// <returns></returns>
IEnumerable<(TPlugin PluginInstance, string PluginId)> EnablePluginsFull<TPlugin>()
where TPlugin : IPlugin; // BasePlugin
/// <summary>
/// 所有启用的插件 的 PluginId
/// </summary>
/// <returns></returns>
IEnumerable<string> EnablePluginIds<TPlugin>()
where TPlugin : IPlugin; // BasePlugin
#endregion
#region
/// <summary>
/// 所有启用的插件
/// </summary>
/// <returns></returns>
IEnumerable<IPlugin> EnablePlugins();
/// <summary>
/// 所有启用的插件
/// </summary>
/// <returns></returns>
IEnumerable<(IPlugin PluginInstance, string PluginId)> EnablePluginsFull();
/// <summary>
/// 所有启用的插件 的 PluginId
/// </summary>
/// <returns></returns>
IEnumerable<string> EnablePluginIds();
#endregion
#region pluginId
/// <summary>
/// 获取指定 pluginId 的启用插件
/// </summary>
/// <param name="pluginId"></param>
/// <returns>1.插件未启用返回null, 2.找不到此插件上下文返回null 3.找不到插件主dll返回null 4.插件主dll中找不到实现了IPlugin的Type返回null, 5.无法实例化插件返回null</returns>
IPlugin Plugin(string pluginId);
#endregion
}
}

View File

@ -0,0 +1,30 @@
//===================================================
// License: GNU LGPLv3
// Contributors: yiyungent@gmail.com
// Project: https://yiyungent.github.io/PluginCore
// GitHub: https://github.com/yiyungent/PluginCore
//===================================================
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
namespace PluginCore.Interfaces
{
public interface IPluginManager
{
void LoadPlugin(string pluginId);
void UnloadPlugin(string pluginId);
/// <summary>
/// 加载插件程序集
/// </summary>
/// <param name="pluginId"></param>
Assembly GetPluginAssembly(string pluginId);
}
}

View File

@ -0,0 +1,27 @@
//===================================================
// License: GNU LGPLv3
// Contributors: yiyungent@gmail.com
// Project: https://yiyungent.github.io/PluginCore
// GitHub: https://github.com/yiyungent/PluginCore
//===================================================
using System;
using System.Collections.Generic;
using System.Text;
namespace PluginCore.Models
{
/// <summary>
/// 插件设置模型
/// <para>对应插件目录下 settings.json</para>
/// <para>插件开发者自己的插件设置模型 必须继承该类</para>
/// </summary>
public class PluginSettingsModel
{
}
}

View File

@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
<PackageId>PluginCore.IPlugins</PackageId>
<Version>0.9.1</Version>
<Company>yiyun</Company>
<Authors>yiyun</Authors>
<Description>PluginCore 插件开发包</Description>
<Copyright>Copyright (c) 2021-present yiyun</Copyright>
<RepositoryUrl>https://github.com/yiyungent/PluginCore</RepositoryUrl>
<PackageLicenseUrl>https://github.com/yiyungent/PluginCore/blob/main/LICENSE</PackageLicenseUrl>
<PackageTags>PluginCore PluginCore.IPlugin</PackageTags>
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
</PropertyGroup>
<!-- 生成注释xml -->
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|netstandard2.0|AnyCPU'">
<DocumentationFile>bin\Release\netstandard2.0\PluginCore.IPlugins.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<!--<PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />-->
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" />
<PackageReference Include="System.Text.Json" Version="5.0.2" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,82 @@
# git-cliff ~ configuration file
# https://git-cliff.org/docs/configuration
[changelog]
# template for the changelog header
header = """
# Changelog\n
All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines.\n
"""
# template for the changelog body
# https://keats.github.io/tera/docs/#introduction
body = """
---
{% if version %}\
{% if previous.version %}\
## [{{ version | trim_start_matches(pat="v") }}]($REPO/compare/{{ previous.version }}..{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
{% endif %}\
{% else %}\
## [unreleased]
{% endif %}\
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | striptags | trim | upper_first }}
{% for commit in commits
| filter(attribute="scope")
| sort(attribute="scope") %}
- **({{commit.scope}})**{% if commit.breaking %} [**breaking**]{% endif %} \
{{ commit.message }} - ([{{ commit.id | truncate(length=7, end="") }}]($REPO/commit/{{ commit.id }})) - {{ commit.author.name }}
{%- endfor -%}
{% raw %}\n{% endraw %}\
{%- for commit in commits %}
{%- if commit.scope -%}
{% else -%}
- {% if commit.breaking %} [**breaking**]{% endif %}\
{{ commit.message }} - ([{{ commit.id | truncate(length=7, end="") }}]($REPO/commit/{{ commit.id }})) - {{ commit.author.name }}
{% endif -%}
{% endfor -%}
{% endfor %}\n
"""
# template for the changelog footer
footer = """
<!-- generated by git-cliff -->
"""
# remove the leading and trailing whitespace from the templates
trim = true
# postprocessors
postprocessors = [
{ pattern = '\$REPO', replace = "https://github.com/yiyungent/PluginCore" }, # replace repository URL
]
[git]
# parse the commits based on https://www.conventionalcommits.org
conventional_commits = true
# filter out the commits that are not conventional
filter_unconventional = true
# process each line of a commit as an individual commit
split_commits = false
# regex for preprocessing the commit messages
commit_preprocessors = [
# { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/orhun/git-cliff/issues/${2}))"}, # replace issue numbers
]
# regex for parsing and grouping commits
commit_parsers = [
{ message = "^feat", group = "Features" },
{ message = "^fix", group = "Bug Fixes" },
{ message = "^doc", group = "Documentation" },
{ message = "^perf", group = "Performance" },
{ message = "^refactor", group = "Refactoring" },
{ message = "^style", group = "Style" },
{ message = "^revert", group = "Revert" },
{ message = "^test", group = "Tests" },
{ message = "^chore\\(version\\):", skip = true },
{ message = "^chore", group = "Miscellaneous Chores" },
{ body = ".*security", group = "Security" },
]
# filter out the commits that are not matched by commit parsers
filter_commits = false
# sort the tags topologically
topo_order = false
# sort the commits inside sections by oldest/newest order
sort_commits = "oldest"

View File

@ -0,0 +1 @@
dotnet pack -c Release

Some files were not shown because too many files have changed in this diff Show More