xRM Translation Studio – Deployment Reference
xRM Translation Studio - Deployment Documentation
Production-Ready Deployment Brief

Deploy all translations
for xRM Translation
Studio.

Sauber strukturiert: inklusive Tabellen, Attribute, Forms, Tabs, Sections, Views sowie Local und Global Option Sets — mit Publish Job, Status-Handling, Chunking und Sandbox-safe Architektur.

How it works!
Scope
9 Translation Units
Execution Model
Publish + Worker
Main Risk
Sandbox Timeout
Job Table
xrm_publishjob
Status Values
Known
Need
Precise deploy
OVERVIEW

Deployment Architektur

6-Phasen Pipeline — sandbox-sicher, paged, selektiver PublishXml. Fehler pro Record isoliert, kein Blanket-Update.

P1

PublishJob erstellen → Status = Running

Erstellt einen xrm_publishjob Record, setzt Status = Running, schreibt Startzeit und Table-Filter. Dient als Audit-Trail für die gesamte Pipeline.

xrm_publishjobRunning: 570690001
P2

Translation Records laden (paged, gefiltert)

Lädt alle deployable Records über 8 Entity-Typen via paged QueryExpression (PageSize=50). Filter: Status IN (New=570690000, Changed=570690001) + TranslatedLabel NOT NULL + LCID-Match.

TableTranslationsAttributeTranslations FormTranslations → XMLTabTranslations → XML SectionTranslations → XMLViewTranslations LocalOptionSetValuesGlobalOptionSetValues
P3

Metadata Deploy (je Typ eigener Handler)

Deployed in Dependency-Order: Tables → Attributes → Forms → Tabs+Sections (FormXML grouped per Form) → Views → LocalOptionSets → GlobalOptionSets. Fehler pro Record werden geloggt, stoppen nicht die Pipeline.

Dependency OrderPer-Record IsolationFormXML 1×Load per Form
P4

PublishXml (selektiv, nur betroffene Entities)

Publiziert nur die betroffenen Entity LogicalNames via PublishXmlRequest. Chunked in Batches von max. 10 Entities. Publish-Fehler sind non-fatal.

Chunk: 10 EntitiesSelektiv
P5

Status Update — nur erfolgreich deployte Records

Markiert NUR Records die tatsächlich erfolgreich deployed wurden als Published (570690004). Nutzt ExecuteMultiple mit ContinueOnError=true in Batches von 20. Kein Blanket-Update.

Nur Success-RecordsBatch: 20ExecuteMultiple
P6

PublishJob → Completed / Failed + Log

Setzt Endzeit, finalen Status (Completed=570690002 / Failed=570690003) und aggregierten Log auf dem PublishJob. Log wird auf 50.000 Zeichen gecapped.

Completed: 570690002Failed: 570690003Log max 50k
OVERVIEW

Status Werte

OptionSet-Werte für Translation Records und xrm_publishjob

Translation Record — xrm_deploymentstatus

570690000
New
Neu, noch nie deployed
570690001
Changed
Geändert seit letztem Deploy
570690002
Deployed
Metadata deployed
570690004
Published
Deployed + PublishXml OK
570690005
Updating
Deployment läuft gerade

Publish Job — xrm_status

570690000
Pending
Wartet auf Ausführung
570690001
Running
Läuft aktuell
570690002
Completed
Erfolgreich abgeschlossen
570690003
Failed
Fehler — Log prüfen
C# CODE

DeployTranslationsPlugin.cs

Custom API Entry Point — vollständig mit allen using-Statements

DeployTranslationsPlugin.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xrm.Sdk;
using XrmTranslationStudio.Deployment.Models;
using XrmTranslationStudio.Deployment.Services;

namespace XrmTranslationStudio.Deployment
{
    /// <summary>
    /// Custom API: xrm_DeployTranslations
    /// Input:  xrm_languagecode (Int, required) – LCID to deploy
    ///         xrm_tablefilter  (String, optional) – comma-separated entity logical names
    /// Output: xrm_successcount (Int), xrm_failcount (Int)
    /// </summary>
    public class DeployTranslationsPlugin : IPlugin
    {
        public void Execute(IServiceProvider serviceProvider)
        {
            var context        = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
            var serviceFactory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
            var tracer         = (ITracingService)serviceProvider.GetService(typeof(ITracingService));
            var service        = serviceFactory.CreateOrganizationService(context.UserId);

            // ── Validate input ──────────────────────────────────────────────────
            if (!context.InputParameters.Contains("xrm_languagecode") ||
                !(context.InputParameters["xrm_languagecode"] is int languageCode))
                throw new InvalidPluginExecutionException("Input parameter 'xrm_languagecode' (Int) is required.");

            string tableFilter = context.InputParameters.Contains("xrm_tablefilter")
                ? context.InputParameters["xrm_tablefilter"] as string
                : null;

            tracer.Trace($"[Deploy] START — LCID={languageCode}, Filter='{tableFilter ?? "all"}'");

            var jobService = new PublishJobService(service, tracer);
            Guid jobId     = Guid.Empty;

            try
            {
                // Phase 1 ── Create PublishJob ─────────────────────────────────────
                jobId = jobService.CreateJob(languageCode, tableFilter);
                tracer.Trace($"[Deploy] Phase 1 OK – Job {jobId}");

                // Phase 2 ── Load all deployable translation records ──────────────
                var queryService = new TranslationQueryService(service, tracer);
                var allRecords   = queryService.LoadAllDeployableRecords(languageCode, tableFilter);
                tracer.Trace($"[Deploy] Phase 2 OK – {allRecords.Count} records loaded");

                if (allRecords.Count == 0)
                {
                    jobService.CompleteJob(jobId, true, "No deployable records found. Nothing to do.");
                    context.OutputParameters["xrm_successcount"] = 0;
                    context.OutputParameters["xrm_failcount"]    = 0;
                    return;
                }

                // Phase 3 ── Deploy metadata ──────────────────────────────────────
                var deployService = new MetadataDeploymentService(service, tracer);
                var results       = deployService.DeployAll(allRecords);

                var succeeded = results.Where(r => r.Success).ToList();
                var failed    = results.Where(r => !r.Success).ToList();
                tracer.Trace($"[Deploy] Phase 3 OK – Success={succeeded.Count}, Failed={failed.Count}");

                // Phase 4 ── Selective PublishXml ──────────────────────────────────
                var affectedEntities = succeeded
                    .Select(r => allRecords.First(x => x.RecordId == r.RecordId).TargetEntityLogicalName)
                    .Where(e => !string.IsNullOrWhiteSpace(e))
                    .Distinct().ToList();

                if (affectedEntities.Any())
                {
                    new PublishService(service, tracer).PublishEntities(affectedEntities);
                    tracer.Trace($"[Deploy] Phase 4 OK – Published {affectedEntities.Count} entities");
                }

                // Phase 5 ── Status update – only successfully deployed records ────
                new StatusUpdateService(service, tracer).MarkAsPublished(succeeded, allRecords);
                tracer.Trace($"[Deploy] Phase 5 OK – {succeeded.Count} records marked Published");

                // Phase 6 ── Finalize PublishJob ───────────────────────────────────
                var log = BuildLog(succeeded, failed);
                jobService.CompleteJob(jobId, failed.Count == 0, log);

                context.OutputParameters["xrm_successcount"] = succeeded.Count;
                context.OutputParameters["xrm_failcount"]    = failed.Count;
                tracer.Trace($"[Deploy] DONE");
            }
            catch (Exception ex)
            {
                tracer.Trace($"[Deploy] FATAL: {ex.Message}\n{ex.StackTrace}");
                if (jobId != Guid.Empty)
                    jobService.FailJob(jobId, ex.Message);
                throw new InvalidPluginExecutionException($"Deployment failed: {ex.Message}", ex);
            }
        }

