diff --git a/XrmPluginCore.Tests/CustomApis/CustomApiConfigBuilderTests.cs b/XrmPluginCore.Tests/CustomApis/CustomApiConfigBuilderTests.cs index e017d8a..45d7b4e 100644 --- a/XrmPluginCore.Tests/CustomApis/CustomApiConfigBuilderTests.cs +++ b/XrmPluginCore.Tests/CustomApis/CustomApiConfigBuilderTests.cs @@ -103,20 +103,21 @@ public void WithExecutePrivilegeName_CalledMultipleTimes_UsesLastValue() } [Theory] - [InlineData(Privilege.Create, "prvCreateaccount")] - [InlineData(Privilege.Read, "prvReadaccount")] - [InlineData(Privilege.Write, "prvWriteaccount")] - [InlineData(Privilege.Delete, "prvDeleteaccount")] - [InlineData(Privilege.Append, "prvAppendaccount")] - [InlineData(Privilege.AppendTo, "prvAppendToaccount")] - [InlineData(Privilege.Assign, "prvAssignaccount")] - [InlineData(Privilege.Share, "prvShareaccount")] - public void WithExecutePrivilege_Generic_ResolvesPrivilegeNameFromEntity(Privilege privilege, string expected) + [InlineData(Privilege.Create, "prvCreateAccount")] + [InlineData(Privilege.Read, "prvReadAccount")] + [InlineData(Privilege.Write, "prvWriteAccount")] + [InlineData(Privilege.Delete, "prvDeleteAccount")] + [InlineData(Privilege.Append, "prvAppendAccount")] + [InlineData(Privilege.AppendTo, "prvAppendToAccount")] + [InlineData(Privilege.Assign, "prvAssignAccount")] + [InlineData(Privilege.Share, "prvShareAccount")] + public void WithExecutePrivilege_Generic_ResolvesPrivilegeNameFromSchemaName(Privilege privilege, string expected) { // Arrange var builder = new CustomApiConfigBuilder("test_api"); // Act + // Privilege names use the schema name, which the early-bound type name (typeof(Account).Name = "Account") provides. var config = builder.WithExecutePrivilege(privilege).Build(); // Assert @@ -137,16 +138,16 @@ public void WithExecutePrivilege_Generic_ReturnsBuilderForChaining() } [Fact] - public void WithExecutePrivilege_WithLogicalName_ResolvesPrivilegeName() + public void WithExecutePrivilege_WithSchemaName_ResolvesPrivilegeName() { // Arrange var builder = new CustomApiConfigBuilder("test_api"); // Act - var config = builder.WithExecutePrivilege("new_widget", Privilege.Write).Build(); + var config = builder.WithExecutePrivilege("new_Widget", Privilege.Write).Build(); // Assert - config.ExecutePrivilegeName.Should().Be("prvWritenew_widget"); + config.ExecutePrivilegeName.Should().Be("prvWritenew_Widget"); } [Fact] diff --git a/XrmPluginCore/CHANGELOG.md b/XrmPluginCore/CHANGELOG.md index 9ea3147..ab27c9a 100644 --- a/XrmPluginCore/CHANGELOG.md +++ b/XrmPluginCore/CHANGELOG.md @@ -1,5 +1,5 @@ -### v1.4.0 - 01 July 2026 -* Add: `WithExecutePrivilege(Privilege)` on the Custom API builder for type-safe execute-privilege configuration, e.g. `.WithExecutePrivilege(Privilege.Read)` resolves to `prvReadaccount`. A `WithExecutePrivilege(string entityLogicalName, Privilege)` overload is available for late-bound scenarios. The existing `WithExecutePrivilegeName(string)` remains for non-standard privilege names. +### v1.4.1 - 01 July 2026 +* Add: `WithExecutePrivilege(Privilege)` on the Custom API builder for type-safe execute-privilege configuration, e.g. `.WithExecutePrivilege(Privilege.Read)` resolves to `prvReadAccount`. Privilege names use the entity schema name, taken from the early-bound type name (`typeof(T).Name`). A `WithExecutePrivilege(string entitySchemaName, Privilege)` overload is available for late-bound scenarios, and the existing `WithExecutePrivilegeName(string)` remains for non-standard privilege names. * Add: Type-safe Custom API request/response wrappers. `RegisterAPI(name, handlerMethodName)` now generates `{ApiName}Request`/`{ApiName}Response` classes (named after the API, in the plugin's namespace) from the `AddRequestParameter`/`AddResponseProperty` declarations. The handler accepts the request and returns the response; a generated `ActionWrapper` marshals `InputParameters` into the request and the returned response into `OutputParameters`. When no request parameters are declared the handler takes no argument, and when no response properties are declared it returns `void`. * Add: Error XPC4004: Custom API handler method not found (with code fix to create the method). * Add: Warning XPC4005 / Error XPC4006: Custom API handler signature does not match the declared request parameters and response properties (with code fix to correct the signature). diff --git a/XrmPluginCore/CustomApis/CustomApiConfigBuilder.cs b/XrmPluginCore/CustomApis/CustomApiConfigBuilder.cs index 4bef6ec..3a558c7 100644 --- a/XrmPluginCore/CustomApis/CustomApiConfigBuilder.cs +++ b/XrmPluginCore/CustomApis/CustomApiConfigBuilder.cs @@ -121,26 +121,29 @@ public CustomApiConfigBuilder WithExecutePrivilegeName(string privilegeName) /// /// Sets the execute privilege to a standard table privilege of , - /// e.g. WithExecutePrivilege<Account>(Privilege.Read) resolves to prvReadaccount. + /// e.g. WithExecutePrivilege<Account>(Privilege.Read) resolves to prvReadAccount. + /// The entity schema name is taken from the early-bound type name (typeof(T).Name), which + /// early-bound generators produce from the entity schema name. /// /// The early-bound entity type the privilege applies to. /// The table privilege required to execute the custom API. - public CustomApiConfigBuilder WithExecutePrivilege(Privilege privilege) where T : Entity, new() + public CustomApiConfigBuilder WithExecutePrivilege(Privilege privilege) where T : Entity { - return WithExecutePrivilege(EntityLogicalNameCache.GetLogicalName(), privilege); + return WithExecutePrivilege(typeof(T).Name, privilege); } /// /// Sets the execute privilege to a standard table privilege of the entity identified by - /// , following the prv{Privilege}{EntityLogicalName} - /// convention. Use the generic overload when an - /// early-bound type is available. + /// , following the prv{Privilege}{EntitySchemaName} + /// convention. Privilege names use the schema name (e.g. Account), not the logical name + /// (account). Prefer the generic overload + /// when an early-bound type is available. /// - /// The logical name of the table the privilege applies to. + /// The schema name of the table the privilege applies to. /// The table privilege required to execute the custom API. - public CustomApiConfigBuilder WithExecutePrivilege(string entityLogicalName, Privilege privilege) + public CustomApiConfigBuilder WithExecutePrivilege(string entitySchemaName, Privilege privilege) { - Config.ExecutePrivilegeName = PrivilegeNameResolver.GetExecutePrivilegeName(privilege, entityLogicalName); + Config.ExecutePrivilegeName = PrivilegeNameResolver.GetExecutePrivilegeName(privilege, entitySchemaName); return this; } diff --git a/XrmPluginCore/Enums/Privilege.cs b/XrmPluginCore/Enums/Privilege.cs index 333652c..fbf488f 100644 --- a/XrmPluginCore/Enums/Privilege.cs +++ b/XrmPluginCore/Enums/Privilege.cs @@ -3,8 +3,8 @@ namespace XrmPluginCore.Enums /// /// Standard Dataverse table (entity) privileges.
/// Each value maps to a privilege whose name follows the convention - /// prv{Privilege}{EntityLogicalName}, e.g. on the - /// account table resolves to prvReadaccount. + /// prv{Privilege}{EntitySchemaName}, e.g. on the + /// Account table resolves to prvReadAccount. ///
public enum Privilege { diff --git a/XrmPluginCore/Helpers/PrivilegeNameResolver.cs b/XrmPluginCore/Helpers/PrivilegeNameResolver.cs index bd3a8d7..3e30baa 100644 --- a/XrmPluginCore/Helpers/PrivilegeNameResolver.cs +++ b/XrmPluginCore/Helpers/PrivilegeNameResolver.cs @@ -7,16 +7,17 @@ internal static class PrivilegeNameResolver { /// /// Builds the Dataverse execute-privilege name for the given on the - /// supplied entity, following the prv{Privilege}{EntityLogicalName} convention. - /// For example, on account resolves to prvReadaccount. + /// supplied entity, following the prv{Privilege}{EntitySchemaName} convention. + /// For example, on Account resolves to prvReadAccount. /// /// - /// Privilege names predate the schema name concept and are based on the entity logical name - /// (e.g. account), so the logical name is used verbatim. + /// Privilege names use the entity schema name (e.g. Account), not the logical name + /// (account): the platform stores the logical name as the lowercased schema name, so the + /// original casing must come from the schema name. Callers pass it via the early-bound type name. /// - public static string GetExecutePrivilegeName(Privilege privilege, string entityLogicalName) + public static string GetExecutePrivilegeName(Privilege privilege, string entitySchemaName) { - return $"prv{GetPrivilegeVerb(privilege)}{entityLogicalName}"; + return $"prv{GetPrivilegeVerb(privilege)}{entitySchemaName}"; } private static string GetPrivilegeVerb(Privilege privilege)