同步术语组示例 SharePoint 外接程序

Core.MMSSync 示例演示如何使用提供程序托管的外接程序来同步源和目标分类。 此外接程序同步托管元数据服务中的两个术语库-源术语库和目标术语库。

以下对象用于同步术语组:

  • TermStore
  • ChangeInformation

如果需要执行以下操作,请使用此解决方案:

  • 同步两个分类。 例如,可以将 SharePoint Online 和 SharePoint Server 内部部署用于不同的术语集,但它们使用相同的分类。
  • 同步仅对特定术语组所做的更改。

准备工作

若要开始,请从 GitHub 上的 Office 365 开发人员模式和做法项目下载 Core.MMSSync 示例外接程序。

注意

本文中的代码按原样提供,不提供任何明示或暗示的担保,包括对特定用途适用性、适销性或不侵权的默示担保。

运行此外接程序之前,您需要具有访问 Managed Metadata Service 中的术语存储的权限。 下图展示了分配这些权限的 Office 365 管理中心。

显示 SharePoint 管理中心的屏幕截图,具有突出显示的术语库、分类术语库搜索框和术语库管理员框。

分配对术语库的权限:

  1. 从 Office 365 管理中心中,选择“术语库”。

  2. 在“分类术语库”中,选择你希望向其分配管理员权限的术语集。

  3. 在“术语库管理员”中,输入需要术语库管理员权限的组织帐户。

使用 Core.MMSSync 示例外接程序

在启动外接程序时,将显示 Core.MMSSync 控制台应用程序,如下图所示。 系统将提示你输入以下信息:

  • 包含源术语存储的 Office 365 管理中心的 URL(这是源 Managed Metadata Service 的 URL)。 例如,您可以输入 https://contososource-admin.sharepoint.com
  • 你的源 Managed Metadata Service 上的术语库管理员的用户名和密码。
  • 包含目标术语库的 Office 365 管理中心的 URL(这是目标 MMS 的 URL)。 例如,可以输入 https://contosotarget-admin.sharepoint.com
  • 你的目标 Managed Metadata Service 上的术语库管理员的用户名和密码。
  • 您想要执行的操作的类型。 您可以:
    • 使用 TermStore 对象移动术语组(方案 1)。
    • 使用 ChangeInformation 对象处理更改(方案 2)。

重要

此示例外接程序适用于 SharePoint Online 和本地 SharePoint Server 2013。

提示要输入的信息的控制台应用的屏幕截图。

选择方案后,输入要从源 Managed Metadata Service 同步到目标 Managed Metadata Service 的术语组的名称,如下图所示。 例如,可以输入 Enterprise

分类术语库下拉列表的屏幕截图。

方案 1 - 移动术语组

当你选择“移动术语组”时,外接程序将提示你输入要同步的术语组,然后调用 MMSSyncManager.cs 中的 CopyNewTermGroups 方法。 然后 CopyNewTermGroups 执行下列操作,将术语组从源术语库复制到目标术语库:

  1. 检索源和目标术语存储对象。

  2. 验证源和目标术语库的语言相匹配。

  3. 确认源术语组在目标术语库中不存在,然后使用 CreateNewTargetTermGroup 将源术语组复制到目标术语库。

可以设置 TermGroupExclusionsTermGroupToCopyTermSetInclusions 参数来筛选要处理的术语。

下面的代码展示了 MMSSyncManager.cs 中的 CopyNewTermGroupsCreateNewTargetTermGroup 方法。

