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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ Server-side JavaScript that runs on the Power Pages runtime, callable from both
| Sample | Integrates | Use this sample to learn |
| --- | --- | --- |
| [SharePoint Integration](samples/server-logic/sharepoint-integration/) | Microsoft Graph + SharePoint | Call Microsoft Graph and SharePoint Online from server logic using an Entra client-credentials app. Pairs with the [File Upload (SharePoint)](samples/spa/snippets/file-upload/sharepoint/) code-site sample. |
| [Unbound Custom API Invocation](samples/server-logic/unbound-custom-api/) | Dataverse Custom APIs | Invoke an unbound Custom Action with `POST` and a parameterized unbound Custom Function with `GET` from server logic using a ready-to-import solution. |

## Sample categories

Expand Down
1 change: 1 addition & 0 deletions samples/server-logic/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ and lets a site reach services that the portal Web API can't.
| Sample | Integrates | Use this sample to learn |
| --- | --- | --- |
| [SharePoint Integration](sharepoint-integration/) | Microsoft Graph + SharePoint | Call Microsoft Graph and SharePoint Online from server logic using an Entra client-credentials app. Pairs with the [File Upload (SharePoint)](../spa/snippets/file-upload/sharepoint/) code-site sample. |
| [Unbound Custom API Invocation](unbound-custom-api/) | Dataverse Custom APIs | Invoke an unbound Custom Action with `POST` and a parameterized unbound Custom Function with `GET` from server logic using a ready-to-import solution. |

See the repository [CONTRIBUTING guide](../../CONTRIBUTING.md) for how the sample
categories are organized and where a new sample belongs.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
243 changes: 243 additions & 0 deletions samples/server-logic/unbound-custom-api/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
# Invoke unbound Dataverse Custom APIs from server logic

This sample shows how Power Pages server logic can invoke both forms of an
unbound Dataverse Custom API:

- A **Custom Action** by using `POST` with request parameters.
- A **Custom Function** by using `GET` with OData parameter aliases and no
request body.

The importable unmanaged solution includes an enhanced-data-model Power Pages
site, its ready-to-run home page, server logic, two Custom APIs, their
parameters and response properties, and a compiled, signed Dataverse plug-in.
No code compilation, page editing, or plug-in registration is required after
import.

![Sample home page showing successful Custom Action and parameterized Custom Function results](./screenshot.png)

## Solution contents

| Component | Value |
| --- | --- |
| Solution | `ServerLogicUnboundCustomApiSample_1_0_0_2.zip` |
| Site | `Custom API Support in Server Logic` |
| Site type | Enhanced-data-model site (not a code site) |
| Home page | Automatically invokes the server logic endpoint and renders both results |
| Server logic endpoint | `sl-unbound-customapi-manual-test` |
| Custom Action | `new_ServerLogicUnboundActionManualTest` |
| Custom Function | `new_ServerLogicUnboundFunctionManualTest` |
| Action inputs | `InputText` (String), `InputNumber` (Integer) |
| Function inputs | `InputText` (String), `InputNumber` (Integer) |
| Action and function outputs | `ResponseText` (String), `ResponseNumber` (Integer) |
| Access | Anonymous Users and Authenticated Users web roles |

## Prerequisites

- A Power Platform environment with Power Pages provisioned.
- Permission to import solutions and reactivate a Power Pages site.
- Power Pages runtime version `9.8.9.xx` or later for unbound Custom Function
calls through `InvokeCustomApi`.
- The standard Power Pages managed solutions. The package declares these
dependencies:
- `PowerPages_CoreBase` version `1.1.2605.1` or later.
- `Basic` version `1.0`.

## Import and activate the sample