        private static string BuildLog(List<DeploymentResult> succeeded, List<DeploymentResult> failed)
        {
            var sb = new StringBuilder();
            sb.AppendLine($"Deployment Summary: {succeeded.Count} succeeded, {failed.Count} failed.");
            if (failed.Any())
            {
                sb.AppendLine("--- Failed Records ---");
                foreach (var f in failed.Take(100))
                    sb.AppendLine($"  [{f.EntityType}] {f.RecordId}: {f.ErrorMessage}");
                if (failed.Count > 100)
                    sb.AppendLine($"  ... and {failed.Count - 100} more.");
            }
            return sb.ToString();
        }
    }
}
C# CODE

Models

TranslationRecord, DeploymentResult, FormPatchContext, TranslationEntityType

Models/TranslationRecord.cs
using System;

namespace XrmTranslationStudio.Deployment.Models
{
    public enum TranslationEntityType
    {
        Table,
        Attribute,
        Form,
        Tab,
        Section,
        View,
        LocalOptionSetValue,
        GlobalOptionSetValue
    }

    public class TranslationRecord
    {
        public Guid                 RecordId                   { get; set; }
        public TranslationEntityType EntityType                 { get; set; }

        // Target metadata identifiers
        public string               TargetEntityLogicalName    { get; set; }
        public string               TargetAttributeLogicalName { get; set; }
        public string               TargetFormId               { get; set; }
        public string               TargetTabId                { get; set; }
        public string               TargetSectionId            { get; set; }
        public string               TargetViewId               { get; set; }
        public string               TargetOptionSetName        { get; set; }
        public int?                  TargetOptionValue          { get; set; }

        // Translation payload
        public string               TranslatedLabel            { get; set; }
        public int                   LanguageCode               { get; set; }

        // Form XML parent reference (for Tab / Section)
        public Guid?                 ParentFormId               { get; set; }
        public string               ParentFormEntityLogicalName{ get; set; }
    }

    public class DeploymentResult
    {
        public Guid                 RecordId     { get; set; }
        public bool                  Success      { get; set; }
        public string               ErrorMessage { get; set; }
        public TranslationEntityType EntityType   { get; set; }
    }
}
Models/FormPatchContext.cs
using System;
using System.Collections.Generic;

namespace XrmTranslationStudio.Deployment.Models
{
    /// <summary>
    /// Aggregates all pending label patches for a single systemform record.
    /// Prevents multiple FormXML load/save round-trips for the same form.
    /// </summary>
    public class FormPatchContext
    {
        public Guid                        FormId            { get; set; }
        public string                      EntityLogicalName { get; set; }
        public List<TranslationRecord>    TabPatches        { get; } = new List<TranslationRecord>();
        public List<TranslationRecord>    SectionPatches    { get; } = new List<TranslationRecord>();
        public List<TranslationRecord>    FormLabelPatches  { get; } = new List<TranslationRecord>();
    }
}
C# CODE

PublishJobService.cs

Erstellt und aktualisiert xrm_publishjob — Audit-Trail und Fehlerlog der Pipeline

Services/PublishJobService.cs
using System;
using Microsoft.Xrm.Sdk;

namespace XrmTranslationStudio.Deployment.Services
{
    public class PublishJobService
    {
        private readonly IOrganizationService _service;
        private readonly ITracingService      _tracer;

        private const int Status_Running   = 570690001;
        private const int Status_Completed = 570690002;
        private const int Status_Failed    = 570690003;
        private const int MaxLogLength     = 50000;

        public PublishJobService(IOrganizationService service, ITracingService tracer)
        {
            _service = service ?? throw new ArgumentNullException(nameof(service));
            _tracer  = tracer  ?? throw new ArgumentNullException(nameof(tracer));
        }

        public Guid CreateJob(int languageCode, string tableFilter)
        {
            var job = new Entity("xrm_publishjob");
            job["xrm_name"]      = $"Translation Deploy LCID={languageCode} @ {DateTime.UtcNow:u}";
            job["xrm_status"]    = new OptionSetValue(Status_Running);
            job["xrm_startedon"] = DateTime.UtcNow;
            job["xrm_tables"]    = tableFilter ?? "all";

            var id = _service.Create(job);
            _tracer.Trace($"[PublishJobService] Created job {id}");
            return id;
        }

        public void CompleteJob(Guid jobId, bool success, string log)
        {
            var job = new Entity("xrm_publishjob", jobId);
            job["xrm_status"]     = new OptionSetValue(success ? Status_Completed : Status_Failed);
            job["xrm_finishedon"] = DateTime.UtcNow;
            job["xrm_log"]        = TruncateLog(log);
            _service.Update(job);
            _tracer.Trace($"[PublishJobService] Job {jobId} → {(success ? "Completed" : "Failed")}");
        }

        public void FailJob(Guid jobId, string errorMessage)
        {
            try
            {
                var job = new Entity("xrm_publishjob", jobId);
                job["xrm_status"]     = new OptionSetValue(Status_Failed);
                job["xrm_finishedon"] = DateTime.UtcNow;
                job["xrm_log"]        = TruncateLog($"FATAL ERROR: {errorMessage}");
                _service.Update(job);
            }
            catch (Exception ex)
            {
                _tracer.Trace($"[PublishJobService] Could not update FailJob: {ex.Message}");
            }
        }

