Merging Word Documents in Dynamics 365 Using Only Built-In Features

 Below is a straightforward approach to merge two Word documents in Dynamics 365—without resorting to third-party paid connectors or extensions. By leveraging a custom Action, a Power Automate flow, and a plugin with OpenXML (embedded via ILRepack), you can seamlessly combine Word files and store the merged result, all in one out-of-the-box solution.


1. Create a Custom Action in Dynamics 365

  • In your D365 solution, add a new Action (e.g., new_mergeaction).
  • Define two input parameters as string fields, Document1 and Document2.
  • Define one output parameter as a string field, MergedDocument.
  • This Action will be called by the plugin to perform the merge.

2. Create a Power Automate Flow

  1. Add Get file content (SharePoint) actions to retrieve both Word documents.
  2. Insert a Perform an unbound/bound action step (Dataverse connector) targeting the Action from step 1:
    • Map Document1 to @{body('Get_file_content_1')}.
        for example:  outputs('Get_file_content')?['body']['$content']
    • Map Document2 to @{body('Get_file_content_2')}.

  3. The Action responds with MergedDocument (base64).

3. Develop the Plugin and Register It on the Action

Use a plugin that:

  • Accepts Document1 and Document2 from InputParameters.
  • Converts them from base64 (or parses "$content" if full JSON).
  • Merges the docs via OpenXML, adding a page break in between.
  • Returns base64 in MergedDocument.

Below is a simplified example (text representation):

using System;
using System.IO;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Wordprocessing;
using Microsoft.Xrm.Sdk;
using Newtonsoft.Json.Linq;

namespace WordDocumentsMerge
{
    public class Merge : IPlugin
    {
        public void Execute(IServiceProvider serviceProvider)
        {
            var context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));

            if (!context.InputParameters.Contains("Document1") || !context.InputParameters.Contains("Document2"))
            {
                throw new InvalidPluginExecutionException("Missing docs");
            }

            string doc1Raw = context.InputParameters["Document1"].ToString();
            string doc2Raw = context.InputParameters["Document2"].ToString();

            string doc1Json = NormalizeInput(doc1Raw);
            string doc2Json = NormalizeInput(doc2Raw);

            string base64Doc1 = ExtractBase64IfJson(doc1Json);
            string base64Doc2 = ExtractBase64IfJson(doc2Json);

            byte[] doc1Bytes = Convert.FromBase64String(base64Doc1);
            byte[] doc2Bytes = Convert.FromBase64String(base64Doc2);
            byte[] mergedBytes = MergeDocs(doc1Bytes, doc2Bytes);

            context.OutputParameters["MergedDocument"] = Convert.ToBase64String(mergedBytes);
        }

        private string NormalizeInput(string input)
        {
            input = input.Trim();
            if (input.StartsWith("\"") && input.EndsWith("\""))
            {
                input = input.Substring(1, input.Length - 2);
                input = input.Replace("\\\"", "\"");
            }
            return input;
        }

        private string ExtractBase64IfJson(string input)
        {
            if (!input.StartsWith("{"))
            {
                return input;
            }
            JObject obj = JObject.Parse(input);
            var content = (string)obj["$content"];
            if (string.IsNullOrEmpty(content))
            {
                throw new InvalidPluginExecutionException("No '$content' found.");
            }
            return content;
        }

        private byte[] MergeDocs(byte[] file1, byte[] file2)
        {
            using (var ms = new MemoryStream())
            {
                ms.Write(file1, 0, file1.Length);
                ms.Position = 0;

                using (var mainDoc = WordprocessingDocument.Open(ms, true))
                {
                    var body = mainDoc.MainDocumentPart.Document.Body;

                    // Insert a page break before appending the second doc
                    var pageBreak = new Paragraph(
                        new Run(
                            new Break { Type = BreakValues.Page }
                        )
                    );
                    body.Append(pageBreak);

                    var altId = "AltChunkId";
                    var altPart = mainDoc.MainDocumentPart.AddAlternativeFormatImportPart(
                        AlternativeFormatImportPartType.WordprocessingML, altId);

                    using (var ms2 = new MemoryStream(file2))
                    {
                        altPart.FeedData(ms2);
                    }

                    body.Append(new AltChunk { Id = altId });
                    mainDoc.MainDocumentPart.Document.Save();
                }

                return ms.ToArray();
            }
        }
    }
}

The key merging logic:

  1. Open the first doc in a MemoryStream (expandable).
  2. Insert a page break.
  3. Use AltChunk to append the second doc.
  4. Save and return base64.

4. Merge OpenXML with Your Plugin via ILRepack

Because D365 runs plugins in a sandbox, external references like OpenXML must be embedded in your assembly. Use a command like:

"C:\WHSC\packages\ILRepack.2.0.36\tools\ILRepack.exe" /out:C:\WHSC\WordDocumentsMerge\bin\Debug\Merge.dll /keyfile:C:\WHSC\WordDocumentsMerge\WordDocumentsMerge.snk C:\WHSC\WordDocumentsMerge\bin\Debug\WordDocumentsMerge.dll C:\WHSC\WordDocumentsMerge\bin\Debug\DocumentFormat.OpenXml.dll

Then register Merge.dll in the Plugin Registration Tool on your custom Action.


5. Receive and Store the Merged File in Power Automate

  • The Action call returns MergedDocument in base64.
  • Add a Create file step (SharePoint) to store the final Word file


Conclusion
With these steps, you can merge two Word documents entirely through standard Microsoft-provided features in Dynamics 365 and Power Automate—no extra costs or third-party extensions required. The combination of a custom Action, a plugin merged with OpenXML, and ILRepack ensures a secure, sandbox-compatible solution that leverages only out-of-the-box functionality.

No comments:

Post a Comment