Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 13 additions & 12 deletions XrmPluginCore.Tests/CustomApis/CustomApiConfigBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Account>(privilege).Build();

// Assert
Expand All @@ -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]
Expand Down
4 changes: 2 additions & 2 deletions XrmPluginCore/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
### v1.4.0 - 01 July 2026
* Add: `WithExecutePrivilege<T>(Privilege)` on the Custom API builder for type-safe execute-privilege configuration, e.g. `.WithExecutePrivilege<Account>(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<T>(Privilege)` on the Custom API builder for type-safe execute-privilege configuration, e.g. `.WithExecutePrivilege<Account>(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<TService>(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).
Expand Down
21 changes: 12 additions & 9 deletions XrmPluginCore/CustomApis/CustomApiConfigBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -121,26 +121,29 @@ public CustomApiConfigBuilder WithExecutePrivilegeName(string privilegeName)

/// <summary>
/// Sets the execute privilege to a standard table privilege of <typeparamref name="T"/>,
/// e.g. <c>WithExecutePrivilege&lt;Account&gt;(Privilege.Read)</c> resolves to <c>prvReadaccount</c>.
/// e.g. <c>WithExecutePrivilege&lt;Account&gt;(Privilege.Read)</c> resolves to <c>prvReadAccount</c>.
/// The entity schema name is taken from the early-bound type name (<c>typeof(T).Name</c>), which
/// early-bound generators produce from the entity schema name.
/// </summary>
/// <typeparam name="T">The early-bound entity type the privilege applies to.</typeparam>
/// <param name="privilege">The table privilege required to execute the custom API.</param>
public CustomApiConfigBuilder WithExecutePrivilege<T>(Privilege privilege) where T : Entity, new()
public CustomApiConfigBuilder WithExecutePrivilege<T>(Privilege privilege) where T : Entity
{
return WithExecutePrivilege(EntityLogicalNameCache.GetLogicalName<T>(), privilege);
return WithExecutePrivilege(typeof(T).Name, privilege);
}

/// <summary>
/// Sets the execute privilege to a standard table privilege of the entity identified by
/// <paramref name="entityLogicalName"/>, following the <c>prv{Privilege}{EntityLogicalName}</c>
/// convention. Use the generic <see cref="WithExecutePrivilege{T}(Privilege)"/> overload when an
/// early-bound type is available.
/// <paramref name="entitySchemaName"/>, following the <c>prv{Privilege}{EntitySchemaName}</c>
/// convention. Privilege names use the schema name (e.g. <c>Account</c>), not the logical name
/// (<c>account</c>). Prefer the generic <see cref="WithExecutePrivilege{T}(Privilege)"/> overload
/// when an early-bound type is available.
/// </summary>
/// <param name="entityLogicalName">The logical name of the table the privilege applies to.</param>
/// <param name="entitySchemaName">The schema name of the table the privilege applies to.</param>
/// <param name="privilege">The table privilege required to execute the custom API.</param>
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;
}

Expand Down
4 changes: 2 additions & 2 deletions XrmPluginCore/Enums/Privilege.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ namespace XrmPluginCore.Enums
/// <summary>
/// Standard Dataverse table (entity) privileges.<br/>
/// Each value maps to a privilege whose name follows the convention
/// <c>prv{Privilege}{EntityLogicalName}</c>, e.g. <see cref="Read"/> on the
/// <c>account</c> table resolves to <c>prvReadaccount</c>.
/// <c>prv{Privilege}{EntitySchemaName}</c>, e.g. <see cref="Read"/> on the
/// <c>Account</c> table resolves to <c>prvReadAccount</c>.
/// </summary>
public enum Privilege
{
Expand Down
13 changes: 7 additions & 6 deletions XrmPluginCore/Helpers/PrivilegeNameResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,17 @@ internal static class PrivilegeNameResolver
{
/// <summary>
/// Builds the Dataverse execute-privilege name for the given <paramref name="privilege"/> on the
/// supplied entity, following the <c>prv{Privilege}{EntityLogicalName}</c> convention.
/// For example, <see cref="Privilege.Read"/> on <c>account</c> resolves to <c>prvReadaccount</c>.
/// supplied entity, following the <c>prv{Privilege}{EntitySchemaName}</c> convention.
/// For example, <see cref="Privilege.Read"/> on <c>Account</c> resolves to <c>prvReadAccount</c>.
/// </summary>
/// <remarks>
/// Privilege names predate the schema name concept and are based on the entity logical name
/// (e.g. <c>account</c>), so the logical name is used verbatim.
/// Privilege names use the entity <em>schema</em> name (e.g. <c>Account</c>), not the logical name
/// (<c>account</c>): 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.
/// </remarks>
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)
Expand Down
Loading