rksoftware

Visual Studio とか C# とかが好きです

Dataverse の WebAPI で Dataverse のテーブルに列追加する C# コード その4

目次

Dataverse の WebAPI を C# で叩く系記事たちの目次です。

rksoftware.hatenablog.com

今回の本文

rksoftware.hatenablog.com

前回の続きです。
これまでテーブルに列追加を一気に行うことを考えていってみます。
流れとしては、いったん手で列追加したテーブルがあるとして、そのテーブルと同じ列を別のテーブルに追加する流れです。

■ 前回

「はい/いいえ」
「選択肢」
「計算式」
の列が追加できませんでした。

■ 「はい/いいえ」

今回はまだ対応できていません。 次回のお楽しみに。

■ 「選択肢」

前回の既存の列の定義取得では選択肢の選択肢定義はくっついてきません。
なのに、列追加では選択肢定義は一つ以上必須とのことです。
なので、Json に OptionSet がない場合は、ダミーで選択肢を一つ追加するようにしてみました。

■ 「計算式」

これは、前回作れませんでしたが、もう一回実行したら生まれました。
計算要素になっている列がまだ作られていなかったからエラーになったのかもしれませんね。

■ コード

クラス二つです。

using DataverseClientShared;
using System.Text;

if (args.Length < 2)
{
    Console.WriteLine("Usage: DataverseAddAttributes <EntityDefinition URL> <Path to JSON file>");
    Console.WriteLine("Example");
    Console.WriteLine("<EntityDefinition URL> : https://orgxxxxxxxx.crmx.dynamics.com/api/data/v9.2/EntityDefinitions(LogicalName='xxxxx_xxxxxxxx')");
    Console.WriteLine("<Path to JSON file> : C:\example\example.json");
    return;
}

string s = args[0] + "/Attributes";
string d = args[1]; ;
HttpHelper http = (await HttpHelper.Instance.Init(s, new HttpClient())).Item1;

Newtonsoft.Json.Linq.JObject originJson = (Newtonsoft.Json.Linq.JObject)Newtonsoft.Json.JsonConvert.DeserializeObject(File.ReadAllText(d))!;
var value = originJson.SelectToken("value");
if (value != null)
    foreach (var attribute in value)
    {
        attribute.SelectToken("MetadataId")?.Parent?.Remove();
        attribute.SelectToken("CreatedOn")?.Parent?.Remove();
        attribute.SelectToken("ModifiedOn")?.Parent?.Remove();

        var type = ((Newtonsoft.Json.Linq.JValue?)attribute["@odata.type"])?.Value?.ToString();
        if (type == "#Microsoft.Dynamics.CRM.PicklistAttributeMetadata"
            || type == "#Microsoft.Dynamics.CRM.MultiSelectPicklistAttributeMetadata")
            if (attribute.SelectToken("OptionSet") == null
                || attribute.SelectToken("OptionSet")?.SelectToken("Options") == null
                || attribute.SelectToken("OptionSet")?.SelectToken("Options")?.Any() == false)
            {
                attribute["OptionSet"] = new Newtonsoft.Json.Linq.JObject
                {
                    ["Options"] = new Newtonsoft.Json.Linq.JArray
                    {
                        new Newtonsoft.Json.Linq.JObject
                        {
                            ["Value"] = 1,
                            ["Label"] = new Newtonsoft.Json.Linq.JObject
                            {
                                ["LocalizedLabels"] = new Newtonsoft.Json.Linq.JArray
                                {
                                    new Newtonsoft.Json.Linq.JObject
                                    {
                                        ["Label"] = "dummy001",
                                        ["LanguageCode"] = 1041
                                    }
                                }
                            }
                        }
                    },
                    ["IsGlobal"] = false,
                    ["OptionSetType"] = "Picklist"
                };

            }

        var json = Newtonsoft.Json.JsonConvert.SerializeObject(attribute);
        Console.WriteLine(await PostAsync(http, json));
    }