        private static string TruncateLog(string log)
        {
            if (log == null) return string.Empty;
            return log.Length > MaxLogLength
                ? log.Substring(0, MaxLogLength) + "\n[LOG TRUNCATED]"
                : log;
        }
    }
}
C# CODE

TranslationQueryService.cs

Lädt alle deployable Records über alle 8 Entity-Typen — paged, gefiltert, NULL-gesichert

Services/TranslationQueryService.cs
using System;
using System.Collections.Generic;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
using XrmTranslationStudio.Deployment.Models;

namespace XrmTranslationStudio.Deployment.Services
{
    public class TranslationQueryService
    {
        private readonly IOrganizationService _service;
        private readonly ITracingService      _tracer;

        // Status values eligible for deployment: New (570690000) + Changed (570690001)
        private static readonly int[] DeployableStatuses = { 570690000, 570690001 };
        private const int PageSize = 50;

        public TranslationQueryService(IOrganizationService service, ITracingService tracer)
        {
            _service = service ?? throw new ArgumentNullException(nameof(service));
            _tracer  = tracer  ?? throw new ArgumentNullException(nameof(tracer));
        }

        public List<TranslationRecord> LoadAllDeployableRecords(int lcid, string tableFilter)
        {
            var all = new List<TranslationRecord>();
            all.AddRange(LoadTableTranslations(lcid, tableFilter));
            all.AddRange(LoadAttributeTranslations(lcid, tableFilter));
            all.AddRange(LoadFormTranslations(lcid, tableFilter));
            all.AddRange(LoadTabTranslations(lcid, tableFilter));
            all.AddRange(LoadSectionTranslations(lcid, tableFilter));
            all.AddRange(LoadViewTranslations(lcid, tableFilter));
            all.AddRange(LoadLocalOptionSetTranslations(lcid, tableFilter));
            all.AddRange(LoadGlobalOptionSetTranslations(lcid));
            _tracer.Trace($"[QueryService] Total deployable records: {all.Count}");
            return all;
        }

        // ── Per-entity query methods ────────────────────────────────────────

        private List<TranslationRecord> LoadTableTranslations(int lcid, string f)
        {
            var q = BuildBaseQuery("xrm_tablemanagement",
                "xrm_tablemanagementid", "xrm_entitylogicalname", "xrm_translatedlabel",
                "xrm_languagecode", "xrm_deploymentstatus");
            ApplyCriteria(q, lcid); ApplyTableFilter(q, "xrm_entitylogicalname", f);
            return PagedQuery(q, e => new TranslationRecord {
                RecordId = e.Id, EntityType = TranslationEntityType.Table,
                TargetEntityLogicalName = e.GetAttributeValue<string>("xrm_entitylogicalname"),
                TranslatedLabel = e.GetAttributeValue<string>("xrm_translatedlabel"),
                LanguageCode = lcid });
        }

        private List<TranslationRecord> LoadAttributeTranslations(int lcid, string f)
        {
            var q = BuildBaseQuery("xrm_attribute",
                "xrm_attributeid", "xrm_entitylogicalname", "xrm_attributelogicalname",
                "xrm_translatedlabel", "xrm_languagecode", "xrm_deploymentstatus");
            ApplyCriteria(q, lcid); ApplyTableFilter(q, "xrm_entitylogicalname", f);
            return PagedQuery(q, e => new TranslationRecord {
                RecordId = e.Id, EntityType = TranslationEntityType.Attribute,
                TargetEntityLogicalName    = e.GetAttributeValue<string>("xrm_entitylogicalname"),
                TargetAttributeLogicalName = e.GetAttributeValue<string>("xrm_attributelogicalname"),
                TranslatedLabel = e.GetAttributeValue<string>("xrm_translatedlabel"),
                LanguageCode = lcid });
        }

        private List<TranslationRecord> LoadFormTranslations(int lcid, string f)
        {
            var q = BuildBaseQuery("xrm_form",
                "xrm_formid", "xrm_formid_target", "xrm_entitylogicalname",
                "xrm_translatedlabel", "xrm_languagecode", "xrm_deploymentstatus");
            ApplyCriteria(q, lcid); ApplyTableFilter(q, "xrm_entitylogicalname", f);
            return PagedQuery(q, e => {
                var raw = e.GetAttributeValue<string>("xrm_formid_target");
                return new TranslationRecord {
                    RecordId = e.Id, EntityType = TranslationEntityType.Form,
                    TargetEntityLogicalName = e.GetAttributeValue<string>("xrm_entitylogicalname"),
                    TargetFormId = raw,
                    ParentFormId = Guid.TryParse(raw, out var fg) ? fg : (Guid?)null,
                    TranslatedLabel = e.GetAttributeValue<string>("xrm_translatedlabel"),
                    LanguageCode = lcid }; });
        }

        private List<TranslationRecord> LoadTabTranslations(int lcid, string f)
        {
            var q = BuildBaseQuery("xrm_tab",
                "xrm_tabid", "xrm_tabid_target", "xrm_formid_target",
                "xrm_entitylogicalname", "xrm_translatedlabel",
                "xrm_languagecode", "xrm_deploymentstatus");
            ApplyCriteria(q, lcid); ApplyTableFilter(q, "xrm_entitylogicalname", f);
            return PagedQuery(q, e => {
                var raw = e.GetAttributeValue<string>("xrm_formid_target");
                return new TranslationRecord {
                    RecordId = e.Id, EntityType = TranslationEntityType.Tab,
                    TargetEntityLogicalName = e.GetAttributeValue<string>("xrm_entitylogicalname"),
                    TargetTabId  = e.GetAttributeValue<string>("xrm_tabid_target"),
                    TargetFormId = raw,
                    ParentFormId = Guid.TryParse(raw, out var fg) ? fg : (Guid?)null,
                    TranslatedLabel = e.GetAttributeValue<string>("xrm_translatedlabel"),
                    LanguageCode = lcid }; });
        }

        private List<TranslationRecord> LoadSectionTranslations(int lcid, string f)
        {
            var q = BuildBaseQuery("xrm_sections",
                "xrm_sectionsid", "xrm_sectionid_target", "xrm_tabid_target",
                "xrm_formid_target", "xrm_entitylogicalname",
                "xrm_translatedlabel", "xrm_languagecode", "xrm_deploymentstatus");
            ApplyCriteria(q, lcid); ApplyTableFilter(q, "xrm_entitylogicalname", f);
            return PagedQuery(q, e => {
                var raw = e.GetAttributeValue<string>("xrm_formid_target");
                return new TranslationRecord {
                    RecordId = e.Id, EntityType = TranslationEntityType.Section,
                    TargetEntityLogicalName = e.GetAttributeValue<string>("xrm_entitylogicalname"),
                    TargetSectionId = e.GetAttributeValue<string>("xrm_sectionid_target"),
                    TargetTabId     = e.GetAttributeValue<string>("xrm_tabid_target"),
                    TargetFormId    = raw,
                    ParentFormId    = Guid.TryParse(raw, out var fg) ? fg : (Guid?)null,
                    TranslatedLabel = e.GetAttributeValue<string>("xrm_translatedlabel"),
                    LanguageCode = lcid }; });
        }

