Showing posts with label Power Apps. Show all posts
Showing posts with label Power Apps. Show all posts

Sunday, 22 February 2026

Build a secure MCP-style server for Power Apps with .NET 8 Azure Functions and Azure AD

Creating an MCP server in PowerApps typically means exposing a secure, typed HTTP API that Power Apps can call via a Custom Connector. This article shows why Azure Functions (.NET 8 isolated) with Azure AD (Entra ID) is the right foundation and how to ship a production-ready MCP-style server with OpenAPI, validation, and zero secret management.

The Problem

You need a reliable backend for Power Apps that enforces validation and security, offers a clean contract (OpenAPI), scales serverlessly, and avoids Function Keys or shared secrets. You also want workflows and data operations to be testable and observable.

Prerequisites

  • .NET 8 SDK
  • Azure CLI 2.58+ and Azure Functions Core Tools v4+
  • Azure subscription with permissions to create Resource Group, Function App, and Managed Identity
  • Power Apps environment with permission to create Custom Connectors

Implementation Details

Project setup

// Terminal commands (run locally)
// 1) Create a .NET 8 isolated Azure Functions app
dotnet new func --worker-runtime dotnet-isolated --name PowerAppsMcpServer

cd PowerAppsMcpServer

// 2) Add packages for validation, OpenAPI, and DI helpers
dotnet add package FluentValidation
dotnet add package FluentValidation.DependencyInjectionExtensions
dotnet add package Microsoft.Azure.Functions.Worker.Extensions.OpenApi
dotnet add package Azure.Identity
dotnet add package Microsoft.Extensions.Azure
dotnet add package Microsoft.Extensions.Logging.ApplicationInsights

Program.cs (minimal hosting, DI, OpenAPI)

using Azure.Core;
using Azure.Identity;
using FluentValidation;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Extensions.OpenApi.Extensions;
using Microsoft.Extensions.Azure;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

// Top-level statements for .NET 8 minimal hosting
var host = new HostBuilder()
    .ConfigureFunctionsWorkerDefaults(builder =>
    {
        // Enables OpenAPI endpoints at /api/swagger.json and /api/swagger/ui
        builder.AddApplicationInsights();
        builder.AddOpenApi();
    })
    .ConfigureServices(services =>
    {
        // Register Application Insights logger
        services.AddLogging();

        // Register validators
        services.AddValidatorsFromAssemblyContaining<CreateOrderRequestValidator>();

        // Register Azure SDK clients using DefaultAzureCredential (Managed Identity in Azure)
        services.AddAzureClients(clientBuilder =>
        {
            clientBuilder.UseCredential(new DefaultAzureCredential());
            // Example: clientBuilder.AddSecretClient(new Uri("https://<your-kv>.vault.azure.net/"));
        });

        // Register domain services
        services.AddSingleton<IOrderService, OrderService>();
    })
    .Build();

host.Run();

Contracts and validation

namespace PowerAppsMcpServer;

public sealed record CreateOrderRequest(
    string CustomerId,      // Must be a known customer
    string Sku,             // Product SKU
    int Quantity            // >= 1
);

public sealed record CreateOrderResponse(
    string OrderId,
    string Status,
    string Message
);
using FluentValidation;

namespace PowerAppsMcpServer;

public sealed class CreateOrderRequestValidator : AbstractValidator<CreateOrderRequest>
{
    public CreateOrderRequestValidator()
    {
        // Ensure CustomerId is present and well-formed
        RuleFor(x => x.CustomerId)
            .NotEmpty().WithMessage("CustomerId is required.")
            .Length(3, 64);

        // Basic SKU constraints
        RuleFor(x => x.Sku)
            .NotEmpty().WithMessage("Sku is required.")
            .Length(2, 64);

        // Quantity must be positive
        RuleFor(x => x.Quantity)
            .GreaterThan(0).WithMessage("Quantity must be at least 1.");
    }
}

Domain service (DI, testable logic)

using System;
using System.Threading;
using System.Threading.Tasks;

namespace PowerAppsMcpServer;

public interface IOrderService
{
    Task<CreateOrderResponse> CreateAsync(CreateOrderRequest request, CancellationToken ct = default);
}

public sealed class OrderService() : IOrderService // Primary constructor (no fields needed)
{
    public Task<CreateOrderResponse> CreateAsync(CreateOrderRequest request, CancellationToken ct = default)
    {
        // Simulate business logic; replace with real persistence/integration
        var orderId = $"ORD-{Guid.NewGuid():N}";
        return Task.FromResult(new CreateOrderResponse(orderId, "Created", "Order accepted"));
    }
}

HTTP-triggered Function with Azure AD auth and OpenAPI

using System.Net;
using System.Text.Json;
using FluentValidation;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Azure.Functions.Worker.Extensions.OpenApi.Attributes;
using Microsoft.OpenApi.Models;

namespace PowerAppsMcpServer;

public sealed class CreateOrderFunction(
    ILogger<CreateOrderFunction> logger,
    IValidator<CreateOrderRequest> validator,
    IOrderService orderService)
{
    // This endpoint is described for OpenAPI and secured via Azure AD (set on the Function App)
    [Function("CreateOrder")]
    [OpenApiOperation(operationId: "CreateOrder", tags: new[] { "orders" }, Summary = "Create order", Description = "Creates an order with validated input.")]
    [OpenApiRequestBody(contentType: "application/json", bodyType: typeof(CreateOrderRequest), Required = true, Description = "Order payload")]
    [OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "application/json", bodyType: typeof(CreateOrderResponse), Summary = "Order created")]
    public async Task<HttpResponseData> Run(
        [HttpTrigger(AuthorizationLevel.Function, "post", Route = "orders")] HttpRequestData req,
        FunctionContext ctx)
    {
        // Note: Use EasyAuth/Azure AD at the Function App level to avoid handling tokens here.
        // The function still requires a valid AAD token via Custom Connector.

        var payload = await JsonSerializer.DeserializeAsync<CreateOrderRequest>(req.Body, new JsonSerializerOptions
        {
            PropertyNameCaseInsensitive = true
        });

        // Validate request
        var result = await validator.ValidateAsync(payload!);
        if (!result.IsValid)
        {
            var bad = req.CreateResponse(HttpStatusCode.BadRequest);
            await bad.WriteStringAsync(JsonSerializer.Serialize(new
            {
                error = "ValidationFailed",
                details = result.Errors.Select(e => new { e.PropertyName, e.ErrorMessage })
            }));
            return bad;
        }

        // Execute domain logic
        var responseModel = await orderService.CreateAsync(payload!, ctx.CancellationToken);

        // Return success
        var ok = req.CreateResponse(HttpStatusCode.OK);
        await ok.WriteStringAsync(JsonSerializer.Serialize(responseModel));
        return ok;
    }
}

Note: Set the Function App to be secured by Azure AD (App Service Authentication/Authorization). Do not rely on Function Keys in production.

OpenAPI for the Custom Connector

  • Run locally and navigate to /api/swagger/ui to inspect the contract. The OpenAPI JSON is available at /api/swagger.json.
  • Export this JSON and import it when creating your Power Apps Custom Connector, selecting OAuth 2.0 (Azure AD) as the authentication type.

Power Apps integration (Custom Connector)