public bool CopyNewTermGroups(ClientContext sourceContext, ClientContext targetContext, List<string> termGroupExclusions = null, string termGroupToCopy = null)
        {
            TermStore sourceTermStore = GetTermStoreObject(sourceContext);
            TermStore targetTermStore = GetTermStoreObject(targetContext);

            
            List<int> languagesToProcess = null;
            if (!ValidTermStoreLanguages(sourceTermStore, targetTermStore, out languagesToProcess))
            {
                Log.Internal.TraceError((int)EventId.LanguageMismatch, "The target termstore default language is not available as language in the source term store, syncing cannot proceed.");
                return false;
            }

            // Get a list of term groups to process. Exclude site collection-scoped groups and system groups.
            IEnumerable<TermGroup> termGroups = sourceContext.LoadQuery(sourceTermStore.Groups.Include(g => g.Name,
                                                                                                       g => g.Id,
                                                                                                       g => g.IsSiteCollectionGroup,
                                                                                                       g => g.IsSystemGroup))
                                                                                              .Where(g => g.IsSystemGroup == false &amp;&amp; g.IsSiteCollectionGroup == false);
            sourceContext.ExecuteQuery();

            foreach (TermGroup termGroup in termGroups)
            {
                // Skip term group if you only want to copy one particular term group.
                if (!String.IsNullOrEmpty(termGroupToCopy))
                {
                    if (!termGroup.Name.Equals(termGroupToCopy, StringComparison.InvariantCultureIgnoreCase))
                    {
                        continue;
                    }
                }

                // Skip term groups that you do not want to copy.
                if (termGroupExclusions != null &amp;&amp; termGroupExclusions.Contains(termGroup.Name, StringComparer.InvariantCultureIgnoreCase))
                {
                    Log.Internal.TraceInformation((int)EventId.CopyTermGroup_Skip, "Skipping {0} as this is a system termgroup", termGroup.Name);
                    continue;
                }

                // About to start copying a term group.
                TermGroup sourceTermGroup = GetTermGroup(sourceContext, sourceTermStore, termGroup.Name);
                TermGroup targetTermGroup = GetTermGroup(targetContext, targetTermStore, termGroup.Name);

                if (sourceTermGroup == null)
                {
                    continue;
                }
                if (targetTermGroup != null)
                {
                    if (sourceTermGroup.Id != targetTermGroup.Id)
                    {
                        // Term group exists with a different ID, unable to sync.
                        Log.Internal.TraceWarning((int)EventId.CopyTermGroup_IDMismatch, "The term groups have different ID's. I don't know how to work it.");
                    }
                    else
                    {
                        // Do nothing as this term group was previously copied. Term group changes need to be 
                        // picked up by the change log processing.
                        Log.Internal.TraceInformation((int)EventId.CopyTermGroup_AlreadyCopied, "Termgroup {0} was already copied...changes to it will need to come from changelog processing.", termGroup.Name);
                    }
                }
                else
                {
                    Log.Internal.TraceInformation((int)EventId.CopyTermGroup_Copying, "Copying termgroup {0}...", termGroup.Name);
                    this.CreateNewTargetTermGroup(sourceContext, targetContext, sourceTermGroup, targetTermStore, languagesToProcess);
                }
            }

            return true;
        }



private void CreateNewTargetTermGroup(ClientContext sourceClientContext, ClientContext targetClientContext, TermGroup sourceTermGroup, TermStore targetTermStore, List<int> languagesToProcess)
        {
            TermGroup destinationTermGroup = targetTermStore.CreateGroup(sourceTermGroup.Name, sourceTermGroup.Id);
            if (!string.IsNullOrEmpty(sourceTermGroup.Description))
            {
                destinationTermGroup.Description = sourceTermGroup.Description;
            }

            TermSetCollection sourceTermSetCollection = sourceTermGroup.TermSets;
            if (sourceTermSetCollection.Count > 0)
            {
                foreach (TermSet sourceTermSet in sourceTermSetCollection)
                {
                    sourceClientContext.Load(sourceTermSet,
                                              set => set.Name,
                                              set => set.Description,
                                              set => set.Id,
                                              set => set.Contact,
                                              set => set.CustomProperties,
                                              set => set.IsAvailableForTagging,
                                              set => set.IsOpenForTermCreation,
                                              set => set.CustomProperties,
                                              set => set.Terms.Include(
                                                        term => term.Name,
                                                        term => term.Description,
                                                        term => term.Id,
                                                        term => term.IsAvailableForTagging,
                                                        term => term.LocalCustomProperties,
                                                        term => term.CustomProperties,
                                                        term => term.IsDeprecated,
                                                        term => term.Labels.Include(label => label.Value, label => label.Language, label => label.IsDefaultForLanguage)));

                    sourceClientContext.ExecuteQuery();

                    TermSet targetTermSet = destinationTermGroup.CreateTermSet(sourceTermSet.Name, sourceTermSet.Id, targetTermStore.DefaultLanguage);
                    targetClientContext.Load(targetTermSet, set => set.CustomProperties);
                    targetClientContext.ExecuteQuery();
                    UpdateTermSet(sourceClientContext, targetClientContext, sourceTermSet, targetTermSet);

                    foreach (Term sourceTerm in sourceTermSet.Terms)
                    {
                        Term reusedTerm = targetTermStore.GetTerm(sourceTerm.Id);
                        targetClientContext.Load(reusedTerm);
                        targetClientContext.ExecuteQuery();

                        Term targetTerm;
                        if (reusedTerm.ServerObjectIsNull.Value)
                        {
                            try
                            {
                                targetTerm = targetTermSet.CreateTerm(sourceTerm.Name, targetTermStore.DefaultLanguage, sourceTerm.Id);
                                targetClientContext.Load(targetTerm, term => term.IsDeprecated,
                                                                     term => term.CustomProperties,
                                                                     term => term.LocalCustomProperties);
                                targetClientContext.ExecuteQuery();
                                UpdateTerm(sourceClientContext, targetClientContext, sourceTerm, targetTerm, languagesToProcess);
                            }
                            catch (ServerException ex)
                            {
                                if (ex.Message.IndexOf("Failed to read from or write to database. Refresh and try again.") > -1)
                                {
                                    // This exception was due to caching issues and generally is thrown when terms are reused across groups.
                                    targetTerm = targetTermSet.ReuseTerm(reusedTerm, false);
                                }
                                else
                                {
                                    throw ex;
                                }
                            }
                        }
                        else
                        {
                            targetTerm = targetTermSet.ReuseTerm(reusedTerm, false);
                        }

                        targetClientContext.Load(targetTerm);
                        targetClientContext.ExecuteQuery();

                        targetTermStore.UpdateCache();

                        // Refresh session and term store references to force reload of the term just added. You need 
                        // to do this because there can be an update change event following next, and if you don't,
                        // the newly created term set cannot be obtained from the server.
                        targetTermStore = GetTermStoreObject(targetClientContext);

                        // Recursively add the other terms.
                        ProcessSubTerms(sourceClientContext, targetClientContext, targetTermSet, targetTerm, sourceTerm, languagesToProcess, targetTermStore.DefaultLanguage);
                    }
                }
            }
            targetClientContext.ExecuteQuery();
        }