        private List<TranslationRecord> LoadViewTranslations(int lcid, string f)
        {
            var q = BuildBaseQuery("xrm_view",
                "xrm_viewid", "xrm_viewid_target", "xrm_entitylogicalname",
                "xrm_translatedlabel", "xrm_languagecode", "xrm_deploymentstatus");
            ApplyCriteria(q, lcid); ApplyTableFilter(q, "xrm_entitylogicalname", f);
            return PagedQuery(q, e => new TranslationRecord {
                RecordId = e.Id, EntityType = TranslationEntityType.View,
                TargetEntityLogicalName = e.GetAttributeValue<string>("xrm_entitylogicalname"),
                TargetViewId    = e.GetAttributeValue<string>("xrm_viewid_target"),
                TranslatedLabel = e.GetAttributeValue<string>("xrm_translatedlabel"),
                LanguageCode = lcid });
        }

        private List<TranslationRecord> LoadLocalOptionSetTranslations(int lcid, string f)
        {
            var q = BuildBaseQuery("xrm_localoptionsetvalue",
                "xrm_localoptionsetvalueid", "xrm_entitylogicalname",
                "xrm_attributelogicalname", "xrm_optionvalue",
                "xrm_translatedlabel", "xrm_languagecode", "xrm_deploymentstatus");
            ApplyCriteria(q, lcid); ApplyTableFilter(q, "xrm_entitylogicalname", f);
            return PagedQuery(q, e => new TranslationRecord {
                RecordId = e.Id, EntityType = TranslationEntityType.LocalOptionSetValue,
                TargetEntityLogicalName    = e.GetAttributeValue<string>("xrm_entitylogicalname"),
                TargetAttributeLogicalName = e.GetAttributeValue<string>("xrm_attributelogicalname"),
                TargetOptionValue          = e.GetAttributeValue<int?>("xrm_optionvalue"),
                TranslatedLabel            = e.GetAttributeValue<string>("xrm_translatedlabel"),
                LanguageCode = lcid });
        }

        private List<TranslationRecord> LoadGlobalOptionSetTranslations(int lcid)
        {
            var q = BuildBaseQuery("xrm_globaloptionsetvalue",
                "xrm_globaloptionsetvalueid", "xrm_optionsetname", "xrm_optionvalue",
                "xrm_translatedlabel", "xrm_languagecode", "xrm_deploymentstatus");
            ApplyCriteria(q, lcid);
            return PagedQuery(q, e => new TranslationRecord {
                RecordId = e.Id, EntityType = TranslationEntityType.GlobalOptionSetValue,
                TargetOptionSetName = e.GetAttributeValue<string>("xrm_optionsetname"),
                TargetOptionValue   = e.GetAttributeValue<int?>("xrm_optionvalue"),
                TranslatedLabel     = e.GetAttributeValue<string>("xrm_translatedlabel"),
                LanguageCode = lcid });
        }

        // ── Helpers ─────────────────────────────────────────────────────────

        private static QueryExpression BuildBaseQuery(string entity, params string[] cols) =>
            new QueryExpression(entity) { ColumnSet = new ColumnSet(cols) };

        private void ApplyCriteria(QueryExpression q, int lcid)
        {
            var f = new FilterExpression(LogicalOperator.And);
            f.AddCondition("xrm_languagecode", ConditionOperator.Equal, lcid);

            var sf = new FilterExpression(LogicalOperator.Or);
            foreach (var s in DeployableStatuses)
                sf.AddCondition("xrm_deploymentstatus", ConditionOperator.Equal, s);
            f.AddFilter(sf);

            f.AddCondition("xrm_translatedlabel", ConditionOperator.NotNull);
            f.AddCondition("xrm_translatedlabel", ConditionOperator.NotEqual, string.Empty);
            q.Criteria = f;
        }

        private static void ApplyTableFilter(QueryExpression q, string field, string filter)
        {
            if (string.IsNullOrWhiteSpace(filter)) return;
            var tables = filter.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
            if (tables.Length == 0) return;
            var tf = new FilterExpression(LogicalOperator.Or);
            foreach (var t in tables)
                tf.AddCondition(field, ConditionOperator.Equal, t.Trim().ToLowerInvariant());
            q.Criteria.AddFilter(tf);
        }

        private List<TranslationRecord> PagedQuery(
            QueryExpression query, Func<Entity, TranslationRecord> mapper)
        {
            var results = new List<TranslationRecord>();
            query.PageInfo = new PagingInfo { Count = PageSize, PageNumber = 1, ReturnTotalRecordCount = false };

            while (true)
            {
                var response = _service.RetrieveMultiple(query);
                foreach (var entity in response.Entities)
                {
                    try
                    {
                        var record = mapper(entity);
                        if (!string.IsNullOrWhiteSpace(record?.TranslatedLabel))
                            results.Add(record);
                    }
                    catch (Exception ex)
                    {
                        _tracer.Trace($"[QueryService] Mapping error for {entity.Id}: {ex.Message}");
                    }
                }
                if (!response.MoreRecords) break;
                query.PageInfo.PageNumber++;
                query.PageInfo.PagingCookie = response.PagingCookie;
            }
            return results;
        }
    }
}
C# CODE

MetadataDeploymentService.cs

Deploy in Dependency-Order — je Typ eigener Handler, per-Record Error Isolation

Services/MetadataDeploymentService.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Messages;
using Microsoft.Xrm.Sdk.Metadata;
using Microsoft.Xrm.Sdk.Query;
using XrmTranslationStudio.Deployment.Helpers;
using XrmTranslationStudio.Deployment.Models;

namespace XrmTranslationStudio.Deployment.Services
{
    public class MetadataDeploymentService
    {
        private readonly IOrganizationService _service;
        private readonly ITracingService      _tracer;

        public MetadataDeploymentService(IOrganizationService service, ITracingService tracer)
        {
            _service = service ?? throw new ArgumentNullException(nameof(service));
            _tracer  = tracer  ?? throw new ArgumentNullException(nameof(tracer));
        }