Authentication setup

  • Create an Azure AD App Registration for the Custom Connector (client app) and expose an application ID URI for the Function App (resource app) if you choose user-assigned scopes. Alternatively, enable EasyAuth with “Log in with Azure Active Directory”.
  • In the Custom Connector, choose OAuth 2.0 (Azure AD). Provide the Authorization URL, Token URL, and the client application details. Use the Application ID URI or scope configured for the Function App.
  • Grant users access via Azure AD and Power Platform permissions so they can acquire tokens and use the connector.

Calling the connector from Power Apps

// Power Apps (Canvas) example formula usage (pseudo):
// Assuming Custom Connector named 'OrdersApi'
Set(
    createResult,
    OrdersApi.CreateOrder({
        CustomerId: "CUST-001",
        Sku: "WIDGET-42",
        Quantity: 2
    })
);
// Access response fields: createResult.OrderId, createResult.Status, createResult.Message

Deployment

// Azure deployment with CLI (example)
// 1) Login
az login

// 2) Create resource group
az group create -n rg-mcp-powerapps -l eastus

// 3) Create storage and function app (Linux, isolated, .NET 8)
az storage account create -n mcpfuncstor$RANDOM -g rg-mcp-powerapps -l eastus --sku Standard_LRS
az functionapp create -n mcp-func-app-$RANDOM -g rg-mcp-powerapps -s <storageName> --consumption-plan-location eastus --runtime dotnet-isolated --functions-version 4

// 4) Enable App Service Authentication with Azure AD (EasyAuth)
# Replace <clientId>, <issuerUrl> as per your AAD setup
az webapp auth microsoft update \
  --resource-group rg-mcp-powerapps \
  --name mcp-func-app-XXXX \
  --client-id <clientId> \
  --issuer "https://login.microsoftonline.com/<tenantId>/v2.0" \
  --unauthenticated-client-action RedirectToLoginPage

// 5) Deploy code
func azure functionapp publish mcp-func-app-XXXX

RBAC roles to assign

  • Deployment automation: Contributor on the resource group or scoped roles to Function App and Storage Account.
  • Function App management: Azure Functions Contributor (for CI/CD and app updates).
  • Storage access (if using Managed Identity to access blobs/queues): Storage Blob Data Reader or Storage Blob Data Contributor as needed, following least privilege.
  • Application Insights access (read-only dashboards): Monitoring Reader.

Validation and typing on the client

If you invoke this API from a TypeScript app (e.g., React), validate payloads before sending. Below is a strictly typed example using Zod.

// TypeScript (strict) example with Zod
import { z } from "zod";

const CreateOrderRequest = z.object({
  CustomerId: z.string().min(3).max(64),
  Sku: z.string().min(2).max(64),
  Quantity: z.number().int().positive()
});
type CreateOrderRequest = z.infer<typeof CreateOrderRequest>;

async function createOrder(apiBase: string, token: string, body: CreateOrderRequest) {
  const payload = CreateOrderRequest.parse(body); // Validates at runtime
  const res = await fetch(`${apiBase}/api/orders`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Authorization": `Bearer ${token}`
    },
    body: JSON.stringify(payload)
  });
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return await res.json() as { OrderId: string; Status: string; Message: string };
}

Best Practices & Security

  • Authentication: Use Azure AD (EasyAuth) with the Custom Connector. Avoid Function Keys to eliminate shared secrets.
  • Authorization: Scope access by Entra app roles or scopes; assign least-privilege RBAC to managed identities.
  • Secrets: Prefer Managed Identity and DefaultAzureCredential to access downstream services. Avoid storing credentials in app settings.
  • Validation: Enforce input validation on the server (FluentValidation) and on the client (Zod) for robust defense-in-depth.
  • Observability: Enable Application Insights. Correlate requests by logging operation IDs and include key business fields.
  • Resiliency: Add retry policies on the client, idempotency keys for order creation, and request timeouts to prevent retries creating duplicates.
  • Versioning: Version endpoints (e.g., /api/v1/orders) and keep your Custom Connector mapped to a specific version.
  • Cost: Consumption plan scales to zero; set sampling in Application Insights to control telemetry cost.

Best Practice: Use AsNoTracking() in Entity Framework when performing read-only queries to improve performance.

Monitoring and alerting

With Application Insights, create alerts on failed requests or latency thresholds.

// Examples (Kusto)
// 1) High failure rate in the last 15 minutes
requests
| where timestamp > ago(15m)
| summarize FailureRate = 100.0 * countif(success == false) / count()

// 2) P95 latency by operation
requests
| where timestamp > ago(1h)
| summarize p95_duration = percentile(duration, 95) by operation_Name

Summary

  • Build your MCP-style server for Power Apps with .NET 8 isolated Azure Functions, DI, and FluentValidation.
  • Secure the API with Azure AD and avoid Function Keys; integrate via a Custom Connector using OAuth 2.0.
  • Ship with OpenAPI, monitoring, and clear RBAC to ensure production readiness.

Surface What’s New: Power Apps integration with a secure .NET 8 API for MCP server updates

The question what is new in powerapps for MCP server lacks a precise product definition, so rather than speculating on features, this guide shows how to reliably surface and version "What’s New" updates into Power Apps using a secure .NET 8 backend, strict TypeScript validation, and Azure-native security. You will get a production-ready pattern that Power Apps can call via a Custom Connector, keeping your release notes current without manual edits.

The Problem

Teams need a trustworthy way to display "What’s New" for MCP server in Power Apps, but the upstream source and format of updates can change. Hardcoding content or querying unsecured endpoints leads to drift, security gaps, and poor developer experience.

Prerequisites

  • .NET 8 SDK
  • Node.js 20+ and a package manager (pnpm/npm)
  • Azure subscription with permissions to create: Azure Functions or Container Apps, Key Vault, Azure API Management, Application Insights
  • Entra ID app registration for the API (OAuth 2.0)
  • Power Apps environment (to build a Custom Connector)

The Solution (Step-by-Step)

1) .NET 8 Minimal API that normalizes "What’s New" items

This minimal API demonstrates production-ready design: DI-first, HttpClientFactory, global exception handling, validation, versioned contract, and managed identity for downstream access.

// Program.cs (.NET 8, file-scoped namespace, minimal API, DI-centric)
using System.Net.Http.Json;
using System.Text.Json;
using System.Text.Json.Serialization;
using Azure.Identity; // DefaultAzureCredential for managed identity
using Azure.Security.KeyVault.Secrets;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Azure;

var builder = WebApplication.CreateBuilder(args);

// Strongly typed options for upstream source configuration
builder.Services.Configure<NewsOptions>(builder.Configuration.GetSection("News"));

// HttpClient with resilient handler
builder.Services.AddHttpClient<INewsClient, NewsClient>()
    .SetHandlerLifetime(TimeSpan.FromMinutes(5));

// Azure clients via DefaultAzureCredential (uses Managed Identity in Azure)
builder.Services.AddAzureClients(azure =>
{
    azure.UseCredential(new DefaultAzureCredential());
    var kvUri = builder.Configuration["KeyVaultUri"]; // e.g., https://my-kv.vault.azure.net/
    if (!string.IsNullOrWhiteSpace(kvUri))
    {
        azure.AddSecretClient(new Uri(kvUri));
    }
});