方案 2 - 流程更改

当你选择“流程更改”时,外接程序将提示你输入要同步的术语组,然后调用 MMSSyncManager.cs 中的 ProcessChanges 方法。 ProcessChanges 使用 ChangedInformation 类的 GetChanges 方法在源 Managed Metadata Service 中检索对组、术语集和术语进行的所有更改。 然后,对目标 Managed Metadata Service 应用更改。

注意

本文档仅收录了 ProcessChanges 方法的部分内容。 若要查看完整方法,请在 Visual Studio 中打开 Core.MMSSync 解决方案。


ProcessChanges 方法首先创建 TaxonomySession 对象。

Log.Internal.TraceInformation((int)EventId.TaxonomySession_Open, "Opening the taxonomy session");
            TaxonomySession sourceTaxonomySession = TaxonomySession.GetTaxonomySession(sourceClientContext);
            TermStore sourceTermStore = sourceTaxonomySession.GetDefaultKeywordsTermStore();
            sourceClientContext.Load(sourceTermStore,
                                            store => store.Name,
                                            store => store.DefaultLanguage,
                                            store => store.Languages,
                                            store => store.Groups.Include(group => group.Name, group => group.Id));
            sourceClientContext.ExecuteQuery();


接下来,它将使用 ChangeInformation 对象检索更改,并为 ChangeInformation 对象设置起始日期。 本示例检索在过去一年中所做的所有更改。

Log.Internal.TraceInformation((int)EventId.TermStore_GetChangeLog, "Reading the changes");
            ChangeInformation changeInformation = new ChangeInformation(sourceClientContext);
            changeInformation.StartTime = startFrom;
            ChangedItemCollection termStoreChanges = sourceTermStore.GetChanges(changeInformation);
            sourceClientContext.Load(termStoreChanges);
            sourceClientContext.ExecuteQuery();


GetChanges 方法返回 ChangedItemCollection,这将枚举术语库中发生的所有更改,如以下代码示例中所示。 将检查示例的最后一行,以确定 ChangedItem 是否为术语组。 ProcessChanges 包括用于在术语集和术语的 ChangedItem 上执行类似检查的代码。

foreach (ChangedItem _changeItem in termStoreChanges)
                {
                    
                    if (_changeItem.ChangedTime < startFrom)
                    {
                        Log.Internal.TraceVerbose((int)EventId.TermStore_SkipChangeLogEntry, "Skipping item {1} changed at {0}", _changeItem.ChangedTime, _changeItem.Id);
                        continue;
                    }

                    Log.Internal.TraceVerbose((int)EventId.TermStore_ProcessChangeLogEntry, "Processing item {1} changed at {0}. Operation = {2}, ItemType = {3}", _changeItem.ChangedTime, _changeItem.Id, _changeItem.Operation, _changeItem.ItemType);

                    #region Group changes
                    if (_changeItem.ItemType == ChangedItemType.Group)


更改项类型可能是术语组、术语集或术语。 每个更改项类型具有可以对它执行的不同操作。 下表列出可以对每个更改项类型执行的操作。


什么发生了变化? (ChangedItemType) 可以对已更改项类型执行的操作 (ChangedOperationType)
Group

删除组

添加组

编辑组

TermSet

删除术语集

移动术语集

复制术语集

添加术语集

编辑术语集

术语

删除术语

移动术语

复制术语

更改术语路径

合并术语

添加术语

编辑术语

下面的代码展示了如何在源 Managed Metadata Service 中删除术语组时执行删除操作。

#region Delete group
                        if (_changeItem.Operation == ChangedOperationType.DeleteObject)
                        {
                            TermGroup targetTermGroup = targetTermStore.GetGroup(_changeItem.Id);
                            targetClientContext.Load(targetTermGroup, group => group.Name);
                            targetClientContext.ExecuteQuery();

                            if (!targetTermGroup.ServerObjectIsNull.Value)
                            {
                                if (termGroupExclusions == null || !termGroupExclusions.Contains(targetTermGroup.Name, StringComparer.InvariantCultureIgnoreCase))
                                {
                                    Log.Internal.TraceInformation((int)EventId.TermGroup_Delete, "Deleting group: {0}", targetTermGroup.Name);
                                    targetTermGroup.DeleteObject();
                                    targetClientContext.ExecuteQuery();
                                }
                            }
                        }
                        #endregion

另请参阅