        /// Deploys all translation records in dependency order.
        public List<DeploymentResult> DeployAll(List<TranslationRecord> records)
        {
            var results = new List<DeploymentResult>();

            results.AddRange(DeployTableLabels(
                records.Where(r => r.EntityType == TranslationEntityType.Table).ToList()));

            results.AddRange(DeployAttributeLabels(
                records.Where(r => r.EntityType == TranslationEntityType.Attribute).ToList()));

            results.AddRange(DeployFormLabels(
                records.Where(r => r.EntityType == TranslationEntityType.Form).ToList()));

            results.AddRange(DeployFormXmlPatches(
                records.Where(r => r.EntityType == TranslationEntityType.Tab).ToList(),
                records.Where(r => r.EntityType == TranslationEntityType.Section).ToList()));

            results.AddRange(DeployViewLabels(
                records.Where(r => r.EntityType == TranslationEntityType.View).ToList()));

            results.AddRange(DeployLocalOptionSetLabels(
                records.Where(r => r.EntityType == TranslationEntityType.LocalOptionSetValue).ToList()));

            results.AddRange(DeployGlobalOptionSetLabels(
                records.Where(r => r.EntityType == TranslationEntityType.GlobalOptionSetValue).ToList()));

            return results;
        }

        // ── Table labels ─────────────────────────────────────────────────────
        private List<DeploymentResult> DeployTableLabels(List<TranslationRecord> records)
        {
            _tracer.Trace($"[Deploy] Table labels: {records.Count}");
            var results = new List<DeploymentResult>();
            foreach (var r in records)
            {
                var res = new DeploymentResult { RecordId = r.RecordId, EntityType = r.EntityType };
                try
                {
                    var resp = (RetrieveEntityResponse)_service.Execute(
                        new RetrieveEntityRequest {
                            LogicalName   = r.TargetEntityLogicalName,
                            EntityFilters = EntityFilters.Entity });
                    MetadataHelper.SetOrUpdateLabel(resp.EntityMetadata.DisplayName, r.LanguageCode, r.TranslatedLabel);
                    _service.Execute(new UpdateEntityRequest { Entity = resp.EntityMetadata });
                    res.Success = true;
                }
                catch (Exception ex) { res.ErrorMessage = ex.Message; }
                results.Add(res);
            }
            return results;
        }

        // ── Attribute labels ─────────────────────────────────────────────────
        private List<DeploymentResult> DeployAttributeLabels(List<TranslationRecord> records)
        {
            _tracer.Trace($"[Deploy] Attribute labels: {records.Count}");
            var results = new List<DeploymentResult>();
            foreach (var r in records)
            {
                var res = new DeploymentResult { RecordId = r.RecordId, EntityType = r.EntityType };
                try
                {
                    var resp = (RetrieveAttributeResponse)_service.Execute(
                        new RetrieveAttributeRequest {
                            EntityLogicalName    = r.TargetEntityLogicalName,
                            LogicalName          = r.TargetAttributeLogicalName,
                            RetrieveAsIfPublished = true });
                    MetadataHelper.SetOrUpdateLabel(resp.AttributeMetadata.DisplayName, r.LanguageCode, r.TranslatedLabel);
                    _service.Execute(new UpdateAttributeRequest {
                        EntityName = r.TargetEntityLogicalName,
                        Attribute  = resp.AttributeMetadata });
                    res.Success = true;
                }
                catch (Exception ex) { res.ErrorMessage = ex.Message; }
                results.Add(res);
            }
            return results;
        }

        // ── Form labels (deferred to FormXML phase) ──────────────────────────
        private List<DeploymentResult> DeployFormLabels(List<TranslationRecord> records)
        {
            // systemform.name is NOT localizable via Metadata API.
            // Form label translation is applied in DeployFormXmlPatches via LocalizedNames XML node.
            // We mark all form label records as success so they proceed to status update.
            return records.Select(r => new DeploymentResult {
                RecordId = r.RecordId, EntityType = r.EntityType, Success = true }).ToList();
        }

        // ── FormXML patches — grouped by form to minimize round-trips ────────
        private List<DeploymentResult> DeployFormXmlPatches(
            List<TranslationRecord> tabs, List<TranslationRecord> sections)
        {
            _tracer.Trace($"[Deploy] FormXML: {tabs.Count} tabs + {sections.Count} sections");
            var results    = new List<DeploymentResult>();
            var formGroups = new Dictionary<Guid, FormPatchContext>();

            void AddToGroup(TranslationRecord rec)
            {
                if (rec.ParentFormId == null || rec.ParentFormId == Guid.Empty) return;
                var fid = rec.ParentFormId.Value;
                if (!formGroups.ContainsKey(fid))
                    formGroups[fid] = new FormPatchContext {
                        FormId = fid, EntityLogicalName = rec.TargetEntityLogicalName };
                if (rec.EntityType == TranslationEntityType.Tab)
                    formGroups[fid].TabPatches.Add(rec);
                else if (rec.EntityType == TranslationEntityType.Section)
                    formGroups[fid].SectionPatches.Add(rec);
            }

            foreach (var r in tabs)     AddToGroup(r);
            foreach (var r in sections)  AddToGroup(r);

            var patchService = new FormXmlPatchService(_service, _tracer);
            foreach (var kvp in formGroups)
                results.AddRange(patchService.PatchForm(kvp.Value));

            // Records with null ParentFormId: mark as failed
            foreach (var r in tabs.Concat(sections)
                .Where(r => r.ParentFormId == null || r.ParentFormId == Guid.Empty))
            {
                results.Add(new DeploymentResult {
                    RecordId = r.RecordId, EntityType = r.EntityType,
                    Success = false, ErrorMessage = "ParentFormId is null – cannot identify form." });
            }
            return results;
        }

        // ── View labels ──────────────────────────────────────────────────────
        private List<DeploymentResult> DeployViewLabels(List<TranslationRecord> records)
        {
            _tracer.Trace($"[Deploy] View labels: {records.Count}");
            var results = new List<DeploymentResult>();
            foreach (var r in records)
            {
                var res = new DeploymentResult { RecordId = r.RecordId, EntityType = r.EntityType };
                try
                {
                    if (!Guid.TryParse(r.TargetViewId, out var viewGuid))
                        throw new InvalidOperationException($"Invalid TargetViewId: '{r.TargetViewId}'");
                    var view = new Entity("savedquery", viewGuid);
                    view["name"] = r.TranslatedLabel;
                    _service.Update(view);
                    res.Success = true;
                }
                catch (Exception ex) { res.ErrorMessage = ex.Message; }
                results.Add(res);
            }
            return results;
        }