// Application Insights (OpenTelemetry auto-collection can also be used)
builder.Services.AddApplicationInsightsTelemetry();

var app = builder.Build();

// Global exception handler producing problem+json
app.UseExceptionHandler(errorApp =>
{
    errorApp.Run(async context =>
    {
        var feature = context.Features.Get<IExceptionHandlerPathFeature>();
        var problem = new ProblemDetails
        {
            Title = "Unexpected error",
            Detail = app.Environment.IsDevelopment() ? feature?.Error.ToString() : "",
            Status = StatusCodes.Status500InternalServerError,
            Type = "https://httpstatuses.com/500"
        };
        context.Response.StatusCode = problem.Status ?? 500;
        context.Response.ContentType = "application/problem+json";
        await context.Response.WriteAsJsonAsync(problem);
    });
});

app.MapGet("/health", () => Results.Ok(new { status = "ok" }));

// Versioned route for the normalized news feed (v1)
app.MapGet("/api/v1/news", async (
    INewsClient client
) => Results.Ok(await client.GetNewsAsync()))
.WithName("GetNewsV1")
.Produces<NewsItemV1[]>(StatusCodes.Status200OK)
.ProducesProblem(StatusCodes.Status500InternalServerError);

app.Run();

// Options to control the upstream feed location and parsing mode
public sealed class NewsOptions
{
    public string? SourceUrl { get; init; } // Upstream JSON or RSS converted via a worker
    public string Format { get; init; } = "json"; // json|rss (extend as needed)
}

// Public DTO contract exposed to Power Apps via Custom Connector
public sealed class NewsItemV1
{
    public required string Id { get; init; } // stable identifier
    public required string Title { get; init; }
    public required string Summary { get; init; }
    public required DateTimeOffset PublishedAt { get; init; }
    public string? Category { get; init; } // optional taxonomy
    public string? Url { get; init; } // link to detail page
}

// Client interface for fetching and normalizing upstream data
public interface INewsClient
{
    Task<NewsItemV1[]> GetNewsAsync(CancellationToken ct = default);
}

// Implementation that reads upstream source, validates, and normalizes
public sealed class NewsClient(HttpClient http, Microsoft.Extensions.Options.IOptions<NewsOptions> options) : INewsClient
{
    private readonly HttpClient _http = http;
    private readonly NewsOptions _opts = options.Value;
    private static readonly JsonSerializerOptions JsonOptions = new()
    {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
    };

    public async Task<NewsItemV1[]> GetNewsAsync(CancellationToken ct = default)
    {
        if (string.IsNullOrWhiteSpace(_opts.SourceUrl))
            throw new InvalidOperationException("News:SourceUrl is not configured.");

        // Fetch upstream JSON and map to a stable contract consumed by Power Apps
        var upstreamItems = await _http.GetFromJsonAsync<UpstreamItem[]>(_opts.SourceUrl, JsonOptions, ct)
            ?? Array.Empty<UpstreamItem>();

        // Normalize and order by publish date desc
        return upstreamItems
            .Select(u => new NewsItemV1
            {
                Id = u.Id ?? Guid.NewGuid().ToString("n"),
                Title = u.Title ?? "Untitled",
                Summary = u.Summary ?? string.Empty,
                PublishedAt = u.PublishedAt == default ? DateTimeOffset.UtcNow : u.PublishedAt,
                Category = u.Category,
                Url = u.Url
            })
            .OrderByDescending(n => n.PublishedAt)
            .ToArray();
    }

    // Internal model matching the upstream source (keep flexible)
    private sealed class UpstreamItem
    {
        public string? Id { get; init; }
        public string? Title { get; init; }
        public string? Summary { get; init; }
        public DateTimeOffset PublishedAt { get; init; }
        public string? Category { get; init; }
        public string? Url { get; init; }
    }
}

Configuration (appsettings.json):

{
  "News": {
    "SourceUrl": "https://<your-source>/mcp-news.json",
    "Format": "json"
  },
  "KeyVaultUri": "https://<your-kv>.vault.azure.net/"
}

Pro-Tip: Use AsNoTracking() in Entity Framework when performing read-only queries to improve performance.

2) Secure Azure deployment with Managed Identity and API Management

  • Deploy as Azure Functions (isolated) or Azure Container Apps. Enable System-Assigned Managed Identity.
  • Expose through Azure API Management with OAuth 2.0 (Entra ID) for inbound auth. Create a Power Apps Custom Connector pointing to APIM.

Required RBAC roles (assign to the managed identity or DevOps service principal):

  • Key Vault Secrets User (to read secrets if you store upstream source credentials or API keys)
  • App Configuration Data Reader (if using Azure App Configuration instead of appsettings)
  • API Management Service Contributor (to publish and manage the API surface)
  • Monitoring Reader (to view Application Insights telemetry)
  • Storage Blob Data Reader (only if the upstream source is in Azure Storage)

Pro-Tip: Favor Managed Identity and DefaultAzureCredential across services; avoid connection strings and embedded secrets entirely.

3) Strict TypeScript models with Zod and versioning

The client schema mirrors the v1 API and can evolve with v2+ while keeping backward compatibility in Power Apps and React.

// news.schema.ts (TypeScript, strict mode)
import { z } from "zod";

// Discriminated union enables future breaking changes with clear versioning
export const NewsItemV1 = z.object({
  id: z.string().min(1),
  title: z.string().min(1),
  summary: z.string().default(""),
  publishedAt: z.string().datetime(),
  category: z.string().optional(),
  url: z.string().url().optional()
});

export const NewsResponseV1 = z.array(NewsItemV1);

export type TNewsItemV1 = z.infer<typeof NewsItemV1>;
export type TNewsResponseV1 = z.infer<typeof NewsResponseV1>;

// Future-proof: union for versioned responses
export const NewsResponse = z.union([
  z.object({ version: z.literal("v1"), data: NewsResponseV1 })
]);
export type TNewsResponse = z.infer<typeof NewsResponse>;

4) React 19 component using TanStack Query

Functional component with error and loading states, plus Zod runtime validation.

// NewsPanel.tsx (React 19)
import React from "react";
import { useQuery } from "@tanstack/react-query";
import { NewsResponseV1, type TNewsItemV1 } from "./news.schema";

async function fetchNews(): Promise<TNewsItemV1[]> {
  const res = await fetch("/api/v1/news", { headers: { Accept: "application/json" } });
  if (!res.ok) throw new Error(`Failed: ${res.status}`);
  const json = await res.json();
  const parsed = NewsResponseV1.safeParse(json);
  if (!parsed.success) throw new Error("Schema validation failed");
  return parsed.data;
}

export function NewsPanel(): JSX.Element {
  const { data, error, isLoading } = useQuery({
    queryKey: ["news", "v1"],
    queryFn: fetchNews,
    staleTime: 60_000
  });

  if (isLoading) return <div>Loading updates…</div>;
  if (error) return <div>Failed to load updates.</div>;

  return (
    <ul>
      {data!.map(item => (
        <li key={item.id}>
          <strong>{item.title}</strong> — {new Date(item.publishedAt).toLocaleDateString()}<br />
          {item.summary}
        </li>
      ))}
    </ul>
  );
}

5) Power Apps Custom Connector

Create a Custom Connector targeting the APIM endpoint /api/v1/news. Map the response to your app data schema. Add the connector to your Power App and display the items in a gallery. When the upstream feed changes, you only update the backend normalizer, not the app.

