" MicromOne

Pagine

Building a React Web Resource for Microsoft Dynamics 365 CRM with React App Rewired

Modernizing the user experience in Microsoft Dynamics 365 CRM often means bringing modern frontend technologies into an established platform. While Power Apps Component Framework (PCF) is Microsoft's recommended approach for many scenarios, traditional Web Resources remain an excellent option for complex pages, dialogs, preview screens, and standalone applications.

In this article, we'll build a React-based Web Resource using Create React App and React App Rewired, customize the build output for Dynamics 365, and discuss when a Web Resource is a better choice than a PCF component.

Why Use React for Dynamics 365?

React offers several advantages when developing custom interfaces for Dynamics 365:

  • Component-based architecture

  • Excellent TypeScript support

  • Large ecosystem

  • Easy state management

  • Rich UI libraries such as Fluent UI

  • Easy integration with REST APIs and Dataverse Web API

Instead of writing plain HTML and JavaScript, React allows you to organize your application into reusable components that are easier to maintain.

Project Structure

A typical project uses Create React App together with React App Rewired.

Example dependencies:

  • React 19

  • TypeScript

  • Fluent UI React Components

  • React App Rewired

  • Xrm Type Definitions

The package configuration includes scripts like:

"scripts": {
  "start": "react-app-rewired start",
  "build": "react-app-rewired build"
}

Using React App Rewired allows customization of the webpack configuration without ejecting from Create React App.

Why Customize the Build?

Dynamics 365 Web Resources expect predictable file names.

The default Create React App build generates hashed filenames such as:

main.84af73.js
main.27aa5.css

These change with every build, making deployment inconvenient.

Instead, we configure webpack to produce fixed names like:

dps_contentpreview.js
dps_contentpreview.chunk.js
css/main.css

This makes importing Web Resources into Dynamics much easier.

Customizing the Output Folder

The first customization changes the build destination.

paths.appBuild = paths.appBuild.replace(
    "build",
    "../../dps_/pages/dps_contentpreview"
);

Instead of generating a local build folder, the compiled files are copied directly into the Dynamics solution folder.

This saves an extra copy step during development.

Customizing JavaScript Output

Webpack normally creates hashed bundles.

We override them:

config.output.filename = "dps_contentpreview.js";
config.output.chunkFilename = "dps_contentpreview.chunk.js";

Benefits include:

  • predictable deployment

  • easier solution packaging

  • no need to update Web Resource references after every build

Customizing CSS Output

The same principle applies to CSS.

config.plugins[5].options.filename = "css/[name].css";
config.plugins[5].options.moduleFilename = "css/[name].chunk.css";

Keeping CSS files stable simplifies deployment and version control.

Using Fluent UI

This project uses Fluent UI v9.

"@fluentui/react-components"

Fluent UI provides components that closely match Microsoft's design language, resulting in interfaces that feel native inside Dynamics 365.

Examples include:

  • Buttons

  • Dialogs

  • Tables

  • Cards

  • Tooltips

  • Inputs

  • Dropdowns

TypeScript Support

Using TypeScript greatly improves development.

Benefits include:

  • IntelliSense

  • Compile-time error checking

  • Better refactoring

  • Strong typing for Dynamics APIs

Adding:

"@types/xrm"

provides typing for the Xrm namespace, making interactions with the Dynamics client API much safer.

Accessing the Dynamics Context

Inside a Web Resource, the CRM context can be accessed using:

const formContext = parent.Xrm.Page;

or, in modern implementations:

const globalContext = parent.Xrm.Utility.getGlobalContext();

From there you can:

  • retrieve user information

  • access organization settings

  • execute Dataverse Web API requests

  • navigate to records

  • open dialogs

  • display notifications

Calling the Dataverse Web API

React works very well with the Dataverse Web API.

Typical operations include:

  • RetrieveMultiple

  • Retrieve

  • Create

  • Update

  • Delete

  • Custom Actions

  • Custom APIs