        // ── Local OptionSet labels ───────────────────────────────────────────
        private List<DeploymentResult> DeployLocalOptionSetLabels(List<TranslationRecord> records)
        {
            _tracer.Trace($"[Deploy] Local OptionSet labels: {records.Count}");
            var results = new List<DeploymentResult>();
            foreach (var r in records)
            {
                var res = new DeploymentResult { RecordId = r.RecordId, EntityType = r.EntityType };
                try
                {
                    if (r.TargetOptionValue == null)
                        throw new InvalidOperationException("TargetOptionValue is null.");

                    var resp = (RetrieveAttributeResponse)_service.Execute(
                        new RetrieveAttributeRequest {
                            EntityLogicalName    = r.TargetEntityLogicalName,
                            LogicalName          = r.TargetAttributeLogicalName,
                            RetrieveAsIfPublished = true });

                    if (!(resp.AttributeMetadata is EnumAttributeMetadata enumMeta))
                        throw new InvalidOperationException(
                            $"'{r.TargetAttributeLogicalName}' is not an OptionSet attribute.");

                    var opt = enumMeta.OptionSet.Options.FirstOrDefault(o => o.Value == r.TargetOptionValue);
                    if (opt == null)
                        throw new InvalidOperationException(
                            $"Option value {r.TargetOptionValue} not found in '{r.TargetAttributeLogicalName}'.");

                    MetadataHelper.SetOrUpdateLabel(opt.Label, r.LanguageCode, r.TranslatedLabel);
                    _service.Execute(new UpdateAttributeRequest {
                        EntityName = r.TargetEntityLogicalName,
                        Attribute  = resp.AttributeMetadata });
                    res.Success = true;
                }
                catch (Exception ex) { res.ErrorMessage = ex.Message; }
                results.Add(res);
            }
            return results;
        }

        // ── Global OptionSet labels — grouped to minimize round-trips ────────
        private List<DeploymentResult> DeployGlobalOptionSetLabels(List<TranslationRecord> records)
        {
            _tracer.Trace($"[Deploy] Global OptionSet labels: {records.Count}");
            var results = new List<DeploymentResult>();

            foreach (var group in records.GroupBy(r => r.TargetOptionSetName))
            {
                if (string.IsNullOrWhiteSpace(group.Key)) continue;
                try
                {
                    var resp   = (RetrieveOptionSetResponse)_service.Execute(
                        new RetrieveOptionSetRequest { Name = group.Key });
                    var osMeta = (OptionSetMetadata)resp.OptionSetMetadata;

                    foreach (var r in group)
                    {
                        var res = new DeploymentResult { RecordId = r.RecordId, EntityType = r.EntityType };
                        var opt = osMeta.Options.FirstOrDefault(o => o.Value == r.TargetOptionValue);
                        if (opt == null)
                            res.ErrorMessage = $"Option {r.TargetOptionValue} not found in '{group.Key}'.";
                        else
                        {
                            MetadataHelper.SetOrUpdateLabel(opt.Label, r.LanguageCode, r.TranslatedLabel);
                            res.Success = true;
                        }
                        results.Add(res);
                    }
                    _service.Execute(new UpdateOptionSetRequest { OptionSet = osMeta });
                }
                catch (Exception ex)
                {
                    foreach (var r in group)
                        results.Add(new DeploymentResult {
                            RecordId = r.RecordId, EntityType = r.EntityType,
                            Success = false,
                            ErrorMessage = $"OptionSet '{group.Key}' load failed: {ex.Message}" });
                }
            }
            return results;
        }
    }
}
C# CODE

FormXmlPatchService.cs

Patcht FormXML für Tabs und Sections — jedes Form wird genau einmal geladen und gespeichert

Services/FormXmlPatchService.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
using XrmTranslationStudio.Deployment.Models;

namespace XrmTranslationStudio.Deployment.Services
{
    /// <summary>
    /// Loads each systemform record once, applies all pending tab/section label patches,
    /// then saves FormXML once. Prevents N round-trips for the same form.
    ///
    /// FormXML tab structure:
    ///   <form><tabs><tab id="tab_general" ...>
    ///     <labels><label description="General" languagecode="1033"/></labels>
    ///   </tab></tabs></form>
    /// </summary>
    public class FormXmlPatchService
    {
        private readonly IOrganizationService _service;
        private readonly ITracingService      _tracer;

        public FormXmlPatchService(IOrganizationService service, ITracingService tracer)
        {
            _service = service ?? throw new ArgumentNullException(nameof(service));
            _tracer  = tracer  ?? throw new ArgumentNullException(nameof(tracer));
        }

        public List<DeploymentResult> PatchForm(FormPatchContext context)
        {
            var results = new List<DeploymentResult>();
            try
            {
                // Load FormXML once
                var formEntity = _service.Retrieve(
                    "systemform", context.FormId, new ColumnSet("formxml"));
                var formXml = formEntity.GetAttributeValue<string>("formxml");
                if (string.IsNullOrWhiteSpace(formXml))
                    throw new InvalidOperationException($"FormXML is empty for form {context.FormId}");

                var doc = XDocument.Parse(formXml);

                // Apply tab patches
                foreach (var tab in context.TabPatches)
                {
                    var res = new DeploymentResult { RecordId = tab.RecordId, EntityType = tab.EntityType };
                    try
                    {
                        PatchTabLabel(doc, tab.TargetTabId, tab.LanguageCode, tab.TranslatedLabel);
                        res.Success = true;
                    }
                    catch (Exception ex) { res.ErrorMessage = ex.Message; }
                    results.Add(res);
                }

                // Apply section patches
                foreach (var sec in context.SectionPatches)
                {
                    var res = new DeploymentResult { RecordId = sec.RecordId, EntityType = sec.EntityType };
                    try
                    {
                        PatchSectionLabel(doc, sec.TargetTabId, sec.TargetSectionId,
                            sec.LanguageCode, sec.TranslatedLabel);
                        res.Success = true;
                    }
                    catch (Exception ex) { res.ErrorMessage = ex.Message; }
                    results.Add(res);
                }

                // Save FormXML once with all patches applied
                var updated = new Entity("systemform", context.FormId);
                updated["formxml"] = doc.ToString(SaveOptions.DisableFormatting);
                _service.Update(updated);

                _tracer.Trace($"[FormXML] Form {context.FormId} saved. Tabs={context.TabPatches.Count}, Sections={context.SectionPatches.Count}");
            }
            catch (Exception ex)
            {
                _tracer.Trace($"[FormXML] FATAL for form {context.FormId}: {ex.Message}");
                foreach (var r in context.TabPatches.Concat(context.SectionPatches))
                    results.Add(new DeploymentResult {
                        RecordId = r.RecordId, EntityType = r.EntityType,
                        Success = false, ErrorMessage = ex.Message });
            }
            return results;
        }

