Japan Dynamics CRM Team Blog

Microsoft Dynamics CRM technical information for partners and customers

Dynamics CRM 2011 REST エンドポイントでレコード更新時に発生する問題

Dynamics CRM 2011 REST エンドポイントでレコード更新時に発生する問題

  • Comments 0

みなさん、こんにちは。

今日は Dynamics CRM 2011 の REST エンドポイント利用時における既知の問題を
紹介します。個人的にちょうどこの問題に先日遭遇したため、きっと他の方にも有益な
情報になると思います。

REST エンドポイントでデータを更新した場合に、値が更新されていない列も不要に
更新され、以下の問題が発生する可能性があります。

  • 一部データのロス
  • 監査が期待通りに機能しない
  • ワークフローやプラグインが必要ないタイミングで実行される

これだけみると心配になりますが、この記事で原因と対策を紹介しますのでご安心ください。

事象の説明

Dynamics CRM SDK の 「REST エンドポイントを使用して Microsoft Dynamics CRM で基本的なデータ操作を実行する
のページでは、以下の説明が含まれています。

image

これは、Silverlight アプリケーションにおいて、OData サービス用に生成したプロキシを利用する際の
動作です。エンティティのレコードを作成し DataServiceContext.BeginSaveChanges Method
渡してデータを更新する際に、エンティティが持っている全てのフィールドの情報も渡されます。

例えば、エンティティのインスタンスを作成し、そこに Dynamics CRM 2011 から取得したデータを
渡したとします。その後一部の情報を変更して更新処理を行うと、変更していない列も更新対象と
渡され、更新対象となります。逆に、エンティティのインスタンスを作成後、GUID や一部のデータのみ
付与して更新を行うと、データを与えていない列の値は NULL となり、更新後のレコードのそれらの列も
NULL となってしまいます。

対応策

この問題に対処するには、どの列が更新対象であるかを追跡し、更新時には対象の列のみを
渡すことが必要です。対応策は比較的容易で、以下の手順を既存のアプリケーションに対して
実装するだけです。

Data Service Context の拡張をソリューションに追加する

1. Silverlight プロジェクトに System.Xml.Linq 参照を追加します。

2. 新しいクラスを追加する。ファイル名はここでは DataServiceContextExtensions.cs としました。

3. コードの中身として以下を貼り付ける。

using System;
using System.Linq;
using System.Data.Services.Client;
using System.Reflection;
using System.Collections.Generic;
using System.ComponentModel;
using System.Collections.ObjectModel;
using System.Xml.Linq;

namespace <データサービスコンテキストの名前空間>
{
    partial class <データサービスコンテキストのクラス名>
    {
        #region Methods

        partial void OnContextCreated()
        {
            this.ReadingEntity += this.OnReadingEntity;
            this.WritingEntity += this.OnWritingEntity;
        }

        #endregion
 
       #region Event Handlers

        private void OnReadingEntity(object sender, ReadingWritingEntityEventArgs e)
        {
            ODataEntity entity = e.Entity as ODataEntity;
            if (null == entity)
            {
                return;
            }
            entity.ClearChangedProperties();
        }

        private void OnWritingEntity(object sender, ReadingWritingEntityEventArgs e)
        {
            ODataEntity entity = e.Entity as ODataEntity;
            if (null == entity)
            {
                return;
            }
            entity.RemoveUnchangedProperties(e.Data);
        }

        #endregion
    }

    public abstract class ODataEntity
    {
        private readonly Collection<string> ChangedProperties = new Collection<string>();

        public ODataEntity()
        {
            EventInfo info = this.GetType().GetEvent("PropertyChanged");
            if (null != info)
            {
                PropertyChangedEventHandler method = new PropertyChangedEventHandler(this.OnEntityPropertyChanged);    //Ensure that the method is not attached and reattach it
                info.RemoveEventHandler(this, method);
                info.AddEventHandler(this, method);
            }
        }

        #region Methods

        public void ClearChangedProperties()
        {
            this.ChangedProperties.Clear();
        }

        internal void RemoveUnchangedProperties(XElement element)
        {
            const string AtomNamespace = "http://www.w3.org/2005/Atom";
            const string DataServicesNamespace = "http://schemas.microsoft.com/ado/2007/08/dataservices";
            const string DataServicesMetadataNamespace = DataServicesNamespace + "/metadata";
            if (null == element)
            {
                throw new ArgumentNullException("element");
            }
            List<XElement> properties = (from c in element.Elements(XName.Get("content", AtomNamespace)
                        ).Elements(XName.Get("properties", DataServicesMetadataNamespace)).Elements()
                                         select c).ToList();
            foreach (XElement property in properties)
            {
                if (!this.ChangedProperties.Contains(property.Name.LocalName))
                {
                    property.Remove();
                }
            }
        }

        private void OnEntityPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
        {
            if (!this.ChangedProperties.Contains(e.PropertyName))
            {
                this.ChangedProperties.Add(e.PropertyName);
            }
        }

        #endregion
    }
}

4. 名前空間とクラス名には、OData サービスコンテキストと同じ名称を利用します。
SDK にあるサンプルの場合は以下の通りです。

名前空間: Microsoft.Crm.Sdk.Samples.CrmODataService
クラス名: AdventureWorksCycleContext

5. ソリューションエクスプローラーより 「すべてのファイルを表示」 をクリックします。

image

6. サービス参照を展開して、Reference.cs ファイルを開きます。

image

7. 以下の文字列を検���して置換します。

検索文字列 「: global::System.ComponentModel.INotifyPropertyChanged」

置換文字列 「: ODataEntity, global::System.ComponentModel.INotifyPropertyChanged」

複数個所あります。

8. プロジェクトをリコンパイルして、コンパイルが成功するか確認します。

テスト

SDK のサンプルを少し変更して、取引先企業の作成、更新、削除を行う Silverlight を作成。
作成時には名前と説明を追加し、更新時には名前だけ変更するようにしました。また結果を
見るために、取引先企業に対して監査を有効にしています。

対策なしの結果

レコード作成時 : 多くの列の情報が渡され、取引先企業名と説明もわたっている。

image

レコード更新時 : 作成時同様に多くの列が渡され、新しい値として null が設定。
説明列も値を失っています。

image

image

対策ありの結果

レコード作成時 : 先ほどと同じく設定した値と初期値を渡しています。

image

レコード更新時 : 更新対象の取引先企業名のみ渡しています。説明は失われていません。

image

SDK サンプルについて

この回避策は、SDK 5.0.5 でサンプルとして追加される予定です。

不明な点があれば、是非コメントで質問してください。

情報元: Issues When Updating Records Using the REST Endpoint for Web Resources and Silverlight
http://blogs.msdn.com/b/crm/archive/2011/06/20/updating-records-using-the-rest-endpoint-for-web-resources-and-silverlight.aspx

‐ Dynamics CRM サポート 中村 憲一郎

  • Loading...
Leave a Comment
  • Please add 6 and 2 and type the answer here:
  • Post