Using async/await makes the code much easier to read than older XMLHttpRequest implementations.

Web Resource vs PCF

One of the most common questions is:

Should I build a Web Resource or a PCF component?

The answer depends on the scenario.

Choose a Web Resource when

  • Building an entire application

  • Creating dashboards

  • Developing preview pages

  • Building administration tools

  • Displaying complex reports

  • Implementing wizard-like interfaces

  • Creating rich dialogs

A Web Resource gives you full control over the page.

Choose PCF when

  • Replacing a form field

  • Creating custom controls

  • Enhancing grids

  • Building reusable UI components

  • Integrating directly with form data

PCF components integrate deeply with the Power Platform lifecycle and are the recommended choice for reusable controls.

Advantages of Web Resources

  • Easier migration from existing JavaScript projects

  • Full React application

  • No PCF lifecycle complexity

  • Complete routing support

  • Freedom to use almost any React library

  • Easier debugging

Advantages of PCF

  • Native Power Platform integration

  • Better form lifecycle support

  • Automatic responsiveness

  • Strong metadata integration

  • Better ALM support

  • Standard deployment model

Deployment Considerations

When deploying React Web Resources, consider the following:

  • Keep filenames stable.

  • Minimize bundle size.

  • Use production builds.

  • Avoid unnecessary dependencies.

  • Separate large components using lazy loading.

  • Keep Fluent UI versions consistent across projects.

  • Test inside different Dynamics apps (Sales, Customer Service, Model-driven Apps).

Performance Tips

To improve performance:

  • Use React.memo where appropriate.

  • Lazy-load large modules.

  • Cache API responses.

  • Reduce unnecessary re-renders.

  • Bundle only the libraries you actually use.

  • Enable production optimizations.

Since Web Resources are loaded inside Dynamics, every kilobyte matters.

Is React Still a Good Choice for Dynamics?

Absolutely.

React remains one of the best technologies for building rich user interfaces inside Dynamics 365.

When combined with TypeScript, Fluent UI, and the Dataverse Web API, it provides an excellent developer experience while producing highly maintainable applications.

PCF is the preferred solution for custom controls embedded directly into forms and grids, but React Web Resources continue to be an excellent option for larger applications, dashboards, and standalone experiences where complete control over the interface is required.

Choosing between Web Resources and PCF should be driven by the type of solution you're building rather than by trends. In many enterprise projects, both approaches coexist successfully: PCF components enhance individual form elements, while React Web Resources deliver sophisticated pages and workflows that extend the capabilities of Dynamics 365.

Arriva QUERY The New HTTP Method for Complex Queries

The web has relied on the same set of HTTP methods for decades: GET, POST, PUT, PATCH, and DELETE. While these methods have proven reliable, modern APIs are increasingly dealing with highly complex search requests that don't fit neatly into the traditional model.

A new proposal, known as QUERY, aims to address this limitation by introducing a dedicated HTTP method specifically designed for complex read-only queries.

Why GET Isn't Always Enough

The GET method is ideal for retrieving resources using simple URL parameters. However, it has several drawbacks when queries become more sophisticated:

  • URLs can become extremely long.

  • Nested filters are difficult to represent.

  • Complex JSON structures cannot be included in the request body.

  • Long URLs may exceed browser or server limits.

For example, searching a large product catalog with dozens of filters quickly becomes impractical using query parameters alone.

Why POST Isn't the Perfect Solution

Many developers solve this problem by using POST requests for searches.

While this works technically, POST was originally intended for operations that create or modify server-side state. Using POST for read-only searches introduces several disadvantages:

  • Caching becomes less efficient.

  • API semantics become less clear.

  • HTTP tooling cannot easily distinguish between read and write operations.

  • Monitoring and optimization become harder.

Enter the QUERY Method

The proposed QUERY HTTP method provides a clean solution.

Like GET, QUERY is intended for safe, read-only operations. Unlike GET, it allows clients to send a request body containing structured data, typically JSON.

Example:

QUERY /products HTTP/1.1
Content-Type: application/json

{
  "category": "laptops",
  "price": {
    "min": 800,
    "max": 2000
  },
  "brands": [
    "Dell",
    "Lenovo",
    "Framework"
  ],
  "sort": "price"
}

This approach makes complex filtering much easier while preserving the semantics of a read-only request.

Main Benefits

The QUERY method offers several advantages:

  • Cleaner API design.

  • Better support for complex filtering.

  • JSON request bodies.

  • Easier integration with modern applications.

  • Potential compatibility with caching mechanisms designed for safe requests.

  • Clear separation between reading and modifying data.

Potential Use Cases

QUERY could become particularly useful for:

  • E-commerce search engines

  • Analytics dashboards

  • Business intelligence platforms

  • Geographic Information Systems (GIS)

  • AI-powered search APIs

  • Graph-like querying without adopting GraphQL

Current Status

It's important to note that QUERY is not yet part of the official HTTP standard and browser, server, and proxy support is still evolving. Developers should verify compatibility before using it in production environments.

Nevertheless, the proposal reflects an important trend: modern web applications increasingly require expressive, structured, and efficient query mechanisms that traditional GET requests cannot easily provide.

As APIs continue to grow in complexity, HTTP itself must evolve. The proposed QUERY method is an elegant attempt to bridge the gap between the simplicity of GET and the flexibility of POST while preserving proper HTTP semantics.

Whether QUERY becomes a widely adopted standard remains to be seen, but it represents an exciting step toward more expressive and developer-friendly web APIs.


How to Enable Text Selection and Right-Click Using JavaScript

Many websites disable text selection, the context menu, or keyboard shortcuts to prevent accidental copying or to customize the user experience. While these restrictions may serve a purpose, they can also interfere with legitimate activities such as taking notes, using translation tools, or accessing browser features.

This article demonstrates a simple JavaScript snippet that restores standard browser interactions by removing common client-side restrictions.


What This Script Does

The script performs several actions:

  • Re-enables text selection.

  • Restores the browser's right-click context menu.

  • Removes a common popup element if it exists.

  • Removes fixed-position overlays that may block page interaction.

  • Prevents page scripts from intercepting common mouse and clipboard events.

Because the script runs entirely in your browser, it only affects the current page during the current browsing session.


JavaScript Code

(() => {
    // Enable text selection
    const style = document.createElement("style");
    style.textContent = `
        * {
            user-select: text !important;
            -webkit-user-select: text !important;
            -moz-user-select: text !important;
            pointer-events: auto !important;
        }
    `;
    document.head.appendChild(style);

    // Remove popup
    document.querySelector("#notRemoverPopup")?.remove();

    // Remove fixed overlays
    document.querySelectorAll("[style*='z-index'], .modal, .overlay").forEach(el => {
        if (getComputedStyle(el).position === "fixed") {
            el.remove();
        }
    });

    // Restore context menu and clipboard events
    ["contextmenu","copy","cut","paste","selectstart","mousedown","mouseup","dragstart"].forEach(evt => {
        window.addEventListener(evt, e => e.stopImmediatePropagation(), true);
    });

    console.log("Restrictions removed.");
})();


How It Works

Restores Text Selection

The script injects CSS rules that override restrictions such as:

  • user-select: none

  • -webkit-user-select: none

This allows text to be highlighted again.

Removes a Popup

Some websites display a popup with the ID:

#notRemoverPopup

The script removes it from the page if it is present.

Removes Fixed Overlays

Many websites display fullscreen overlays using CSS such as:

position: fixed;
z-index: 9999;

The script searches for common overlay elements and removes them if they use fixed positioning.

Restores Browser Events

Websites sometimes intercept events like:

  • contextmenu

  • copy

  • cut

  • paste

  • selectstart

  • mousedown

  • mouseup

  • dragstart

The script stops these interception handlers from taking priority, allowing the browser's default behavior to work normally in many cases.


