diff --git a/.ai/prompts/create-plugin.md b/.ai/prompts/create-plugin.md
index 886e9b043..c1632b860 100644
--- a/.ai/prompts/create-plugin.md
+++ b/.ai/prompts/create-plugin.md
@@ -41,7 +41,7 @@ Scaffold a new installable GrandNode plugin end to end, with the correct project
1. `SystemName` in `Manifest.cs` must equal the value in `{Feature}Defaults` and the output folder name.
2. `Group` must be one of the existing group names in `.ai/knowledge/plugin-types.md`.
-3. All GrandNode project references must be `Private="false"`.
+3. Import `src/Build/Grand.Plugin.props` for the shared host references; do not repeat them in the plugin. Anything referenced beyond that set must be `Private="false"`.
4. Use `Microsoft.NET.Sdk.Razor` when the plugin contains `.cshtml` files, `Microsoft.NET.Sdk` otherwise.
5. `Install()` saves default settings and adds localization resources, then calls `base.Install()` last.
6. `Uninstall()` deletes settings and removes localization resources, then calls `base.Uninstall()` last.
diff --git a/.ai/standards/dependencies.md b/.ai/standards/dependencies.md
index b03c9ab24..9736693f6 100644
--- a/.ai/standards/dependencies.md
+++ b/.ai/standards/dependencies.md
@@ -50,7 +50,8 @@ Do not override `TargetFramework` or `LangVersion` in an individual project.
## Project references
-- Reference GrandNode projects with `false` in plugins and modules — the host already loads those assemblies.
+- Plugins import `src/Build/Grand.Plugin.props`, which carries the host projects every plugin compiles against, each already `Private="false"`. Do not restate that list in a plugin — a new core project goes in the props file, so it reaches all plugins at once.
+- Reference GrandNode projects with `false` in modules, and in a plugin for anything beyond the shared set — the host already loads those assemblies. `Private="false"` does not carry to a reference's own dependencies, so each project has to be named.
- Use `all` (plugins referencing `Grand.Web` / `Grand.Web.Common`) or `runtime` (modules) following the nearest existing project of the same kind.
- Never reference a plugin from core, business, or web projects. Dependencies point inward only.
diff --git a/.ai/templates/plugin/base-plugin.md b/.ai/templates/plugin/base-plugin.md
index 75aa3dd42..317bd58cc 100644
--- a/.ai/templates/plugin/base-plugin.md
+++ b/.ai/templates/plugin/base-plugin.md
@@ -13,6 +13,7 @@ Placeholders: `{SystemName}` = `{Group}.{Name}`, `{Feature}` = type-name prefix.
```xml
+
true
false
@@ -28,31 +29,6 @@ Placeholders: `{SystemName}` = `{Group}.{Name}`, `{Feature}` = type-name prefix.
$(OutputPath)
-
-
- false
-
-
- false
-
-
- false
-
-
- false
-
-
- false
-
-
- false
-
-
- false
- all
-
-
-
Always
@@ -61,7 +37,11 @@ Placeholders: `{SystemName}` = `{Group}.{Name}`, `{Feature}` = type-name prefix.
```
-Use `Microsoft.NET.Sdk` and drop the Razor properties when the plugin ships no `.cshtml`. Trim project references down to what the plugin actually uses.
+Use `Microsoft.NET.Sdk` and drop the Razor properties when the plugin ships no `.cshtml`.
+
+`Grand.Plugin.props` brings in the host projects every plugin compiles against — `Grand.SharedKernel`, `Grand.Domain`, `Grand.Data`, `Grand.Mapping`, `Grand.Mediator`, `Grand.Infrastructure`, `Grand.Business.Core` and `Grand.Web.Common` — each with `Private=false` so nothing is copied into the plugin folder. Do not restate them in the plugin: they were duplicated across all 16 plugins until `Grand.Mediator` was added to the solution, added to none of them, and copied into every plugin's output. Add a new core project to `src/Build/Grand.Plugin.props`, not here.
+
+The unused references cost nothing at runtime, so there is no need to trim the list. A plugin needing something beyond that set — a NuGet package, or `Grand.Web` as `Theme.Modern` does — declares it in its own `ItemGroup`.
## 2. `Manifest.cs`
diff --git a/src/Build/Grand.Plugin.props b/src/Build/Grand.Plugin.props
new file mode 100644
index 000000000..d0ff5d048
--- /dev/null
+++ b/src/Build/Grand.Plugin.props
@@ -0,0 +1,50 @@
+
+
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Core/Grand.Infrastructure/Plugins/PluginInfoAttribute.cs b/src/Core/Grand.Infrastructure/Plugins/PluginInfoAttribute.cs
index 4511ed7a3..aba23d772 100644
--- a/src/Core/Grand.Infrastructure/Plugins/PluginInfoAttribute.cs
+++ b/src/Core/Grand.Infrastructure/Plugins/PluginInfoAttribute.cs
@@ -1,22 +1,18 @@
-using System.Reflection;
-
-namespace Grand.Infrastructure.Plugins;
+namespace Grand.Infrastructure.Plugins;
[AttributeUsage(AttributeTargets.Assembly)]
public class PluginInfoAttribute : Attribute
{
- public PluginInfoAttribute()
- {
- var assembly = Assembly.GetExecutingAssembly();
- var fullVersion = assembly.GetName().Version;
- SupportedVersion = $"{fullVersion?.Major}.{fullVersion?.Minor}";
- }
-
public string Group { get; set; } = string.Empty;
public string FriendlyName { get; set; } = string.Empty;
public string SystemName { get; set; } = string.Empty;
public string Author { get; set; } = string.Empty;
+ ///
+ /// The GrandNode version this plugin supports, as "Major.Minor". Left unset by convention -
+ /// it is then resolved from the assembly's Grand.Infrastructure reference by
+ /// .
+ ///
public string SupportedVersion { get; set; }
public string Version { get; set; }
diff --git a/src/Core/Grand.Infrastructure/Plugins/PluginManager.cs b/src/Core/Grand.Infrastructure/Plugins/PluginManager.cs
index 278806195..6265f94d9 100644
--- a/src/Core/Grand.Infrastructure/Plugins/PluginManager.cs
+++ b/src/Core/Grand.Infrastructure/Plugins/PluginManager.cs
@@ -254,7 +254,7 @@ private static PluginInfo PreparePluginInfo(FileInfo pluginFile)
Group = pluginInfo.Group,
SystemName = pluginInfo.SystemName,
Version = pluginInfo.Version,
- SupportedVersion = pluginInfo.SupportedVersion,
+ SupportedVersion = PluginVersionResolver.ResolveSupportedVersion(assembly, pluginInfo.SupportedVersion),
Author = pluginInfo.Author,
PluginFileName = plug.Name,
OriginalAssemblyFile = pluginFile,
diff --git a/src/Core/Grand.Infrastructure/Plugins/PluginVersionResolver.cs b/src/Core/Grand.Infrastructure/Plugins/PluginVersionResolver.cs
new file mode 100644
index 000000000..c525b0285
--- /dev/null
+++ b/src/Core/Grand.Infrastructure/Plugins/PluginVersionResolver.cs
@@ -0,0 +1,35 @@
+using System.Reflection;
+
+namespace Grand.Infrastructure.Plugins;
+
+///
+/// Resolves which GrandNode version a plugin assembly was built against.
+///
+public static class PluginVersionResolver
+{
+ private const string CoreAssemblyName = "Grand.Infrastructure";
+
+ ///
+ /// Returns the GrandNode version the plugin supports, as "Major.Minor".
+ ///
+ /// The plugin assembly the info attribute was read from
+ /// The version declared on , if any
+ ///
+ /// The declared version when the plugin states one, otherwise the version of the
+ /// reference it was compiled against. Null when neither is available -
+ /// such a plugin cannot be matched against and is
+ /// therefore treated as incompatible.
+ ///
+ public static string ResolveSupportedVersion(Assembly pluginAssembly, string declaredVersion)
+ {
+ if (!string.IsNullOrWhiteSpace(declaredVersion))
+ return declaredVersion.Trim();
+
+ var coreReference = pluginAssembly?.GetReferencedAssemblies()
+ .FirstOrDefault(x => string.Equals(x.Name, CoreAssemblyName, StringComparison.OrdinalIgnoreCase));
+
+ return coreReference?.Version == null
+ ? null
+ : $"{coreReference.Version.Major}.{coreReference.Version.Minor}";
+ }
+}
diff --git a/src/Modules/Grand.Module.Api/Grand.Module.Api.csproj b/src/Modules/Grand.Module.Api/Grand.Module.Api.csproj
index 5bc94f45a..6abfa6983 100644
--- a/src/Modules/Grand.Module.Api/Grand.Module.Api.csproj
+++ b/src/Modules/Grand.Module.Api/Grand.Module.Api.csproj
@@ -16,6 +16,10 @@
False
runtime
+
+ False
+ runtime
+
False
runtime
diff --git a/src/Modules/Grand.Module.Installer/Grand.Module.Installer.csproj b/src/Modules/Grand.Module.Installer/Grand.Module.Installer.csproj
index c23d05e9e..58b290707 100644
--- a/src/Modules/Grand.Module.Installer/Grand.Module.Installer.csproj
+++ b/src/Modules/Grand.Module.Installer/Grand.Module.Installer.csproj
@@ -24,6 +24,9 @@
False
+
+ False
+
diff --git a/src/Modules/Grand.Module.Migration/Grand.Module.Migration.csproj b/src/Modules/Grand.Module.Migration/Grand.Module.Migration.csproj
index be1148574..380b06646 100644
--- a/src/Modules/Grand.Module.Migration/Grand.Module.Migration.csproj
+++ b/src/Modules/Grand.Module.Migration/Grand.Module.Migration.csproj
@@ -20,6 +20,9 @@
False
+
+ False
+
diff --git a/src/Modules/Grand.Module.ScheduledTasks/Grand.Module.ScheduledTasks.csproj b/src/Modules/Grand.Module.ScheduledTasks/Grand.Module.ScheduledTasks.csproj
index 6eae10918..5018356d4 100644
--- a/src/Modules/Grand.Module.ScheduledTasks/Grand.Module.ScheduledTasks.csproj
+++ b/src/Modules/Grand.Module.ScheduledTasks/Grand.Module.ScheduledTasks.csproj
@@ -13,6 +13,10 @@
False
runtime
+
+ False
+ runtime
+
False
runtime
diff --git a/src/Plugins/Authentication.Facebook/Authentication.Facebook.csproj b/src/Plugins/Authentication.Facebook/Authentication.Facebook.csproj
index d58e2f3ec..12a2a36e8 100644
--- a/src/Plugins/Authentication.Facebook/Authentication.Facebook.csproj
+++ b/src/Plugins/Authentication.Facebook/Authentication.Facebook.csproj
@@ -1,5 +1,6 @@
+
true
false
@@ -19,37 +20,7 @@
-
-
-
-
-
-
-
-
- false
-
-
- false
-
-
- false
-
-
- false
-
-
- false
-
-
- false
- all
-
-
- false
-
-
diff --git a/src/Plugins/Authentication.Google/Authentication.Google.csproj b/src/Plugins/Authentication.Google/Authentication.Google.csproj
index cf6d6ec07..072a9ef93 100644
--- a/src/Plugins/Authentication.Google/Authentication.Google.csproj
+++ b/src/Plugins/Authentication.Google/Authentication.Google.csproj
@@ -1,5 +1,6 @@
+
true
false
@@ -18,37 +19,7 @@
-
-
-
-
-
-
-
-
- false
-
-
- false
-
-
- false
-
-
- false
-
-
- false
-
-
- false
- all
-
-
- false
-
-
diff --git a/src/Plugins/DiscountRules.Standard/DiscountRules.Standard.csproj b/src/Plugins/DiscountRules.Standard/DiscountRules.Standard.csproj
index d99106c54..fd8214640 100644
--- a/src/Plugins/DiscountRules.Standard/DiscountRules.Standard.csproj
+++ b/src/Plugins/DiscountRules.Standard/DiscountRules.Standard.csproj
@@ -1,5 +1,6 @@
+
true
false
@@ -15,30 +16,6 @@
$(OutputPath)
-
-
- false
-
-
- false
-
-
- false
-
-
- false
-
-
- false
-
-
- false
- all
-
-
- false
-
-
Always
diff --git a/src/Plugins/ExchangeRate.McExchange/ExchangeRate.McExchange.csproj b/src/Plugins/ExchangeRate.McExchange/ExchangeRate.McExchange.csproj
index b7a9486b8..23e8be1ca 100644
--- a/src/Plugins/ExchangeRate.McExchange/ExchangeRate.McExchange.csproj
+++ b/src/Plugins/ExchangeRate.McExchange/ExchangeRate.McExchange.csproj
@@ -1,5 +1,6 @@
+
..\..\Web\Grand.Web\Plugins\ExchangeRate.McExchange\
@@ -11,30 +12,6 @@
$(OutputPath)
-
-
- false
-
-
- false
-
-
- false
-
-
- false
-
-
- false
-
-
- false
-
-
- false
- all
-
-
diff --git a/src/Plugins/Payments.BrainTree/Payments.BrainTree.csproj b/src/Plugins/Payments.BrainTree/Payments.BrainTree.csproj
index 96795784c..2b8789b69 100644
--- a/src/Plugins/Payments.BrainTree/Payments.BrainTree.csproj
+++ b/src/Plugins/Payments.BrainTree/Payments.BrainTree.csproj
@@ -1,5 +1,6 @@
+
true
false
@@ -20,38 +21,7 @@
-
-
-
-
-
-
-
-
-
- false
-
-
- false
-
-
- false
-
-
- false
-
-
- false
-
-
- false
- all
-
-
- false
-
-
diff --git a/src/Plugins/Payments.CashOnDelivery/Payments.CashOnDelivery.csproj b/src/Plugins/Payments.CashOnDelivery/Payments.CashOnDelivery.csproj
index 4476dc110..fdc6f6863 100644
--- a/src/Plugins/Payments.CashOnDelivery/Payments.CashOnDelivery.csproj
+++ b/src/Plugins/Payments.CashOnDelivery/Payments.CashOnDelivery.csproj
@@ -1,5 +1,6 @@
+
true
false
@@ -15,30 +16,6 @@
$(OutputPath)
-
-
- false
-
-
- false
-
-
- false
-
-
- false
-
-
- false
-
-
- false
- all
-
-
- false
-
-
diff --git a/src/Plugins/Payments.StripeCheckout/Payments.StripeCheckout.csproj b/src/Plugins/Payments.StripeCheckout/Payments.StripeCheckout.csproj
index cd4f0e44b..533e643d1 100644
--- a/src/Plugins/Payments.StripeCheckout/Payments.StripeCheckout.csproj
+++ b/src/Plugins/Payments.StripeCheckout/Payments.StripeCheckout.csproj
@@ -1,5 +1,6 @@
+
true
false
@@ -19,38 +20,7 @@
-
-
-
-
-
-
-
-
-
- false
-
-
- false
-
-
- false
-
-
- false
-
-
- false
-
-
- false
-
-
- false
- all
-
-
diff --git a/src/Plugins/Shipping.ByWeight/Shipping.ByWeight.csproj b/src/Plugins/Shipping.ByWeight/Shipping.ByWeight.csproj
index e6e6277bc..7f1f31cc8 100644
--- a/src/Plugins/Shipping.ByWeight/Shipping.ByWeight.csproj
+++ b/src/Plugins/Shipping.ByWeight/Shipping.ByWeight.csproj
@@ -1,5 +1,6 @@
+
true
false
@@ -15,30 +16,6 @@
$(OutputPath)
-
-
- false
-
-
- false
-
-
- false
-
-
- false
-
-
- false
-
-
- false
-
-
- false
- all
-
-
diff --git a/src/Plugins/Shipping.FixedRateShipping/Shipping.FixedRateShipping.csproj b/src/Plugins/Shipping.FixedRateShipping/Shipping.FixedRateShipping.csproj
index e0eb36d3b..b6586751e 100644
--- a/src/Plugins/Shipping.FixedRateShipping/Shipping.FixedRateShipping.csproj
+++ b/src/Plugins/Shipping.FixedRateShipping/Shipping.FixedRateShipping.csproj
@@ -1,5 +1,6 @@
+
true
false
@@ -15,30 +16,6 @@
$(OutputPath)
-
-
- false
-
-
- false
-
-
- false
-
-
- false
-
-
- false
-
-
- false
-
-
- false
- all
-
-
diff --git a/src/Plugins/Shipping.ShippingPoint/Shipping.ShippingPoint.csproj b/src/Plugins/Shipping.ShippingPoint/Shipping.ShippingPoint.csproj
index 164635d1d..142598e9b 100644
--- a/src/Plugins/Shipping.ShippingPoint/Shipping.ShippingPoint.csproj
+++ b/src/Plugins/Shipping.ShippingPoint/Shipping.ShippingPoint.csproj
@@ -1,5 +1,6 @@
+
true
false
@@ -15,30 +16,6 @@
$(OutputPath)
-
-
- false
-
-
- false
-
-
- false
-
-
- false
-
-
- false
-
-
- false
-
-
- false
- all
-
-
diff --git a/src/Plugins/Tax.CountryStateZip/Tax.CountryStateZip.csproj b/src/Plugins/Tax.CountryStateZip/Tax.CountryStateZip.csproj
index 1bcf0371c..e4944e7d8 100644
--- a/src/Plugins/Tax.CountryStateZip/Tax.CountryStateZip.csproj
+++ b/src/Plugins/Tax.CountryStateZip/Tax.CountryStateZip.csproj
@@ -1,5 +1,6 @@
+
true
false
@@ -15,30 +16,6 @@
$(OutputPath)
-
-
- false
-
-
- false
-
-
- false
-
-
- false
-
-
- false
-
-
- false
-
-
- false
- all
-
-
diff --git a/src/Plugins/Tax.FixedRate/Tax.FixedRate.csproj b/src/Plugins/Tax.FixedRate/Tax.FixedRate.csproj
index 253057629..2316dea02 100644
--- a/src/Plugins/Tax.FixedRate/Tax.FixedRate.csproj
+++ b/src/Plugins/Tax.FixedRate/Tax.FixedRate.csproj
@@ -1,5 +1,6 @@
+
true
false
@@ -15,30 +16,6 @@
$(OutputPath)
-
-
- false
-
-
- false
-
-
- false
-
-
- false
-
-
- false
-
-
- false
-
-
- false
- all
-
-
diff --git a/src/Plugins/Theme.Modern/Theme.Modern.csproj b/src/Plugins/Theme.Modern/Theme.Modern.csproj
index 4763b7070..043c5e1e0 100644
--- a/src/Plugins/Theme.Modern/Theme.Modern.csproj
+++ b/src/Plugins/Theme.Modern/Theme.Modern.csproj
@@ -1,5 +1,6 @@
+
true
false
@@ -16,28 +17,6 @@
-
- false
-
-
- false
-
-
- false
-
-
- false
-
-
- false
-
-
- false
-
-
- false
- all
-
false
all
diff --git a/src/Plugins/Widgets.FacebookPixel/Widgets.FacebookPixel.csproj b/src/Plugins/Widgets.FacebookPixel/Widgets.FacebookPixel.csproj
index ba5b7120b..ac023b1c0 100644
--- a/src/Plugins/Widgets.FacebookPixel/Widgets.FacebookPixel.csproj
+++ b/src/Plugins/Widgets.FacebookPixel/Widgets.FacebookPixel.csproj
@@ -1,5 +1,6 @@
+
true
false
@@ -15,30 +16,6 @@
$(OutputPath)
-
-
- false
-
-
- false
-
-
- false
-
-
- false
-
-
- false
-
-
- false
-
-
- false
- all
-
-
diff --git a/src/Plugins/Widgets.GoogleAnalytics/Widgets.GoogleAnalytics.csproj b/src/Plugins/Widgets.GoogleAnalytics/Widgets.GoogleAnalytics.csproj
index f7c53f599..8fc0f29eb 100644
--- a/src/Plugins/Widgets.GoogleAnalytics/Widgets.GoogleAnalytics.csproj
+++ b/src/Plugins/Widgets.GoogleAnalytics/Widgets.GoogleAnalytics.csproj
@@ -1,5 +1,6 @@
+
true
false
@@ -15,30 +16,6 @@
$(OutputPath)
-
-
- false
-
-
- false
-
-
- false
-
-
- false
-
-
- false
-
-
- false
-
-
- false
- all
-
-
diff --git a/src/Plugins/Widgets.Slider/Widgets.Slider.csproj b/src/Plugins/Widgets.Slider/Widgets.Slider.csproj
index c416b24c4..e9b6bdb28 100644
--- a/src/Plugins/Widgets.Slider/Widgets.Slider.csproj
+++ b/src/Plugins/Widgets.Slider/Widgets.Slider.csproj
@@ -1,5 +1,6 @@
+
true
false
@@ -15,30 +16,6 @@
$(OutputPath)
-
-
- false
-
-
- false
-
-
- false
-
-
- false
-
-
- false
-
-
- false
-
-
- false
- all
-
-
PreserveNewest
diff --git a/src/Tests/Grand.Infrastructure.Tests/Plugins/PluginVersionResolverTests.cs b/src/Tests/Grand.Infrastructure.Tests/Plugins/PluginVersionResolverTests.cs
new file mode 100644
index 000000000..bcf6a85c9
--- /dev/null
+++ b/src/Tests/Grand.Infrastructure.Tests/Plugins/PluginVersionResolverTests.cs
@@ -0,0 +1,79 @@
+using Grand.Infrastructure.Plugins;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using System.Reflection;
+
+namespace Grand.Infrastructure.Tests.Plugins;
+
+[TestClass]
+public class PluginVersionResolverTests
+{
+ ///
+ /// Stands in for a plugin assembly: it is compiled against the same Grand.Infrastructure
+ /// as any real plugin, so its reference version is the one a plugin would carry.
+ ///
+ private static readonly Assembly PluginLikeAssembly = typeof(PluginVersionResolverTests).Assembly;
+
+ [TestMethod]
+ public void ResolveSupportedVersion_AssemblyBuiltAgainstCurrentCore_MatchesSupportedPluginVersion()
+ {
+ var result = PluginVersionResolver.ResolveSupportedVersion(PluginLikeAssembly, null);
+
+ Assert.AreEqual(GrandVersion.SupportedPluginVersion, result);
+ }
+
+ [TestMethod]
+ public void ResolveSupportedVersion_DeclaredVersion_WinsOverAssemblyReference()
+ {
+ var result = PluginVersionResolver.ResolveSupportedVersion(PluginLikeAssembly, "1.0");
+
+ Assert.AreEqual("1.0", result);
+ Assert.AreNotEqual(GrandVersion.SupportedPluginVersion, result,
+ "A plugin declaring an old version must stay distinguishable from the current one - " +
+ "this is the comparison the compatibility gate relies on.");
+ }
+
+ [TestMethod]
+ public void ResolveSupportedVersion_DeclaredVersionIsWhitespace_FallsBackToAssemblyReference()
+ {
+ var result = PluginVersionResolver.ResolveSupportedVersion(PluginLikeAssembly, " ");
+
+ Assert.AreEqual(GrandVersion.SupportedPluginVersion, result);
+ }
+
+ [TestMethod]
+ public void ResolveSupportedVersion_DeclaredVersionIsPadded_IsTrimmed()
+ {
+ var result = PluginVersionResolver.ResolveSupportedVersion(PluginLikeAssembly, " 2.3 ");
+
+ Assert.AreEqual("2.3", result);
+ }
+
+ [TestMethod]
+ public void ResolveSupportedVersion_AssemblyWithoutCoreReference_ReturnsNull()
+ {
+ //System.Private.CoreLib does not reference Grand.Infrastructure
+ var result = PluginVersionResolver.ResolveSupportedVersion(typeof(object).Assembly, null);
+
+ Assert.IsNull(result,
+ "A plugin that cannot be tied to a core version must not be reported as compatible.");
+ }
+
+ [TestMethod]
+ public void ResolveSupportedVersion_NoAssembly_ReturnsNull()
+ {
+ var result = PluginVersionResolver.ResolveSupportedVersion(null, null);
+
+ Assert.IsNull(result);
+ }
+
+ [TestMethod]
+ public void PluginInfoAttribute_DoesNotSelfAssignSupportedVersion()
+ {
+ //The attribute used to compute SupportedVersion in its constructor from
+ //Assembly.GetExecutingAssembly() - always Grand.Infrastructure, never the plugin -
+ //which made every plugin compare equal to GrandVersion.SupportedPluginVersion.
+ var attribute = new PluginInfoAttribute();
+
+ Assert.IsNull(attribute.SupportedVersion);
+ }
+}