Best Practices & Security

  • Authentication: Use Entra ID for APIM inbound auth. Backend-to-Azure uses DefaultAzureCredential with Managed Identity.
  • Secrets: Store upstream tokens in Key Vault; assign Key Vault Secrets User to the app’s managed identity.
  • Telemetry: Enable Application Insights. Track request IDs and dependency calls for the upstream fetch.
  • Authorization: Restrict APIM access via OAuth scopes and, if needed, IP restrictions or rate limits.
  • Resilience: Configure retry and timeout on HttpClient with sensible limits; add circuit breakers if the upstream is unreliable.
  • Versioning: Pin /api/v1/news; introduce /api/v2/news when the contract changes. Version TypeScript schemas alongside API versions.

App Insights integration (example):

// Add to Program.cs before app.Run(); ensure Application Insights is enabled
app.Use(async (ctx, next) =>
{
    // Correlate requests with upstream calls via trace IDs
    ctx.Response.Headers["Request-Id"] = System.Diagnostics.Activity.Current?.Id ?? Guid.NewGuid().ToString();
    await next();
});

Testing strategy:

  • API: Unit test NewsClient with mocked HttpMessageHandler; integration test /api/v1/news in-memory.
  • TypeScript: Schema tests to ensure validation rejects malformed payloads; component tests for loading/error states.
  • Contract: Add a CI step that fetches a sample payload from the upstream and validates against NewsResponseV1.

Security roles recap:

  • API surface: API Management Service Contributor
  • Secrets: Key Vault Secrets User
  • Config (if used): App Configuration Data Reader
  • Monitoring: Monitoring Reader
  • Storage (if used): Storage Blob Data Reader

Pro-Tip: Use APIM policies (validate-content, rate-limit-by-key) to protect the API consumed by Power Apps.

Summary

  • Do not guess "what’s new" for MCP server; centralize updates behind a stable, versioned API.
  • Use Managed Identity, DefaultAzureCredential, and APIM OAuth to secure end-to-end access for Power Apps.
  • Validate with Zod, monitor with Application Insights, and evolve safely via API/schema versioning.

Wednesday, 21 January 2026

React version not specified in eslint-plugin-react settings. See https://github.com/jsx-eslint/eslint-plugin-react#configuration

When we  build the PCF project and  receive below warning  

Error:

Warning: React version not specified in eslint-plugin-react settings. See https://github.com/jsx-eslint/eslint-plugin-react#configuration .

[4:47:40 PM] [start]  Compiling and bundling control...



Solution : Open the ".eslintrc.json" and add below configuration 


"settings": {

      "react": {

        "version": "detect"

      }

    }



Dynamics CRM and Model-Driven Apps in Power Apps: The Complete, No-Fluff Guide

Dynamics CRM and model-driven app in Power Apps are core pieces of Microsoft’s business application stack that help teams design scalable, secure, and data-driven solutions with minimal code. This guide explains what each is, how they connect, when to use them, and how to get started—complete with practical examples.

What is Dynamics CRM (Dynamics 365 Customer Engagement)?

Originally known as Dynamics CRM, Microsoft now delivers these capabilities through Dynamics 365 Customer Engagement (CE) apps like Sales, Customer Service, and Marketing. These apps run on Microsoft Dataverse, offering standardized tables (Accounts, Contacts, Leads, Opportunities, Cases) and robust features like security roles, business process flows, audit history, and integration with Microsoft 365 and Azure.

  • Purpose-built apps: Sales pipeline, service case management, field service, and more.
  • Enterprise-grade platform: Role-based security, activity tracking, SLA management, and analytics.
  • Extensible: Customize with Power Platform (Power Apps, Power Automate, Power BI) and Azure services.

What is a Model-Driven App in Power Apps?

A model-driven app is a Power Apps application generated from your data model in Dataverse. Instead of pixel-perfect canvas design, you define tables, columns, relationships, forms, views, charts, dashboards, and the app renders responsive UI automatically across devices.

  • Metadata-driven: Configure data structure and business rules; the UI follows.
  • Built-in UX patterns: Grids, forms, advanced filtering, and command bar actions.
  • Process automation: Business rules, business process flows (BPFs), and Power Automate flows.

How Dynamics CRM and Model-Driven Apps Work Together

Dynamics 365 CE apps are essentially first-party model-driven apps built on Dataverse with Microsoft’s prebuilt tables and logic. You can extend these apps or create your own model-driven apps alongside them to address specialized processes—reusing the same security, data, and automation framework.

  • Shared data: Use Accounts, Contacts, and custom tables in both Dynamics 365 and your model-driven apps.
  • Unified security: Apply the same roles, field security, and auditing across apps.
  • Coexistence: Extend Sales or Customer Service with custom model-driven apps for unique teams or regions.

Model-Driven Apps vs. Canvas Apps

  • Model-driven apps: Best for data-intensive processes, standardized UIs, enterprise security, and rapid configuration.
  • Canvas apps: Best for tailored, pixel-perfect experiences, task apps, and multi-connector mashups.
  • Hybrid approach: Use model-driven for core records and processes; embed canvas components for specialized screens.

When to Use Model-Driven Apps

  • Complex data relationships: Many related tables (e.g., Accounts → Opportunities → Quotes).
  • Strict governance: Need audit trails, role-based access, field-level security, and compliance.
  • Process-centric work: Guided stages with approvals and SLAs using business process flows.
  • Fast time-to-value: Configure metadata, not custom code, to deliver quickly.

Step-by-Step: Build a Model-Driven App

  • 1. Plan your data model: Identify tables (e.g., Deals, Accounts), columns (e.g., Deal Value), and relationships.
  • 2. Create tables in Dataverse: Add columns, set data types, define relationships and choice fields.
  • 3. Design forms and views: Build main forms, quick create forms, and list views with relevant columns and filters.
  • 4. Add business rules and BPFs: Enforce field logic and create stage-based process guidance (Qualify → Propose → Close).
  • 5. Add charts and dashboards: Visualize KPIs like open deals, case backlogs, or SLA breaches.
  • 6. Secure the app: Configure security roles, field security profiles, and sharing as needed.
  • 7. Build the app shell: In Power Apps, create a model-driven app, add navigation areas, tables, views, and dashboards.
  • 8. Automate: Use Power Automate for notifications, approvals, and integrations (e.g., Teams, Outlook, SharePoint).
  • 9. Test and publish: Validate role-based experiences, performance, and data quality rules before rollout.

Real-World Examples

Sales Pipeline Management

Create tables for Leads, Opportunities, Products, and Quotes. Use a BPF to guide reps from Qualify to Close, automate quote approval with Power Automate, and surface a dashboard showing pipeline by stage and forecast category.

Customer Service Case Management

Use Cases, Queues, and SLAs. Route incoming cases based on topic and priority, track response times, and surface knowledge articles. Enable agents to update records via a streamlined model-driven interface.

Partner Onboarding Portal (with Power Pages)

Maintain Accounts and Partner Profiles in Dataverse. Expose selected data externally through Power Pages, while internal teams manage approvals and reviews in a model-driven app.