async Task<string> PostAsync(HttpHelper http, string json)
{
    string? jsonContent = "";
    var content = new StringContent(json, Encoding.UTF8, "application/json");
    if (http.HttpClient == null) return jsonContent;
    var response = await http.HttpClient.PostAsync(http.Path, content);
    if (response.IsSuccessStatusCode)
    {
        Console.WriteLine($"Success: {response.StatusCode}");
        jsonContent = await response.Content.ReadAsStringAsync();
    }
    else
    {
        Console.WriteLine($"Error: {response.StatusCode}");
        jsonContent = await response.Content.ReadAsStringAsync();
        Console.WriteLine(jsonContent);
    }
    return jsonContent ?? "";
}

次のクラスはこれまで毎回書いてきた定型的な部分をクラスでまとめたものです。

namespace DataverseClientShared
{
    public class HttpHelper
    {
        public static HttpHelper Instance { get; } = new HttpHelper();

        public string? Resource { get; private set; }
        public Uri? BaseAddress { get; private set; }
        public string? Path { get; private set; }
        public static string ClientId { get; } = "51f81489-12ee-4a9e-aaae-a2591f45987d";
        public static string RedirectUri { get; } = "http://localhost";

        private HttpHelper() { }

        public Microsoft.Identity.Client.AuthenticationResult? Token { get; private set; }
        public HttpClient? HttpClient { get; private set; }

        public HttpHelper InitPath(string fullPath)
        {
            var uri = new Uri(fullPath);
            Resource = uri.Scheme + "://" + uri.Host;
            BaseAddress = new Uri(Instance.Resource + string.Join("", uri.Segments.Take(4)));
            Path = new string(uri.PathAndQuery.Skip(BaseAddress.AbsolutePath.Length).ToArray());
            return Instance;
        }

        public async Task<Microsoft.Identity.Client.AuthenticationResult> AuthenticateAsync()
        {
            var authBuilder = Microsoft.Identity.Client.PublicClientApplicationBuilder.Create(ClientId)
               .WithAuthority(Microsoft.Identity.Client.AadAuthorityAudience.AzureAdMultipleOrgs)
               .WithRedirectUri(RedirectUri)
               .Build();
            string[] scopes = { Resource + "/user_impersonation" };
            Microsoft.Identity.Client.AuthenticationResult token = await authBuilder.AcquireTokenInteractive(scopes).ExecuteAsync();
            return Token = token;
        }

        public HttpClient SetHttpClient(HttpClient httpClient)
        {
            var client = HttpClient = httpClient ?? HttpClient ?? new HttpClient();
            client.BaseAddress = BaseAddress;
            client.Timeout = new TimeSpan(0, 2, 0);
            System.Net.Http.Headers.HttpRequestHeaders headers = client.DefaultRequestHeaders;
            headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", Token?.AccessToken);
            headers.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
            return client;
        }

        public async Task<(HttpHelper, HttpClient)> Init(string fullPath, HttpClient httpClient)
        {
            InitPath(fullPath);
            await AuthenticateAsync();
            SetHttpClient(httpClient);
            return (Instance!, HttpClient!);
        }
    }
}

■ エラーメッセージ

このコードにたどり着くまでにいくつものエラーを乗り越えてきました。

{"error":{"code":"0x80048403","message":"An option set was not specified.
{"error":{"code":"0x80048403","message":"IsGlobal is not specified. When IsGlobal is true the picklist will use the global option set that matches the specified option set name."}}
{"error":{"code":"0x80048d19","message":"Error identified in Payload provided by the user for Entity :'', For more information on this error please follow this help link https://go.microsoft.com/fwlink/?linkid=2195293 
{"error":{"code":"0x0","message":"Requested value 'MultiSelectPicklist' was not found."}}

難しいですね。

■ GitHub

GitHub リポジトリにコードを置いて試しています。

github.com