How to Run the Script

  1. Open the desired webpage.

  2. Press F12 to open Developer Tools.

  3. Select the Console tab.

  4. Paste the JavaScript code.

  5. Press Enter.

The changes apply only to the current page and disappear after a refresh.


Limitations

This technique only affects client-side JavaScript and CSS. It does not bypass server-side protections, authentication, or access controls. Some websites may also reload or recreate interface elements dynamically, requiring the script to be run again.

For developers, students, and power users, browser-side JavaScript can be a useful way to inspect how a page behaves and to restore standard browser functionality during debugging or testing. Understanding how event listeners, CSS properties, and DOM manipulation work is also a great way to learn more about modern web development.

If you frequently use this type of script, consider turning it into a userscript with extensions such as Tampermonkey so it can run automatically on pages where you have permission to use it.



(() => {
    "use strict";

    function hidePopup() {
        const popup = document.getElementById("notRemoverPopup");
        if (popup) {
            popup.remove(); // oppure: popup.style.display = "none";
        }
    }

    function enableRightClick() {
        document.oncontextmenu = null;
        document.onmousedown = null;
        document.onmouseup = null;
        document.onclick = null;

        document.addEventListener("contextmenu", e => {
            e.stopImmediatePropagation();
        }, true);

        document.addEventListener("mousedown", e => {
            if (e.button === 2) {
                e.stopImmediatePropagation();
            }
        }, true);

        document.querySelectorAll("*").forEach(el => {
            el.oncontextmenu = null;
            el.onmousedown = null;
            el.onmouseup = null;
            el.onclick = null;
        });
    }

    hidePopup();
    enableRightClick();

    new MutationObserver(() => {
        hidePopup();
    }).observe(document.body, {
        childList: true,
        subtree: true
    });

    console.log("Popup  off.");
})();


https://www.examtopics.com/discussions/microsoft/view/94167-exam-az-204-topic-1-question-33-discussion

How to Find and Manage Your Connections in XrmToolBox

If you work with Microsoft Dataverse or Dynamics 365, XrmToolBox is undoubtedly your ultimate Swiss Army knife. However, as environments multiply—from development and testing to production—managing your connections can quickly become overwhelming.

Whether you need to move your saved environments to a new computer or you are simply trying to find where XrmToolBox stores your credentials, this quick guide has got you covered.

Finding Connections Inside the App (The Easy Way)

When you launch XrmToolBox, accessing your environments is straightforward:
  • The Connect Button: Look at the top-left corner of the main toolbar. Clicking "Connect" opens the Connection Manager window.
  • Organization: From here, you can select an existing environment, edit connection properties, or click "New Connection" to link a new Dataverse environment using OAuth, Client Id, or Connection Strings.

Where is the Connection File Saved? (The Local Path)

If you are changing your laptop, formatting your PC, or want to share environments with a colleague, you need to find the actual physical files on your hard drive.
XrmToolBox stores your configurations inside the Windows user profile. Here is the exact path:
C:\Users\<YourUsername>\AppData\Roaming\MscrmTools\XrmToolBox\Settings\

Key Files to Look For:

  • ConnectionWizard.xml: This is the most critical file. It contains the list of your saved connections, organization URLs, and configurations.
  • XrmToolBox.Settings.xml: Stores your general application preferences and plugin layouts.
Tip: The AppData folder is hidden by default in Windows. To see it, open File Explorer, click on the View tab at the top, and check the box for Hidden items.

How to Backup or Move Your Connections

Moving your workspace to a new machine takes less than a minute:
  1. Close XrmToolBox on both your old and new computers.
  2. Navigate to the AppData path listed above on your old PC.
  3. Copy the ConnectionWizard.xml file.
  4. Paste it into the exact same folder on your new PC.
  5. Open XrmToolBox on the new machine—all your environments will be ready to go!
Knowing where your XrmToolBox connections live saves time and prevents the headache of re-authenticating dozens of environments. Keep a backup of your ConnectionWizard.xml in a secure place, and you will never lose your setup again.