Key Benefits

  • Consistency: Standardized UI and validated data entry across teams.
  • Compliance-ready: Audit logs, security roles, and field-level security.
  • Scalable: Handles complex schemas and large user bases.
  • Low-code agility: Configure quickly, extend with pro-code when needed.

Best Practices and Governance

  • Solution-first development: Use managed/unmanaged solutions, publisher prefixes, and ALM pipelines.
  • Naming and schema discipline: Use consistent table/column naming and choice values.
  • Security by design: Define least-privilege roles early; avoid broad org-level permissions.
  • Data quality: Use required fields, duplicate detection, and business rules to enforce standards.
  • Performance: Optimize views, indexes, and avoid overly complex client scripts.
  • Documentation: Capture data model diagrams, process maps, and support playbooks.

FAQ

Is Dynamics CRM the same as Dynamics 365?

The term “Dynamics CRM” refers to the legacy branding. Today, capabilities are delivered through Dynamics 365 Customer Engagement apps (e.g., Sales, Customer Service) running on Dataverse.

Do I need Dynamics 365 to build a model-driven app?

No. You can build standalone model-driven apps on Dataverse. However, many organizations combine them with Dynamics 365 for richer, out-of-the-box capabilities.

Can I integrate with Outlook, Teams, or SharePoint?

Yes. Model-driven apps integrate with Microsoft 365, enabling email tracking, file storage, collaboration, and notifications.

Sunday, 18 January 2026

Integrate Power Apps with AI using Azure Functions (.NET 8) and Azure OpenAI with Managed Identity

Integrate Power Apps with AI by fronting Azure OpenAI behind a secure .NET 8 isolated Azure Function, authenticated via Entra ID and deployed with Azure Bicep. Why this matters: you keep your Azure OpenAI keyless via Managed Identity, enforce RBAC, and provide a stable HTTPS endpoint for Power Apps using a custom connector.

The Problem

Developers need to call AI securely from Power Apps without exposing keys, while meeting enterprise requirements for RBAC, observability, and least-privilege. Manual wiring through the portal and ad hoc security checks lead to drift and risk.

Prerequisites

  • Azure CLI 2.60+
  • .NET 8 SDK
  • Azure Developer CLI (optional) or Bicep CLI
  • Owner or User Access Administrator on the target subscription
  • Power Apps environment access for creating a custom connector

The Solution (Step-by-Step)

1) Deploy Azure resources with Bicep (Managed Identity, Function App, Azure OpenAI, RBAC, Easy Auth)

This Bicep template creates a Function App with system-assigned managed identity, Azure OpenAI with a model deployment, App Service Authentication (Easy Auth) enforced, and assigns the Cognitive Services OpenAI User role to the identity.

// main.bicep
targetScope = 'resourceGroup'

@description('Name prefix for resources')
param namePrefix string

@description('Location for resources')
param location string = resourceGroup().location

@description('Azure OpenAI model name, e.g., gpt-4o-mini')
param aoaiModelName string = 'gpt-4o-mini'

@description('Azure OpenAI model version for your region (see Azure docs for supported versions).')
param aoaiModelVersion string = '2024-07-18' // Tip: Validate the correct version via az cognitiveservices account list-models

var funcName = '${namePrefix}-func'
var aoaiName = '${namePrefix}-aoai'
var hostingPlanName = '${namePrefix}-plan'
var appInsightsName = '${namePrefix}-ai'
var storageName = toLower(replace('${namePrefix}st${uniqueString(resourceGroup().id)}', '-', ''))

resource storage 'Microsoft.Storage/storageAccounts@2023-05-01' = {
  name: storageName
  location: location
  sku: {
    name: 'Standard_LRS'
  }
  kind: 'StorageV2'
}

resource appInsights 'Microsoft.Insights/components@2020-02-02' = {
  name: appInsightsName
  location: location
  kind: 'web'
  properties: {
    Application_Type: 'web'
  }
}

resource plan 'Microsoft.Web/serverfarms@2023-12-01' = {
  name: hostingPlanName
  location: location
  sku: {
    name: 'Y1' // Consumption for Functions
    tier: 'Dynamic'
  }
}

resource func 'Microsoft.Web/sites@2023-12-01' = {
  name: funcName
  location: location
  kind: 'functionapp'
  identity: {
    type: 'SystemAssigned'
  }
  properties: {
    serverFarmId: plan.id
    siteConfig: {
      appSettings: [
        { name: 'AzureWebJobsStorage', value: storage.listKeys().keys[0].value }
        { name: 'APPLICATIONINSIGHTS_CONNECTION_STRING', value: appInsights.properties.ConnectionString }
        { name: 'FUNCTIONS_EXTENSION_VERSION', value: '~4' }
        { name: 'FUNCTIONS_WORKER_RUNTIME', value: 'dotnet-isolated' }
        // Enable Easy Auth enforcement via config below (authSettingsV2)
      ]
      http20Enabled: true
      minimumTlsVersion: '1.2'
      ftpsState: 'Disabled'
      cors: {
        allowedOrigins: [
          // Add your Power Apps domain if needed; CORS is typically managed by the custom connector
        ]
      }
    }
    httpsOnly: true
  }
}

// Enable App Service Authentication (Easy Auth) with Entra ID and enforce authentication.
resource auth 'Microsoft.Web/sites/config@2023-12-01' = {
  name: '${func.name}/authsettingsV2'
  properties: {
    globalValidation: {
      requireAuthentication: true
      unauthenticatedClientAction: 'Return401'
    }
    identityProviders: {
      azureActiveDirectory: {
        enabled: true
        registration: {
          // When omitted, system can use an implicit provider. For enterprise, specify a registered app:
          // Provide clientId and clientSecretSettingName if using a dedicated app registration.
        }
        validation: {
          // Audience (App ID URI or application ID). Power Apps custom connector should request this audience.
          // Replace with your API application ID URI when using a dedicated AAD app registration.
          allowedAudiences: [
            'api://<your-app-id>'
          ]
        }
        login: {
          disableWWWAuthenticate: false
        }
      }
    }
    platform: {
      enabled: true
      runtimeVersion: '~1'
    }
    login: {
      tokenStore: {
        enabled: true
      }
    }
  }
}

// Azure OpenAI account
resource aoai 'Microsoft.CognitiveServices/accounts@2023-05-01' = {
  name: aoaiName
  location: location
  kind: 'OpenAI'
  sku: {
    name: 'S0'
  }
  properties: {
    customSubDomainName: toLower(aoaiName)
    publicNetworkAccess: 'Enabled'
  }
}

// Azure OpenAI deployment - ensure model/version are valid for your region
resource aoaiDeployment 'Microsoft.CognitiveServices/accounts/deployments@2023-05-01' = {
  name: 'chat-${aoaiModelName}'
  parent: aoai
  properties: {
    model: {
      format: 'OpenAI'
      name: aoaiModelName
      version: aoaiModelVersion
    }
    capacities: [
      {
        capacity: 1
        capacityType: 'Standard'
      }
    ]
  }
}

// Assign RBAC: Cognitive Services OpenAI User role to the Function's managed identity
// Built-in role: Cognitive Services OpenAI User (Role ID: 5e0bd9bd-7ac1-4c9e-8289-1b01f135d4a8)
resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
  name: guid(aoai.id, func.identity.principalId, 'CognitiveServicesOpenAIUserRole')
  scope: aoai
  properties: {
    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '5e0bd9bd-7ac1-4c9e-8289-1b01f135d4a8')
    principalId: func.identity.principalId
    principalType: 'ServicePrincipal'
  }
}

