A Power Apps Code App that talks to Microsoft Dataverse through the Microsoft Dataverse
connector (shared_commondataserviceforapps) instead of native Dataverse.
The UI is intentionally identical to the native Dataverse sample — same components, same styling, same three tabs. Only the data layer differs. The goal is to show that a line-of-business app built on native Dataverse can also be built on the Dataverse connector, and to document exactly where the two approaches diverge.
Important
This sample is a connector counterpart to samples/Dataverse. If you're starting fresh and don't specifically need the connector, prefer native Dataverse — it is first‑class, strongly typed, and has fewer limitations (see Connector vs. native Dataverse).
- Contacts (CRUD) — Create, Read, Update, Delete on the
contacttable, with a lookup that links a contact to a parent account via OData bind syntax. - Accounts (File & Image Attachments) — Upload and download files and images on Dataverse file/image columns.
- Functions & Actions — Three live connector capabilities: an OData query, a FetchXML query,
and a metadata
GetEntitieslisting. - Connector-only data layer — Every call goes through the connector's auto‑generated
MicrosoftDataverseService; no native Dataverse services are used.
There are two ways to reach Dataverse from a Code App, and they produce different generated code:
| Native Dataverse (samples/Dataverse) | Dataverse connector (this sample) | |
|---|---|---|
| Add command | pa app add data-source --connector dataverse --table contact |
pa app add data-source --connector shared_commondataserviceforapps --connection-id <connectionId> --table contact --dataset <orgUrl> |
| Needs a connection? | No | Yes |
| Generated code | One typed service per table (ContactsService, AccountsService, …) + typed models |
A single, untyped MicrosoftDataverseService + MicrosoftDataverseModel |
| Typing | Strongly typed rows | Untyped OData rows (Record<string, unknown>) |
| Methods | getAll / get / create / update / delete |
ListRecords, GetItem, CreateRecord, UpdateRecord, DeleteRecord, PerformUnboundAction, GetEntities, UpdateEntityFileImageFieldContent, … |
| Org targeting | Implicit | Each op has a …WithOrganization variant that takes the Dataverse org URL |
Because the connector service is untyped and requires the org URL and explicit headers on every
call, this sample wraps it in a small helper — src/dataverse/client.ts —
that exposes simple listRows / getRow / createRow / updateRow / deleteRow (plus file and metadata
helpers). Everything above that wrapper (hooks, components) mirrors the native sample.
- Node.js LTS (Download)
- Power Apps CLI for code apps — the
pacommand from@microsoft/power-apps-cli - A Power Platform environment with Dataverse and Code Apps enabled
- A Microsoft Dataverse connection in that environment
npm installpa app init --display-name "Dataverse Connector Demo" --environment-id <your-environment-id># List connections — copy the Connection ID for the "Microsoft Dataverse" (shared_commondataserviceforapps) connection
pa connection listThe connector needs your environment's Dataverse org URL (e.g.
https://orgXXXXXXXX.crm.dynamics.com/) as the -d dataset value. You can find it in the Power
Platform admin center under the environment's details, or read it at runtime from
getContext().app.dataverseOrgUrl (which is what the app itself uses).
pa app add data-source --connector shared_commondataserviceforapps --connection-id <connectionId> --table account --dataset "https://orgXXXXXXXX.crm.dynamics.com/"
pa app add data-source --connector shared_commondataserviceforapps --connection-id <connectionId> --table contact --dataset "https://orgXXXXXXXX.crm.dynamics.com/"Each add-data-source call generates the connector's typed service and models under
src/generated/ (and a .power/ schema folder), with the operations available for the connector.
Note
src/generated/, .power/, and power.config.json are not committed to this repo — they
are environment-specific and generated by pa app init / pa app add data-source. After
cloning, run the steps above to create them for your own environment. Everything the app is built
from (the src/dataverse/client.ts wrapper, hooks, and components) is committed and works
once the data sources are added.
The Accounts tab uploads and downloads to a File column and an Image column on the
account table. Add both in the maker portal (Tables → Account → Columns → New column), then set
each column's logical name in src/types.ts:
// src/types.ts — set these to your columns' logical names
export const ACCOUNT_FILE_COLUMN = 'crdac_filecol' as const // your File column
export const ACCOUNT_IMAGE_COLUMN = 'crdac_imagecol' as const // your Image columnThe values above are examples from the environment this sample was built in — replace them with the logical names the maker portal shows for your columns. For the image full-size download to work, enable "Can store full-size image" on the Image column (see Full-size images).
pa app runOpen the Local Play URL in the same browser profile as your Power Platform tenant.
npm run build
pa app pushsrc/
├── dataverse/
│ └── client.ts # The ONLY data layer — wraps the connector's MicrosoftDataverseService
├── types.ts # Hand-authored Accounts/Contacts types (connector rows are untyped)
├── generated/ # Auto-generated by `pa app add data-source` (not committed)
│ ├── services/MicrosoftDataverseService.ts
│ └── models/MicrosoftDataverseModel.ts
├── hooks/ # Business logic, built on src/dataverse/client
│ ├── useContacts.ts # Contact CRUD + form state
│ ├── useAccountsCrud.ts # Account CRUD for the Accounts tab
│ ├── useAccounts.ts # Read-only accounts for the lookup dropdown
│ └── useLookupResolver.ts # Resolves lookup GUIDs to display names on demand
├── components/ # Presentation only — same look as the native sample
│ ├── ContactList / ContactCard / ContactForm
│ ├── AccountList / AccountCard / AccountForm # AccountForm includes the file/image attachment sub-form
│ ├── ApiActionsPanel.tsx # Functions & Actions tab (OData query, FetchXML, GetEntities)
│ ├── Header / Footer / ErrorMessage
│ └── index.ts
├── App.tsx # Composition + tab navigation
└── App.css # Styling (identical to the native sample)
Components (UI) → Hooks (state + orchestration) → src/dataverse/client → MicrosoftDataverseService → Dataverse
The wrapper handles three things the connector requires but the native service does not:
- Org URL — resolved once from
getContext().app.dataverseOrgUrland passed to every…WithOrganizationoperation. (Plain operations without the org fail with "Invalid organization URL 'null'".) - Headers — reads request
odata.include-annotations="*"(so lookup display names come back as formatted-value annotations) andapplication/json. - Result envelope — connector operations return
{ success, data, error }; the wrapper unwraps it and surfaces a readable error message.
// Read — list rows with OData options
await listRows('contacts', { select: ['contactid', 'firstname', 'lastname'], orderBy: ['createdon desc'], top: 50 });
// Create — plain object; lookups use OData bind syntax
await createRow('contacts', {
firstname: 'Ada',
lastname: 'Lovelace',
'parentcustomerid_account@odata.bind': `/accounts(${accountId})`,
});
// Update / Delete
await updateRow('contacts', id, { jobtitle: 'Engineer' });
await deleteRow('contacts', id);// Column logical names come from src/types.ts (ACCOUNT_FILE_COLUMN / ACCOUNT_IMAGE_COLUMN)
// Upload (files and images use the same operation)
await uploadFileImage('accounts', id, ACCOUNT_FILE_COLUMN, file);
// Download a file — connector returns raw bytes (Uint8Array) + fileName on the envelope
const { blob, fileName } = await downloadFile('accounts', id, ACCOUNT_FILE_COLUMN);
// Download an image — full-size via the file-content endpoint, thumbnail inline from the record
const base64 = await downloadFileImage('accounts', id, ACCOUNT_IMAGE_COLUMN, /* fullSize */ true);// OData / FetchXML queries via ListRecords
await listRows('accounts', { filter: "contains(name, 'a')", top: 5 });
await listRows('contacts', { fetchXml: '<fetch top="5">…</fetch>' });
// List all tables (metadata)
await listEntities(); // MicrosoftDataverseService.GetEntitiesBuilding this connector twin of the native sample surfaced several real differences. They're worth knowing before choosing the connector for a production app.
| Scenario | Native Dataverse | Dataverse connector |
|---|---|---|
| CRUD | ✅ typed services | ✅ untyped, via MicrosoftDataverseService |
| Lookups (read/write) | ✅ | ✅ (@odata.bind write, formatted-value annotations on read) |
| File column upload/download | ✅ | ✅ |
| Image column upload/download | ✅ | ✅ (full-size needs a column with full-size storage enabled) |
| File column delete | ✅ (DELETE /entity(id)/column) |
❌ Not possible — see below |
| Image column delete | ✅ | ✅ (null‑PATCH clears image columns) |
| Dataverse actions (POST) | ✅ | ✅ via PerformUnboundAction / PerformBoundAction |
Dataverse functions (GET, e.g. WhoAmI) |
✅ (add-dataverse-api) |
❌ no GET path — see below |
Relevance Search (GetRelevantRows) |
n/a | ❌ generated without an org binding → can't target the current env |
| FetchXML queries | – (typed getAll exposes $select/$filter/$orderby/$top only) |
✅ via ListRecords fetchXml — see below |
A frequently requested capability: the connector's ListRecords accepts a fetchXml parameter,
so you can run advanced FetchXML — aggregations, link-entity joins, grouping — directly from a Code
App. The native sample's typed getAll only surfaces the common OData options ($select, $filter,
$orderby, $top), so FetchXML is a genuine reason to reach for the connector.
// src/dataverse/client.ts wraps ListRecords; pass a fetchXml string:
await listRows('contacts', {
fetchXml: `
<fetch top="5">
<entity name="contact">
<attribute name="fullname" />
<attribute name="emailaddress1" />
<order attribute="createdon" descending="true" />
</entity>
</fetch>`,
});Try it live on the Functions & Actions tab → FetchXML Query card.
The connector exposes only PUT (upload) and GET (download) for file/image fields — there is
no DELETE operation. And Dataverse deliberately ignores null in a PATCH for File columns
(the record only holds a reference to blob storage, not the bytes). So:
- Image columns (e.g.
entityimage, or a custom Image column) store the value inline and do clear when PATCHed to null — image delete works. - File columns cannot be cleared through the connector at all. Native Dataverse can, because its
generated service issues a real
DELETE /account(id)/columnrequest.
The app keeps the file × button and shows an honest message explaining the limitation rather than a fake success.
Dataverse has two kinds of operations: actions (POST, may have side effects) and functions
(GET, read-only). The connector invokes actions through PerformUnboundAction (unbound) and
PerformBoundAction (bound to a record) — both POST. It does not expose a GET path, so calling a
read-only function like WhoAmI as an action returns:
404 "No HTTP resource was found that matches the request URI '.../api/data/v9.1.0/WhoAmI'"
Some function-style data is still available through dedicated connector operations (for example
GetEntities, GetOrganizations, GetCatalogs) rather than the generic action endpoint.
The built-in entityimage column stores only a ~144px thumbnail, so a "full size" download returns
the same small image. To demo full resolution, add a custom Image column with "Can store
full-size image" enabled. Full-size images are then served as raw bytes by the file-content
endpoint (GET /entity(id)/column/$value?size=full); the thumbnail is read inline from the record.
- Connection & DLP — the connector runs under a user connection, so Data Loss Prevention policies apply. If a policy blocks the Dataverse connector's write actions, create/update/delete return HTTP 442 "Request blocked due to DLP policies" even though reads succeed.
- No schema refresh — if a table's schema changes, delete and re‑add the data source to regenerate the service.
- samples/Dataverse — the native Dataverse version of this app
- Power Apps code apps overview
- Connect your code app to data
- Microsoft Dataverse connector reference