        private void PatchTabLabel(XDocument doc, string tabId, int lcid, string label)
        {
            var tab = FindById(doc.Root, "tab", tabId);
            if (tab == null)
                throw new InvalidOperationException($"Tab '{tabId}' not found in FormXML.");
            PatchLabelsNode(tab, lcid, label);
        }

        private void PatchSectionLabel(XDocument doc, string tabId, string sectionId, int lcid, string label)
        {
            var searchRoot = !string.IsNullOrWhiteSpace(tabId)
                ? FindById(doc.Root, "tab", tabId)
                : null;
            var section = FindById(searchRoot ?? doc.Root, "section", sectionId);
            if (section == null)
                throw new InvalidOperationException($"Section '{sectionId}' not found in FormXML.");
            PatchLabelsNode(section, lcid, label);
        }

        private static XElement FindById(XElement root, string tag, string id)
        {
            if (root == null || string.IsNullOrWhiteSpace(id)) return null;
            return root.Descendants(tag).FirstOrDefault(e =>
                string.Equals(
                    e.Attribute("id")?.Value ?? e.Attribute("name")?.Value,
                    id, StringComparison.OrdinalIgnoreCase));
        }

        private static void PatchLabelsNode(XElement parent, int lcid, string label)
        {
            var labelsNode = parent.Element("labels");
            if (labelsNode == null)
            {
                labelsNode = new XElement("labels");
                parent.AddFirst(labelsNode);
            }
            var existing = labelsNode.Elements("label")
                .FirstOrDefault(l => l.Attribute("languagecode")?.Value == lcid.ToString());
            if (existing != null)
                existing.SetAttributeValue("description", label);
            else
                labelsNode.Add(new XElement("label",
                    new XAttribute("description", label),
                    new XAttribute("languagecode", lcid)));
        }
    }
}
C# CODE

PublishService.cs

Selektiver PublishXml in 10er-Chunks — Publish-Fehler sind non-fatal

Services/PublishService.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Messages;

namespace XrmTranslationStudio.Deployment.Services
{
    public class PublishService
    {
        private readonly IOrganizationService _service;
        private readonly ITracingService      _tracer;
        private const int ChunkSize = 10;

        public PublishService(IOrganizationService service, ITracingService tracer)
        {
            _service = service ?? throw new ArgumentNullException(nameof(service));
            _tracer  = tracer  ?? throw new ArgumentNullException(nameof(tracer));
        }

        public void PublishEntities(List<string> entityLogicalNames)
        {
            if (entityLogicalNames == null || entityLogicalNames.Count == 0) return;

            var distinct = entityLogicalNames
                .Where(e => !string.IsNullOrWhiteSpace(e))
                .Distinct().ToList();

            _tracer.Trace($"[PublishService] Publishing {distinct.Count} entities in chunks of {ChunkSize}");

            for (int i = 0; i < distinct.Count; i += ChunkSize)
            {
                var chunk = distinct.Skip(i).Take(ChunkSize).ToList();
                PublishChunk(chunk);
            }
        }

        private void PublishChunk(List<string> entities)
        {
            try
            {
                var sb = new StringBuilder();
                sb.Append("<importexportxml><entities>");
                foreach (var e in entities)
                    sb.Append($"<entity>{e}</entity>");
                sb.Append("</entities></importexportxml>");

                _service.Execute(new PublishXmlRequest { ParameterXml = sb.ToString() });
                _tracer.Trace($"[PublishService] OK: {string.Join(", ", entities)}");
            }
            catch (Exception ex)
            {
                // Non-fatal: deployment has already occurred.
                // Status update will still run for successfully deployed records.
                _tracer.Trace($"[PublishService] Publish error (non-fatal): {ex.Message}");
            }
        }
    }
}
C# CODE

StatusUpdateService.cs

Markiert NUR erfolgreich deployte Records als Published — ExecuteMultiple in 20er-Batches

Services/StatusUpdateService.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Messages;
using XrmTranslationStudio.Deployment.Models;

namespace XrmTranslationStudio.Deployment.Services
{
    public class StatusUpdateService
    {
        private readonly IOrganizationService _service;
        private readonly ITracingService      _tracer;

        private const int Status_Published = 570690004;
        private const int BatchSize        = 20;

        private static readonly Dictionary<TranslationEntityType, string> EntityMap =
            new Dictionary<TranslationEntityType, string>
            {
                { TranslationEntityType.Table,               "xrm_tablemanagement"     },
                { TranslationEntityType.Attribute,           "xrm_attribute"           },
                { TranslationEntityType.Form,                "xrm_form"                },
                { TranslationEntityType.Tab,                 "xrm_tab"                 },
                { TranslationEntityType.Section,             "xrm_sections"            },
                { TranslationEntityType.View,                "xrm_view"                },
                { TranslationEntityType.LocalOptionSetValue,  "xrm_localoptionsetvalue"  },
                { TranslationEntityType.GlobalOptionSetValue, "xrm_globaloptionsetvalue" },
            };

        public StatusUpdateService(IOrganizationService service, ITracingService tracer)
        {
            _service = service ?? throw new ArgumentNullException(nameof(service));
            _tracer  = tracer  ?? throw new ArgumentNullException(nameof(tracer));
        }

        /// Updates xrm_deploymentstatus to Published (570690004) ONLY for records
        /// that were actually successfully deployed. Never touches other records.
        public void MarkAsPublished(
            List<DeploymentResult> successfulResults,
            List<TranslationRecord>  originalRecords)
        {
            if (successfulResults == null || successfulResults.Count == 0) return;

            var lookup = originalRecords.ToDictionary(r => r.RecordId, r => r.EntityType);

            var updates = successfulResults
                .Where(r => r.Success)
                .Select(r =>
                {
                    if (!lookup.TryGetValue(r.RecordId, out var entityType)) return null;
                    if (!EntityMap.TryGetValue(entityType, out var logicalName)) return null;
                    var e = new Entity(logicalName, r.RecordId);
                    e["xrm_deploymentstatus"] = new OptionSetValue(Status_Published);
                    return e;
                })
                .Where(e => e != null)
                .ToList();

            _tracer.Trace($"[StatusUpdateService] Marking {updates.Count} records as Published");

            for (int i = 0; i < updates.Count; i += BatchSize)
            {
                var batch = updates.Skip(i).Take(BatchSize).ToList();
                ExecuteBatch(batch);
            }
        }