How to Clone a Dynamics 365 CRM Environment

Managing a Dynamics 365 CRM system requires a safe space for testing. Whether you are deploying a new feature, training your team, or troubleshooting a bug, you should never do it directly in your production environment.
Cloning your environment is the best way to create a perfect sandbox copy. Here is a straightforward guide on how to safely clone your Dynamics 365 CRM environment using the Power Platform Admin Center.

Understand Your Copy Options

Before you click any buttons, you need to decide what kind of clone you need. Microsoft offers two choices:
  • Full Copy: This copies everything. You get all the customizations, schemas, and all the data from your source environment. This is ideal for troubleshooting specific user errors or final user acceptance testing (UAT).
  • Minimal Copy: This copies the structure but leaves the data behind. You get all the tables, workflows, code, and customizations, but no user records or data. This is perfect for starting a new development phase.

Prepare Your Target Environment

You cannot overwrite just any environment. Keep these rules in mind:
  • The target environment must be a Sandbox or Trial environment.
  • You cannot copy directly into a Production environment.
  • Both environments must be in the same geographic region and tenant.

Run the Clone Process

Once you are ready, follow these exact steps:
  1. Log into the Power Platform Admin Center.
  2. Click on Environments in the left-hand menu.
  3. Select the source environment you want to clone and click Copy from the top toolbar.
  4. In the side pane, choose the type of copy (Full or Minimal).
  5. Select the target environment you want to overwrite.
  6. Review your settings and click Copy to start the process.
Note: The environment will be unavailable while the cloning process takes place.

Critical Post-Clone Checklist

Once the clone is complete, the new environment automatically enters Administration Mode. This is a safety feature to prevent the cloned environment from interacting with your real-world systems.
Before you open the environment to your team, make sure to:
  • Turn off Administration Mode: Go to the environment settings to allow regular users to log in.
  • Check Email Router/Server-Side Sync: Disable or update email settings so the test environment doesn't accidentally send automated emails to real clients.
  • Update Integrations: If your CRM connects to SharePoint, Power BI, or external ERP systems, update the connections to point to test folders and test databases, not production ones.
Cloning a Dynamics 365 environment is a quick and powerful way to protect your production data while giving your team the freedom to build and test. By following these steps and double-checking your post-copy integrations, you can ensure a smooth, risk-free development process.

RAG vs Fine-Tuning for Codebases - How to Build Your Own AI Coding Assistant

When you want to train an AI to understand your custom codebase, you face a major decision: Should you use Retrieval-Augmented Generation (RAG) or Fine-Tuning?

Both approaches solve different problems. RAG helps your model find real-time facts in your documentation. Fine-Tuning teaches your model a specific coding style, syntax, or tone.
In this article, we will explore both methods with practical Python examples so you can choose the best one for your developer workflow.

Building a Code-Aware RAG System

Best for: Injecting real-time documentation, specific API keys, or rapidly changing functions into your LLM prompt.
RAG works like an open-book exam. When a developer asks a question, the system searches your documentation, extracts the relevant snippet, and hands it to the Large Language Model (LLM) to generate a precise answer.
Here is how to implement a basic RAG system using LangChain and Chroma DB:
from langchain_community.document_loaders import TextLoader
from langchain_community.vectorstores import Chroma
from langchain_openai import ChatOpenAI, OpenAIEmbeddings

# 1. Load your internal codebase documentation
# Imagine "code_docs.txt" contains: "The calculate_bonus(hours) function multiplies hours by 15 if they exceed 40."
loader = TextLoader("code_docs.txt")
documents = loader.load()

# 2. Initialize the embedding model and index the documents into Chroma DB
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vector_store = Chroma.from_documents(documents, embeddings)

# 3. Configure the retriever to find the most relevant document snippet
retriever = vector_store.as_retriever(search_kwargs={"k": 1})

# 4. Search for relevant context based on the developer's query
user_query = "How do I use the function to calculate the company bonus?"
relevant_docs = retriever.invoke(user_query)
context = relevant_docs.page_content