output functionAppName string = func.name
output openAiEndpoint string = 'https://${aoai.name}.openai.azure.com/'
output openAiDeployment string = aoaiDeployment.name

Tip: To find the correct model version for your region, run: az cognitiveservices account list-models --name <aoaiName> --resource-group <rg> --output table.

2) Implement a secure .NET 8 isolated Azure Function with DI and Managed Identity

The function validates Entra ID via Easy Auth headers, performs input validation, calls Azure OpenAI using DefaultAzureCredential, and returns a minimal response. AuthorizationLevel is Anonymous because App Service Authentication is enforcing auth at the edge; the code still verifies identity defensively.

// Program.cs
using System.Text.Json;
using Azure;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Middleware;
using Microsoft.Azure.Functions.Worker.Extensions.OpenApi.Extensions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

var host = new HostBuilder()
    .ConfigureFunctionsWebApplication(builder =>
    {
        // Add global middleware if needed (e.g., correlation, exception handling)
        builder.UseMiddleware<GlobalExceptionMiddleware>();
    })
    .ConfigureServices((ctx, services) =>
    {
        var config = ctx.Configuration;

        // Bind custom options
        services.Configure<OpenAIOptions>(config.GetSection(OpenAIOptions.SectionName));

        // Register OpenAI client using Managed Identity (no keys)
        services.AddSingleton((sp) =>
        {
            var options = sp.GetRequiredService<Microsoft.Extensions.Options.IOptions<OpenAIOptions>>().Value;
            // Use DefaultAzureCredential to leverage Managed Identity in Azure
            var credential = new DefaultAzureCredential();
            return new OpenAIClient(new Uri(options.Endpoint), credential);
        });

        services.AddSingleton<IPromptService, PromptService>();
        services.AddApplicationInsightsTelemetryWorkerService();
    })
    .ConfigureAppConfiguration(config =>
    {
        config.AddEnvironmentVariables();
    })
    .ConfigureLogging(logging =>
    {
        logging.AddConsole();
    })
    .Build();

await host.RunAsync();

file sealed class GlobalExceptionMiddleware : IFunctionsWorkerMiddleware
{
    public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next)
    {
        try
        {
            await next(context);
        }
        catch (Exception ex)
        {
            var logger = context.GetLogger<GlobalExceptionMiddleware>();
            logger.LogError(ex, "Unhandled exception");

            // Avoid leaking internals; return generic error with correlation ID
            var invocationId = context.InvocationId;
            var http = await context.GetHttpRequestDataAsync();
            if (http is not null)
            {
                var response = http.CreateResponse(System.Net.HttpStatusCode.InternalServerError);
                await response.WriteStringAsync($"Request failed. CorrelationId: {invocationId}");
                context.GetInvocationResult().Value = response;
            }
        }
    }
}

file sealed class OpenAIOptions
{
    public const string SectionName = "OpenAI";
    public string Endpoint { get; init; } = string.Empty; // e.g., https://<aoaiName>.openai.azure.com/
    public string Deployment { get; init; } = string.Empty; // e.g., chat-gpt-4o-mini
}

// Service abstraction to keep function logic clean
file interface IPromptService
{
    Task<string> CreateChatResponseAsync(string userInput, CancellationToken ct);
}

file sealed class PromptService(OpenAIClient client, Microsoft.Extensions.Options.IOptions<OpenAIOptions> opts, ILogger<PromptService> logger) : IPromptService
{
    private readonly OpenAIClient _client = client;
    private readonly OpenAIOptions _opts = opts.Value;
    private readonly ILogger<PromptService> _logger = logger;

    public async Task<string> CreateChatResponseAsync(string userInput, CancellationToken ct)
    {
        // Basic input trimming and minimal length check
        var prompt = (userInput ?? string.Empty).Trim();
        if (prompt.Length < 3)
        {
            return "Input too short.";
        }

        // Create chat completion with minimal system prompt
        var req = new ChatCompletionsOptions()
        {
            DeploymentName = _opts.Deployment,
            Temperature = 0.2f,
            MaxTokens = 256
        };
        req.Messages.Add(new ChatRequestSystemMessage("You are a concise assistant.")); // Guardrail
        req.Messages.Add(new ChatRequestUserMessage(prompt));

        _logger.LogInformation("Calling Azure OpenAI deployment {Deployment}", _opts.Deployment);

        var resp = await _client.GetChatCompletionsAsync(req, ct);
        var msg = resp.Value.Choices.Count > 0 ? resp.Value.Choices[0].Message.Content[0].Text : "No response.";
        return msg ?? string.Empty;
    }
}
// HttpFunction.cs
using System.Net;
using System.Security.Claims;
using System.Text;
using System.Text.Json;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace Api;

public sealed class ChatRequest
{
    // Strongly typed DTO for request validation
    public string Prompt { get; init; } = string.Empty;
}

public sealed class ChatFunction(IPromptService promptService, ILogger<ChatFunction> logger) 
{
    // Simple helper to read Easy Auth principal header safely
    private static ClaimsPrincipal? TryGetPrincipal(HttpRequestData req)
    {
        // Easy Auth sets x-ms-client-principal as Base64 JSON
        if (!req.Headers.TryGetValues("x-ms-client-principal", out var values)) return null;
        var b64 = values.FirstOrDefault();
        if (string.IsNullOrWhiteSpace(b64)) return null;

        try
        {
            var json = Encoding.UTF8.GetString(Convert.FromBase64String(b64));
            using var doc = JsonDocument.Parse(json);
            var claims = new List<Claim>();
            if (doc.RootElement.TryGetProperty("claims", out var arr))
            {
                foreach (var c in arr.EnumerateArray())
                {
                    var typ = c.GetProperty("typ").GetString() ?? string.Empty;
                    var val = c.GetProperty("val").GetString() ?? string.Empty;
                    claims.Add(new Claim(typ, val));
                }
            }
            return new ClaimsPrincipal(new ClaimsIdentity(claims, "EasyAuth"));
        }
        catch
        {
            return null;
        }
    }