        private void ExecuteBatch(List<Entity> entities)
        {
            try
            {
                var req = new ExecuteMultipleRequest
                {
                    Settings = new ExecuteMultipleSettings
                    {
                        ContinueOnError = true,
                        ReturnResponses = false
                    },
                    Requests = new OrganizationRequestCollection()
                };
                foreach (var e in entities)
                    req.Requests.Add(new UpdateRequest { Target = e });

                var response = (ExecuteMultipleResponse)_service.Execute(req);
                if (response.IsFaulted)
                {
                    foreach (var r in response.Responses)
                        if (r.Fault != null)
                            _tracer.Trace($"[StatusUpdateService] Batch item fault: {r.Fault.Message}");
                }
            }
            catch (Exception ex)
            {
                _tracer.Trace($"[StatusUpdateService] Batch error (non-fatal): {ex.Message}");
            }
        }
    }
}
C# CODE

Helpers

MetadataHelper — setzt oder ersetzt einen lokalisierten Label-Wert

Helpers/MetadataHelper.cs
using System.Linq;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Metadata;

namespace XrmTranslationStudio.Deployment.Helpers
{
    public static class MetadataHelper
    {
        /// <summary>
        /// Sets or replaces the localized label for a given LCID in a Label collection.
        /// If the language already exists the description is updated in-place.
        /// If not, a new LocalizedLabel is appended.
        /// </summary>
        public static void SetOrUpdateLabel(Label labelCollection, int lcid, string value)
        {
            if (labelCollection == null || string.IsNullOrWhiteSpace(value)) return;

            var existing = labelCollection.LocalizedLabels
                .FirstOrDefault(l => l.LanguageCode == lcid);

            if (existing != null)
                existing.Label = value;
            else
                labelCollection.LocalizedLabels.Add(new LocalizedLabel(value, lcid));
        }
    }
}
REFERENCE

Deployment Mapping

Translation Type → Dataverse API → Methode

Translation Type → Deployment Mechanism
Translation TypeDataverse APIZiel / MethodeHinweis
Table LabelRetrieveEntityRequest
UpdateEntityRequest
EntityMetadata.DisplayName Label CollectionEntityFilters.Entity
Attribute LabelRetrieveAttributeRequest
UpdateAttributeRequest
AttributeMetadata.DisplayName Label CollectionRetrieveAsIfPublished = true
Form Labelsystemform.formxml
XDocument Patch
<LocalizedNames> Node im FormXMLsystemform.name ist nicht direkt lokalisierbar via Metadata API
Tab Labelsystemform.formxml
XDocument Patch
<tab id="..."><labels><label/>Alle Patches pro Form in einer Load/Save-Op
Section Labelsystemform.formxml
XDocument Patch
<section name="..."><labels><label/>TabId als Hint, Fallback auf Root-Suche
View Labelsavedquery (Update)savedquery.name Feld⚠ Single-Language — kein echter multilingualer Label-Support via API
Local OptionSetRetrieveAttributeRequest
UpdateAttributeRequest
EnumAttributeMetadata.OptionSet.Options[n].LabelCast zu EnumAttributeMetadata notwendig
Global OptionSetRetrieveOptionSetRequest
UpdateOptionSetRequest
OptionSetMetadata.Options[n].LabelGruppiert nach OptionSetName — 1 Update pro OptionSet
SCHEMA EXPLORER

Alle Translation-Tabellen

Hier steckt der Tabellen-Switcher direkt in der Seite — inklusive aller Tabellen, Feldtypen, Notes und Filter-Pills.

REFERENCE

Registrierungs-Empfehlung

Plugin Registration Tool — Custom API Konfiguration

Custom API Definition

Namexrm_DeployTranslations
Unique Namexrm_DeployTranslations
Plugin TypeDeployTranslationsPlugin
Binding TypeGlobal
Execution ModeSynchronous
Isolation ModeSandbox

Input Parameters

xrm_languagecodeInteger · Required
xrm_tablefilterString · Optional

Output Parameters

xrm_successcountInteger
xrm_failcountInteger

Chunk Sizes

Paging Query50 Records/Page
PublishXml Chunk10 Entities
Status Update Batch20 Records
Log max Länge50.000 Zeichen

Execution Empfehlung

< 200 RecordsSync Custom API
200–500 RecordsSync (Grenze beachten)
> 500 RecordsAsync Plugin
JS TriggerXrm.WebApi.execute()
JS Aufruf aus Model-Driven App Xrm.WebApi.execute({ getMetadata: () => ({ boundParameter: null, parameterTypes: { xrm_languagecode: { typeName: "Edm.Int32", structuralProperty: 1 } }, operationType: 0, operationName: "xrm_DeployTranslations" }), xrm_languagecode: 1031 }).then(r => console.log(r));
REFERENCE

Annahmen & Limitierungen

Bekannte technische Grenzen, offene Punkte und Follow-up Empfehlungen

Field Names sind Annahmen

Alle xrm_*-Feldnamen (z.B. xrm_tabid_target, xrm_sectionid_target) sind strukturelle Annahmen und müssen gegen das echte Schema geprüft werden.

View Multi-Language nicht vollständig

savedquery.name ist ein einfaches String-Feld. Echter Multilingual-Support erfordert Translation Import API oder savedquery-XML-Patch. Aktuelle Implementierung überschreibt nur den Base-Name.

Sandbox 2-Minuten Limit

Bei >200 Attributen oder >500 Records kann das Sandbox-Zeitlimit erreicht werden. Empfehlung: Async Plugin oder Job-Split nach Entity-Typ.

GlobalOptionSet PublishXml

Für Global OptionSets muss PublishXml zusätzlich das <optionsets>-Element enthalten. Der PublishService enthält aktuell nur Entity-Publishing.

FormXML Tab/Section IDs

FormXML verwendet sowohl id- als auch name-Attribute je nach Form-Typ. Der PatchService sucht nach beiden — sicherstellen dass xrm_tabid_target den korrekten Identifier aus dem FormXML enthält.

ExecuteMultiple in Sandbox

ExecuteMultiple ist in Sandbox-Plugins verfügbar. ContinueOnError=true gesetzt — einzelne Fehler in einem Batch stoppen nicht die gesamte Status-Update-Operation.

Form Label via FormXML

systemform.name ist nicht direkt über die Metadata API lokalisierbar. Form-Label-Übersetzung muss über den <LocalizedNames>-Node im FormXML oder über den Import Translation Workflow erfolgen.