# 5. Pass the retrieved context and the query to the LLM for a precise answer
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
prompt = f"Use the following codebase context to answer the question:\n{context}\n\nQuestion: {user_query}"
response = llm.invoke(prompt)

print(response.content)

How it works:

  • The Loader: Reads your text-based documentation or source code files.
  • The Vector Store: Converts your code into mathematical vectors to understand semantic meaning.
  • The LLM: Uses the injected context to answer without guessing or hallucinating.

Preparing a Dataset for Fine-Tuning

Best for: Teaching an AI your enterprise coding standards, strict naming conventions, or specific framework styles.
Fine-Tuning is like studying for weeks before an exam. You do not pass documents to the model during the prompt. Instead, you train the model on hundreds of examples so it inherently knows how to write code exactly like your team does.
Before you start training, you must format your code into a JSONL (JSON Lines) file. Here is how to prepare that dataset:
import json

# Define the training dataset matching the conversational format required by OpenAI
training_data = [
    {
        "messages": [
            {"role": "system", "content": "You are the lead AI coding assistant at TechCorp. Always write clean, documented Python code."},
            {"role": "user", "content": "Create a function to validate an email address."},
            {"role": "assistant", "content": "def validate_techcorp_email(email):\n    # TechCorp Standard: Check for basic domain presence\n    return '@techcorp.com' in email"}
        ]
    },
    {
        "messages": [
            {"role": "system", "content": "You are the lead AI coding assistant at TechCorp. Always write clean, documented Python code."},
            {"role": "user", "content": "Write a loop to print active user names."},
            {"role": "assistant", "content": "for user in active_users:\n    # TechCorp Standard: Log formatted username\n    print(f'Active User: {user}')"}
        ]
    }
]

# Save the dataset into a JSONL file ready for the fine-tuning API upload
with open("fine_tuning_dataset.jsonl", "w", encoding="utf-8") as file:
    for record in training_data:
        file.write(json.dumps(record, ensure_ascii=False) + "\n")

print("Success! 'fine_tuning_dataset.jsonl' is ready for upload to your LLM provider.")

How it works:

  • System Prompt: Sets the permanent identity and behavioral constraints of your custom model.
  • User/Assistant Pairs: Show the exact syntax, comments, and structure your company expects.
  • JSONL Output: This specific file structure can be directly uploaded to OpenAI, Hugging Face, or fireworks.ai to start the training job.

RAG vs Fine-Tuning: Which should you choose?

  • Choose RAG if: Your codebase changes daily, you have thousands of pages of documentation, and you need 100% accurate factual retrieval.
  • Choose Fine-Tuning if: Your code follows unique architectural patterns, you want to eliminate repetitive system prompts, or you need to train a smaller model to perform like a massive one.
For the ultimate setup, many engineering teams combine both: they fine-tune a model to write code in their specific language syntax, and use RAG to feed that model the latest documentation dynamically.

Solving Dynamics 365 Plugin Trace Log Truncation with a Buffered Tracing Service

A production issue occurs, Plugin Trace Log is enabled, and after reproducing the problem you open the trace...

...only to discover that the beginning of the execution has disappeared.

The reason is simple: Plugin Trace Log has a maximum size of approximately 10 KB (10,240 characters).

Once that limit is exceeded, Dynamics 365 does not stop logging. Instead, it silently discards the oldest trace entries and keeps only the most recent ones.

For simple plugins this isn't an issue.

For complex plugins with multiple validation steps, service calls, and business logic, losing the beginning of the trace often means losing the most useful information.

In one of our projects we solved this problem by introducing a custom implementation of ITracingService that buffers all trace messages and decides what should eventually be written to Dynamics.

Understanding the Default Behavior

Imagine a plugin producing a trace like this:

Plugin started
Reading configuration
Loading Target
Checking security
Loading related records
...
...
...
Calling external API
Updating records
Plugin completed