    [Function("chat")]
    public async Task<HttpResponseData> Run(
        [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "chat")] HttpRequestData req,
        FunctionContext context)
    {
        var principal = TryGetPrincipal(req);
        if (principal is null)
        {
            // Defense in depth: App Service auth should already block, but we double-check.
            var unauth = req.CreateResponse(HttpStatusCode.Unauthorized);
            await unauth.WriteStringAsync("Authentication required.");
            return unauth;
        }

        // Basic content-type check and bound read limit
        if (!req.Headers.TryGetValues("Content-Type", out var ct) || !ct.First().Contains("application/json", StringComparison.OrdinalIgnoreCase))
        {
            var bad = req.CreateResponse(HttpStatusCode.BadRequest);
            await bad.WriteStringAsync("Content-Type must be application/json.");
            return bad;
        }

        // Safe body read
        using var reader = new StreamReader(req.Body);
        var body = await reader.ReadToEndAsync();
        ChatRequest? input;
        try
        {
            input = JsonSerializer.Deserialize<ChatRequest>(body, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
        }
        catch
        {
            var bad = req.CreateResponse(HttpStatusCode.BadRequest);
            await bad.WriteStringAsync("Invalid JSON.");
            return bad;
        }

        if (input is null || string.IsNullOrWhiteSpace(input.Prompt))
        {
            var bad = req.CreateResponse(HttpStatusCode.BadRequest);
            await bad.WriteStringAsync("Prompt is required.");
            return bad;
        }

        var loggerScope = new Dictionary<string, object> { ["UserObjectId"] = principal.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier")?.Value ?? "unknown" };
        using (logger.BeginScope(loggerScope))
        {
            try
            {
                var result = await promptService.CreateChatResponseAsync(input.Prompt, context.CancellationToken);
                var ok = req.CreateResponse(HttpStatusCode.OK);
                await ok.WriteAsJsonAsync(new { response = result });
                return ok;
            }
            catch (Exception ex)
            {
                logger.LogError(ex, "Error generating chat response");
                var err = req.CreateResponse(HttpStatusCode.BadGateway);
                await err.WriteStringAsync("AI request failed. Try again later.");
                return err;
            }
        }
    }
}
// appsettings.json (for local debug)
{
  "OpenAI": {
    "Endpoint": "https://<your-aoai-name>.openai.azure.com/",
    "Deployment": "chat-gpt-4o-mini"
  }
}

Tip: When running locally, ensure you are logged in with Azure CLI (az login) so DefaultAzureCredential can acquire a token. Assign yourself Cognitive Services OpenAI User on the AOAI resource for testing.

3) Enforce Azure AD (Easy Auth) - configuration example

The Bicep above enables Easy Auth and returns 401 for unauthenticated calls. If you maintain settings as JSON, you can also use an ARM style configuration:

// Example snippet (appsettings for site config via ARM/Bicep-style) ensures:
// - requireAuthentication: true
// - allowedAudiences must match the audience used by your client (Power Apps custom connector)
{
  "properties": {
    "authSettingsV2": {
      "platform": { "enabled": true },
      "globalValidation": {
        "requireAuthentication": true,
        "unauthenticatedClientAction": "Return401"
      },
      "identityProviders": {
        "azureActiveDirectory": {
          "enabled": true,
          "validation": {
            "allowedAudiences": [ "api://<your-app-id>" ]
          }
        }
      }
    }
  }
}

Tip: For most enterprise setups, register a dedicated App Registration for the Function API and reference its Application ID URI in allowedAudiences. Keep consent and scopes explicit.