1. Download
[`ServerLogicUnboundCustomApiSample_1_0_0_2.zip`](./ServerLogicUnboundCustomApiSample_1_0_0_2.zip).
2. In [Power Apps](https://make.powerapps.com), open the target environment,
select **Solutions**, and import the ZIP file.
3. Open [Power Pages](https://make.powerpages.microsoft.com) in the same
environment. Locate the imported **Custom API Support in Server Logic**
site and reactivate it.
4. Choose an available site address when prompted.
5. Publish all customizations and wait for the site to finish provisioning.

Site reactivation is required after the first import because a Power Pages site
address cannot be provisioned by a portable solution package. The package
intentionally omits a primary domain so importing a newer sample version does
not replace the address of an already activated site.

## Run the sample

Browse to the activated site's home page:

```text
https://<your-site-domain>/
```

The page automatically calls:

```text
GET /_api/serverlogics/sl-unbound-customapi-manual-test
```

It displays the action and function request values, HTTP statuses, response
values, pass/fail badges, and the reference code used by the sample. You can
also browse directly to the endpoint URL to inspect its raw JSON response.

The endpoint returns a wrapper whose `data` property is a JSON string. A
successful response has this shape:

```json
{
"success": true,
"serverLogicName": "sl-unbound-customapi-manual-test",
"data": "{\"overallPass\":true,...}"
}
```

After parsing `data`, both calls report the expected values:

```json
{
"overallPass": true,
"action": {
"method": "POST",
"response": {
"statusCode": 200,
"body": {
"ResponseNumber": 42
}
},
"passed": true
},
"function": {
"method": "GET",
"request": {
"InputText": "Manual GET parameter test / aliases",
"InputNumber": 61
},
"response": {
"statusCode": 200,
"body": {
"ResponseNumber": 68
}
},
"passed": true
}
}
```

## How it works

The dedicated home Web Template renders the page content and embeds the
client-side script, so the sample does not depend on JavaScript hooks from a
starter site's Header or Footer. The script uses a normal same-origin request
and parses the Server Logic response:

```javascript
const response = await fetch(
"/_api/serverlogics/sl-unbound-customapi-manual-test",
{
method: "GET",
credentials: "same-origin",
headers: { Accept: "application/json" }
}
);
const outer = await response.json();
const result = JSON.parse(outer.data);
```

The server logic sends a body with the unbound action:

```javascript
var actionRequest = {
InputText: "Manual test from Power Pages",
InputNumber: 35
};

var actionResponse = Server.Connector.Dataverse.InvokeCustomApi(
"POST",
"new_ServerLogicUnboundActionManualTest",
JSON.stringify(actionRequest)
);
```

The unbound function uses `GET` and puts its input values in OData parameter
aliases. The binding list stays in the operation path, while the encoded values
stay in the query string:

```javascript
function encodeODataStringAlias(value) {
return encodeURIComponent("'" + value.replace(/'/g, "''") + "'");
}

var functionRequest = {
InputText: "Manual GET parameter test / aliases",
InputNumber: 61
};
var functionUrl =
"new_ServerLogicUnboundFunctionManualTest(InputText=@text,InputNumber=@number)"
+ "?@text=" + encodeODataStringAlias(functionRequest.InputText)
+ "&@number=" + functionRequest.InputNumber;

var functionResponse = Server.Connector.Dataverse.InvokeCustomApi(
"GET",
functionUrl
);
```

The plug-in echoes each input text and adds `7` to `InputNumber`. The action
therefore returns `42`, while the function returns `68`. The server logic parses
the connector response and sets `overallPass` only when both calls return the
expected values.

## Imported Custom API configuration

The action is global (`Binding Type = Global`) and is not a function:

![Unbound Custom Action configuration](./media/unbound-custom-action-configuration.png)

The function is also global and has `Is Function = Yes`:

![Unbound Custom Function configuration](./media/unbound-custom-function-configuration.png)

Its two inputs are configured as Custom API request parameters:

![Unbound Custom Function request parameters](./media/unbound-custom-function-request-parameters.png)

## Source files

- [`source/homepage-content.html`](./source/homepage-content.html) contains the
page markup and styles included in the solution.
- [`source/homepage.js`](./source/homepage.js) invokes the Server Logic endpoint
and renders the action and function results. The dedicated home Web Template
in the solution embeds this script after the editable page content.
- [`source/header.html`](./source/header.html) and
[`source/footer.html`](./source/footer.html) keep the imported site
self-contained and free of starter-template snippet dependencies.
- [`source/sl-unbound-customapi-manual-test.sl`](./source/sl-unbound-customapi-manual-test.sl)
contains the server logic included in the solution.
- [`source/EchoPlugin.cs`](./source/EchoPlugin.cs) contains the equivalent
Dataverse plug-in source. The solution already contains the compiled, signed
assembly; the source is included for learning and review.

## Security note

This demonstration endpoint is assigned to both anonymous and authenticated
web roles because it returns only deterministic sample values. For production
code, grant only the minimum required web roles, validate every input, avoid
returning sensitive Dataverse data, and review the privileges granted to the
portal application user.

## Troubleshooting

- **404 from the server logic URL:** confirm that the imported site is active,
the endpoint name is unchanged, and customizations are published.
- **The home page stays on "Running":** publish all customizations and confirm
that the imported Home Web Template is still assigned to the Home Page
Template.
- **The browser sends two requests to the server logic endpoint:** import
version `1.0.0.2` or later, publish all customizations, and clear the site's
configuration cache. This version removes legacy page JavaScript and also
prevents duplicate initialization.
- **Custom API not found:** confirm the two Custom APIs and the plug-in assembly
were imported successfully, then publish all customizations.
- **The action succeeds but the function reports HTTP 400:** the target
environment has not yet received Power Pages runtime version `9.8.9.xx` or
later. Parameterized unbound Custom Functions through `InvokeCustomApi` are
available starting with `9.8.9.xx`.
- **Managed dependency error during import:** provision Power Pages in the
target environment so the required Power Pages core solutions are installed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
45 changes: 45 additions & 0 deletions samples/server-logic/unbound-custom-api/source/EchoPlugin.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using System;
using System.Reflection;
using Microsoft.Xrm.Sdk;

[assembly: AssemblyTitle("PowerPages.ServerLogic.UnboundCustomApiManualTest")]
[assembly: AssemblyDescription("Dataverse plug-in for the Power Pages Server Logic Custom API sample.")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

namespace PowerPages.ServerLogic.UnboundCustomApiManualTest
{
/// <summary>
/// Returns values that show whether the Custom API received the request.
/// </summary>
public sealed class EchoPlugin : IPlugin
{
/// <summary>
/// Executes the sample Custom API.
/// </summary>
/// <param name="serviceProvider">Provides Dataverse execution services.</param>
public void Execute(IServiceProvider serviceProvider)
{
var context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
var tracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService));
var inputText = context.InputParameters.Contains("InputText")
? context.InputParameters["InputText"] as string
: string.Empty;
var inputNumber = context.InputParameters.Contains("InputNumber")
&& context.InputParameters["InputNumber"] is int
? (int)context.InputParameters["InputNumber"]
: 0;

context.OutputParameters["ResponseText"] = string.IsNullOrEmpty(inputText)
? string.Format("{0} completed successfully.", context.MessageName)
: string.Format("{0} received: {1}", context.MessageName, inputText);
context.OutputParameters["ResponseNumber"] = inputNumber + 7;

tracingService.Trace(
"Sample Custom API completed. Message={0}, InputNumber={1}, ResponseNumber={2}.",
context.MessageName,
inputNumber,
inputNumber + 7);
}
}
}
5 changes: 5 additions & 0 deletions samples/server-logic/unbound-custom-api/source/footer.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<footer role="contentinfo" style="border-top:1px solid #d1d1d1;color:#616161;">
<div style="margin:0 auto;max-width:1440px;padding:24px 20px;">
Power Pages Server Logic Custom API sample
</div>
</footer>
8 changes: 8 additions & 0 deletions samples/server-logic/unbound-custom-api/source/header.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<header role="banner" style="background:#0f6cbd;color:#fff;">
<div style="align-items:center;display:flex;flex-wrap:wrap;gap:12px;justify-content:space-between;margin:0 auto;max-width:1440px;padding:18px 20px;">
<a href="/" aria-label="Custom API Support in Server Logic home" style="color:#fff;font-size:1.25rem;font-weight:700;text-decoration:none;">
Custom API Support in Server Logic
</a>
<span style="font-size:0.95rem;">Power Pages Server Logic sample</span>
</div>
</header>
Loading
Loading