When the trace exceeds 10 KB, Dynamics transforms it into something similar to:

...
...
...
Calling external API
Updating records
Plugin completed

The entire startup sequence disappears.

Unfortunately that's exactly the part we usually need while debugging.

The Solution

Instead of writing directly to the platform tracing service, every message is first stored in memory.

Only at the end of the execution do we decide how to write it.

The implementation has two responsibilities:

  • buffer every trace message

  • generate an optimized trace that fits inside Dynamics limits

Additionally, for selected plugins, the entire trace can also be persisted into a custom Dataverse table.

The architecture looks like this:

Plugin
      │
      ▼
BufferedTracingService
      │
      ├────────► Custom Log Entity (complete log)
      │
      ▼
Optimized Trace
      │
      ▼
Dynamics Plugin Trace Log

Buffering the Trace

Instead of immediately calling the original tracing service, every message is stored inside an internal collection.

private readonly List<string> _buffer = new();

public void Trace(string format, params object[] args)
{
    var message = args?.Length > 0
        ? string.Format(format, args)
        : format;

    _buffer.Add(message);
}

Nothing is written immediately.

The plugin continues executing exactly as before.

Keeping the Most Useful Information

Simply cutting the trace after 9 KB wouldn't solve the problem.

We would still lose the final exception or the last executed operations.

Instead, the algorithm keeps:

  • the beginning

  • the end

  • removes only the middle section

The resulting trace becomes:

Plugin started
Reading configuration
Loading configuration
Checking user privileges

...[TRACE TRUNCATED]...

Updating child records
Calling external service
Plugin completed successfully

This is significantly more valuable because it contains both the initial execution context and the final operations.

Persisting the Complete Log

Sometimes even the optimized trace is not enough.

For particularly critical plugins we also wanted the entire execution log.

Instead of storing every plugin trace, only selected plugins are configured for persistence.

private static readonly HashSet<string> _pluginsToLog =
{
    "OnPostCreateOnPostUpdateSetManagement"
};

When one of these plugins finishes execution, the complete buffered log is stored inside a custom Dataverse table.

var logRecord = new Entity("dps_log")
{
    ["dps_description"] = fullLog,
    ["dps_userid"] = new EntityReference(
        "systemuser",
        userId)
};

_orgService.Create(logRecord);

This gives us an unlimited execution history while keeping the standard Plugin Trace Log readable.

Integrating with Existing Plugins

One of the nicest aspects of this approach is that existing plugin code doesn't change.

Instead of resolving the standard tracing service:

this.tracingService = new Lazy<ITracingService>(
    Get<ITracingService>);

we simply wrap it.

this.tracingService = new Lazy<ITracingService>(() =>
    new BufferedTracingService(
        Get<ITracingService>(),
        OrganizationServiceFactory.CreateOrganizationService(null),
        GetType().Name,
        Context.PrimaryEntityId));

Every existing call remains exactly the same.

context.TracingService.Trace("Processing Account {0}", account.Id);

No refactoring of business logic is required.

Flushing the Buffer

At the very end of plugin execution, the buffer is flushed.

(context.TracingService as BufferedTracingService)?.Flush();

I usually place this call inside the finally block of my custom PluginBase.

This guarantees that traces are written even when an exception is thrown.

Benefits

Dynamics 365's 10 KB Plugin Trace Log limit is unlikely to disappear anytime soon, but that doesn't mean we have to accept losing valuable diagnostic information.

A buffered tracing service gives us complete control over what gets preserved, making debugging much easier without changing existing plugin code.

In practice, this utility has become part of my standard plugin framework because it solves one of the most common frustrations when troubleshooting long-running Dataverse plugins.

Complete Source Code

Below is the full implementation of the BufferedTracingService used throughout this article.

The following implementation can be copied directly into your plugin framework. Adapt the custom log entity (dps_log) and fields to match your own Dataverse solution.

using Microsoft.Xrm.Sdk;
using System;
using System.Collections.Generic;
using System.Text;