4) Create a Power Apps Custom Connector

  1. In Power Apps, open Solutions and create a new Custom Connector.
  2. Set the Host to your Function URL (e.g., https://<func-name>.azurewebsites.net).
  3. Define the POST /chat operation that accepts a JSON body { "prompt": "..." } and returns { "response": "..." }.
  4. Security: Choose OAuth 2.0 (Azure Active Directory). Set Audience to match allowedAudiences (api://<your-app-id>). Supply Tenant ID and Client ID as provided by your admin.
  5. Test the connector. You should receive HTTP 200 with the AI response. Unauthorized calls return 401.

5) Add client-side validation in Power Apps (Power Fx)

Before invoking the connector, validate inputs in the app to fail fast and reduce server load.

// Example Power Fx for a button's OnSelect
If(
    IsBlank(Trim(txtPrompt.Text)) || Len(Trim(txtPrompt.Text)) < 3,
    Notify("Please enter at least 3 characters.", NotificationType.Error),
    Set(
        aiResult,
        YourConnector.chat(
            {
                prompt: Trim(txtPrompt.Text)
            }
        ).response
    )
);

// Display result in a Label: Text = aiResult

Tip: Add input length limits and debouncing for multi-keystroke actions. For read-only UI against data, prefer server-side pagination and AsNoTracking() on the API layer where EF Core is involved.

Best Practices & Security

Identity and RBAC

  • Use Managed Identity for the Function App; never store secrets. The sample uses DefaultAzureCredential.
  • Assign the Cognitive Services OpenAI User role to the Function’s identity on the Azure OpenAI resource. This allows API calls without exposing keys.
  • Enforce Azure AD with Easy Auth at the platform level and verify identity in code for defense in depth.

Least Privilege and Network

  • Restrict access with the minimum RBAC scope necessary. Avoid granting Contributor on the subscription.
  • Enable HTTPS only and TLS 1.2+. Disable FTP/FTPS where possible.
  • Consider Private Endpoints for Azure OpenAI and Functions behind an Application Gateway/WAF for enterprise networks.

Validation, Error Handling, and Reliability

  • Validate input on both client (Power Apps) and server (Function). The code enforces content-type and schema shape.
  • Handle exceptions using middleware and return generic messages with correlation IDs to avoid leaking internals.
  • Implement circuit breakers and retries for downstream calls with transient fault policies if you add HTTP dependencies.

Observability

  • Use Application Insights. Log prompt sizes and latency, not raw sensitive content.
  • Sample KQL to investigate errors:
// Requests by operation
requests
| where url endswith "/chat"
| summarize count() by resultCode

// Exceptions with correlation
exceptions
| where operation_Name == "chat"
| project timestamp, type, problemId, outerMessage, operation_Id

// Latency p95
requests
| where url endswith "/chat"
| summarize p95(duration) by bin(timestamp, 1h)

Cost and Abuse Controls

  • Apply rate limiting at the API Management layer if exposing broadly.
  • Set MaxTokens and Temperature conservatively; monitor usage with budgets and alerts.
  • Implement content filters as needed using Azure AI Content Safety.

Summary

  • Power Apps integrates cleanly with AI by calling a secure .NET 8 Azure Function that uses Managed Identity to access Azure OpenAI.
  • Security is enforced with Azure AD (Easy Auth) and RBAC, specifically the Cognitive Services OpenAI User role.
  • IaC with Bicep, defensive coding, and Application Insights deliver repeatable, production-grade operations.

Tuesday, 13 January 2026

Power Platform 2026: What’s New, What to Expect, and How to Prepare

Curious about what's new on the Power Platform in 2026? While real-time details may vary by region and release wave, this guide outlines the most credible trends, expected enhancements, and practical steps to help you prepare for Power Platform 2026 updates across Power Apps, Power Automate, Power BI, Dataverse, and governance.

Where to Find the Official 2026 Updates

Microsoft typically publishes semiannual release plans (Wave 1 and Wave 2). For authoritative details on Power Platform 2026 features, review the latest release plans, product blogs, and admin center message posts. Use these sources to validate dates, preview availability, and regional rollout specifics.

What’s Likely New Across the Power Platform in 2026

Power Apps: Faster, Smarter, More Governed

  • Deeper Copilot in app building: Expect broader natural language to app/screen/form experiences, with smarter controls suggestions and data-binding from sample prompts.
  • Advanced performance profiles: More diagnostics and client-side performance hints for complex canvas and model-driven apps.
  • Reusable design systems: Expanded theming and component libraries to accelerate enterprise-wide UI consistency.
  • ALM-ready solutions: Tighter pipelines from dev to prod with improved solution layering and environment variables.

Power Automate: End-to-End Automation Intelligence

  • Process mining at scale: Richer discovery and variant analysis integrated with automation recommendations.
  • Copilot for flows: Natural language flow creation, repair suggestions, and test data generation for robust automation.
  • Robotic automation hardening: More resilient desktop flows with enhanced error handling, retries, and monitoring.
  • Event-driven patterns: Expanded triggers and durable patterns for long-running business processes.

Power BI: Fabric-First and AI-Assisted Insights

  • Seamless Fabric integration: Tighter lakehouse/warehouse connectivity, semantic models, and item-level security alignment.
  • AI-assisted analysis: Enhanced narrative summaries, anomaly detection, and Q&A responsiveness.
  • Governed self-service: Broader deployment pipelines, endorsements, and lineage to scale enterprise BI safely.

Dataverse, Security, and Governance

  • Managed Environments maturity: More policy templates for DLP, data residency, and solution lifecycle guardrails.
  • Dataverse scalability: Performance, indexing, and data mesh patterns for cross-domain collaboration.
  • Compliance and audit: Finer-grained logging, retention, and admin analytics for regulated industries.

Integration Trends to Watch in 2026

  • Connector ecosystem growth: More premium and enterprise-grade connectors with higher throughput and better error transparency.
  • Microsoft Fabric alignment: Unified governance and pipelines spanning data engineering, science, and BI.
  • Responsible AI: Stronger content filters, prompt controls, and audit trails for Copilot experiences.
  • Hybrid and multi-cloud: Expanded patterns for secure integration with non-Microsoft services via standard protocols.

How to Prepare Your Organization

  • Adopt Managed Environments: Standardize policies, usage analytics, and solution checks before large rollouts.
  • Harden ALM pipelines: Use solution-based development, branches, and automated validations to reduce drift.
  • Establish a design system: Build a component library for consistent UX across Power Apps.
  • Create a data foundation: Align Dataverse and Fabric models; document ownership, lineage, and policies.
  • Upskill on Copilot: Train makers and data teams to co-create with AI and review outputs for accuracy and compliance.
  • Triage automations: Prioritize high-ROI flows, add observability, and plan for exception handling.

Example Roadmap to Adopt 2026 Features

  • Quarter 1: Inventory apps/flows, implement Managed Environments, define DLP, and set ALM baselines.
  • Quarter 2: Pilot Copilot-assisted app/flow creation in a sandbox; benchmark performance and quality.
  • Quarter 3: Integrate with Fabric for governed datasets; standardize deployment pipelines for BI.
  • Quarter 4: Scale process mining, consolidate connectors, and publish shared components across teams.

Examples: Practical Use Cases

  • Field service app: Use Copilot to scaffold screens from a Dataverse table, add offline capabilities, and enforce role-based access.
  • Invoice automation: Discover steps via process mining, build an approval flow with exception routing, and log metrics to a central dashboard.
  • Executive analytics: Publish Fabric-backed semantic models with certified datasets and AI-generated summaries for board reviews.

Key Takeaways

  • 2026 will emphasize AI + governance: Expect Copilot advances paired with stronger controls and telemetry.
  • Data is the backbone: Align Dataverse and Fabric to reduce duplication and increase trust.
  • Prepare early: Solid ALM, DLP, and design systems make adopting new features faster and safer.

Staying Current

Track the semiannual release plans, enable previews in non-production environments, and review admin center announcements. Validate features in a controlled rollout before scaling to production to ensure performance, security, and compliance objectives are met.

Sunday, 11 January 2026

Binding Pre-Search on Lookup Fields in Model-Driven Apps Using TypeScript

The Problem

Developers often need to filter lookup fields dynamically in Microsoft Power Apps model-driven forms. The challenge is binding a pre-search event to a lookup control using TypeScript with strong typing, ensuring maintainability and avoiding the use of 'any'.

Prerequisites

  • Node.js v20+
  • TypeScript 5+
  • @types/xrm installed for strong typing
  • Power Apps environment with a model-driven app
  • Appropriate security roles: System Customizer or equivalent with form customization privileges

The Solution (Step-by-Step)

1. Add TypeScript Reference

// Add Xrm type definitions at the top of your file
/// 

2. Implement the Pre-Search Binding

// Function to bind pre-search event on a lookup field
function addPreSearchToLookup(executionContext: Xrm.Events.EventContext): void {
    const formContext = executionContext.getFormContext();

    // Get the lookup control by name
    const lookupControl = formContext.getControl("new_lookupfield");

    if (lookupControl) {
        // Add pre-search event handler
        lookupControl.addPreSearch(() => {
            // Define custom filter XML
            const filterXml = "";

            // Apply the filter to the lookup
            lookupControl.addCustomFilter(filterXml, "entitylogicalname");
        });
    }
}

// Register this function on the form's OnLoad event in the form editor

Explanation:

  • We use addPreSearch to inject a custom filter before the lookup search executes.
  • Strong typing is enforced using Xrm.Controls.LookupControl.
  • No 'any' type is used, ensuring type safety.

Best Practices & Security

  • Type Safety: Always use @types/xrm for strong typing and avoid 'any'.
  • Security Roles: Ensure users have roles like System Customizer or App Maker to customize forms.
  • Principle of Least Privilege: Assign only necessary roles to users who manage form scripts.
  • Deployment Automation: Use Power Platform CLI or Azure DevOps pipelines for ALM. For IaC, consider Azure Bicep to manage environment configurations.
  • Testing: Test the pre-search logic in a sandbox environment before deploying to production.

Pro-Tip: Use lookupControl.addCustomFilter() with specific entity names to avoid applying filters globally, which can cause unexpected behavior.

Summary

  • Bind pre-search events using addPreSearch for dynamic lookup filtering.
  • Enforce strong typing with @types/xrm and avoid 'any'.
  • Secure and automate deployments using Power Platform CLI and Azure IaC tools.

Tuesday, 6 January 2026

How to Create a Polymorphic Column in Dataverse: A Complete Guide

What is a Polymorphic Column in Dataverse?

A polymorphic column in Dataverse is a special type of column that can reference multiple tables instead of being limited to a single table. This feature is particularly useful when you need flexibility in your data model, allowing a single column to store references to different entity types without creating multiple lookup fields.

Why Use Polymorphic Columns?

Polymorphic columns provide several benefits for developers and businesses:

  • Flexibility: They allow a single column to relate to multiple tables, reducing complexity.
  • Efficiency: Simplifies data relationships and reduces the need for redundant fields.
  • Scalability: Ideal for scenarios where the related entity can vary, such as activities linked to different record types.

Steps to Create a Polymorphic Column in Dataverse

Follow these steps to create a polymorphic column in Microsoft Dataverse:

  • Step 1: Navigate to the Power Apps Maker Portal and select your environment.
  • Step 2: Open the Tables section and choose the table where you want to add the column.
  • Step 3: Click on Add Column and select Lookup as the data type.
  • Step 4: In the Related Table dropdown, choose Activity or Customer to enable polymorphic behavior. These are the two primary polymorphic relationships supported by Dataverse.
  • Step 5: Save and publish your changes to make the column available in your apps and flows.

Best Practices for Using Polymorphic Columns

To ensure optimal performance and maintainability, consider these best practices:

  • Use polymorphic columns only when necessary to avoid unnecessary complexity.
  • Document the relationships clearly for future reference.
  • Test your apps thoroughly to ensure the column behaves as expected across different scenarios.

By following these steps and best practices, you can effectively create and manage polymorphic columns in Dataverse, enhancing the flexibility and scalability of your data model.