namespace Avanade
{
    public sealed class BufferedTracingService : ITracingService
    {
        private readonly ITracingService _inner;
        private readonly IOrganizationService _orgService;
        private readonly string _pluginName;
        private readonly Guid _triggerEntityId;
        private readonly List<string> _buffer = new List<string>();

        private const int MaxD365TraceBytes = 9500;
        private const string TruncationMarker = "\r\n...[TRACE TRUNCATED]...\r\n";

        public BufferedTracingService(
            ITracingService inner,
            IOrganizationService orgService,
            string pluginName,
            Guid triggerEntityId)
        {
            _inner = inner;
            _orgService = orgService;
            _pluginName = pluginName;
            _triggerEntityId = triggerEntityId;
        }

        public void Trace(string format, params object[] args)
        {
            var message = (args != null && args.Length > 0)
                ? string.Format(format, args)
                : format;
            _buffer.Add(message);
        }

        private static readonly HashSet<string> _pluginsToLog = new HashSet<string>
        {
            "OnPostCreateOnPostUpdateSetManagement",          
        };

        public void Flush()
        {
            if (_buffer.Count == 0) return;

            if (_pluginsToLog.Contains(_pluginName))
            {
                var fullLog = string.Join("\r\n", _buffer);
                try
                {
                    var logRecord = new Entity("dps_log")
                    {
                        ["dps_description"] = fullLog,
                        ["dps_userid"] = new EntityReference("systemuser", new Guid("ba9e89ba-017a-f111-bc81-00224889f7d8")),
                    };
                    _orgService.Create(logRecord);
                }
                catch { }
            }

            _inner.Trace(BuildTruncatedTrace());
        }

        private string BuildTruncatedTrace()
        {
            var encoding = Encoding.UTF8;
            int headBudget = 3000;
            int totalBudget = MaxD365TraceBytes;
            int markerBytes = encoding.GetByteCount(TruncationMarker);

            var headSb = new StringBuilder();
            int headBytes = 0;
            int headEndIndex = _buffer.Count;

            for (int i = 0; i < _buffer.Count; i++)
            {
                var line = _buffer[i] + "\r\n";
                int lb = encoding.GetByteCount(line);
                if (headBytes + lb > headBudget) { headEndIndex = i; break; }
                headSb.Append(line);
                headBytes += lb;
                headEndIndex = i + 1;
            }

            if (headEndIndex >= _buffer.Count)
                return headSb.ToString();

            var tailLines = new List<string>();
            int tailBytes = 0;
            int tailBudget = totalBudget - headBytes - markerBytes;
            int tailStartIndex = _buffer.Count;

            for (int i = _buffer.Count - 1; i >= headEndIndex; i--)
            {
                var line = _buffer[i] + "\r\n";
                int lb = encoding.GetByteCount(line);
                if (tailBytes + lb > tailBudget) break;
                tailLines.Insert(0, _buffer[i]);
                tailBytes += lb;
                tailStartIndex = i;
            }

            var sb = new StringBuilder();
            sb.Append(headSb);
            if (tailStartIndex > headEndIndex) sb.Append(TruncationMarker);
            foreach (var l in tailLines) sb.AppendLine(l);
            return sb.ToString();
        }
    }
}

PluginContext

this.tracingService = new Lazy<ITracingService>(() =>
    new BufferedTracingService(
        Get<ITracingService>(),
        this.OrganizationServiceFactory.CreateOrganizationService(null),
        this.GetType().Name,
        this.Context.PrimaryEntityId));

PluginBase

try
{
    ExecutePlugin(context);
}
finally
{
    (context.TracingService as BufferedTracingService)?.Flush();
}

The implementation is intentionally lightweight, requires virtually no changes to existing plugins, and dramatically improves the debugging experience. In production environments where traces frequently exceed the Dynamics 365 limit, this small utility quickly proves its value. It has become a standard component of my Dataverse plugin framework because it preserves the information that matters most when diagnosing complex issues.