rksoftware

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

Dataverse の WebAPI で Dataverse のテーブルに行追加および更新する C# コード

目次

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

rksoftware.hatenablog.com

今回の本文

テーブルに行追加および行更新を一気に書けます。
CSVで用意したデータをテーブルにインポートする動作です。

CSVは以前に書いたこの記事のコードで取得したものに追加、更新を書けたものを用意します。 rksoftware.hatenablog.com

更新時のキーは {テーブル名id} の自動で作成される列の値です。追加データの場合が該当列は空白にしておいてください。

■ 動かし方

引数なしで動かしたときのコンソール出力がマニュアルになっています。

■ 概要

CSV を JSON に変換します。
Dataverse から列定義を取ってきます。
Dataverse は列の値を文字列で送ったら数値などの列ではいい感じに数値として読んでくれる、というようなことは ありません
Dataverse からとってきた列定義を見ながら、JSON の値を数値にしたり、DateTime にしたり。頑張ります。
{テーブル名}id 列の値があれば、更新。そうでなければ追加で処理をします。

■ コード

クラス3つです。

using DataverseClientShared;
using Newtonsoft.Json.Linq;
using System.Diagnostics;
using System.Net.Http.Json;
using System.Text;

if (args.Length < 2)
{
    Console.WriteLine("Usage: DataverseImport <EntityCollection URL> <Path to CSV file>");
    Console.WriteLine("Example");
    Console.WriteLine("<Data URL> : https://orgxxxxxxxx.crmx.dynamics.com/api/data/v9.2/xxxxxxxxs");
    Console.WriteLine("<Definitions URL> : \"https://orgxxxxxxxx.crmx.dynamics.com/api/data/v9.2/EntityDefinitions(LogicalName='xxxxxxxx')\"");
    Console.WriteLine("<Path to CSV file> : C:\\example\\example.csv");
    return;
}
string s1 = args[0];
string s2 = $"{args[1]}/Attributes";

var jsons = DataverseClientShared.CSV.Instance.ToJosnArrayFromFile(args[2]);

HttpHelper http = (await HttpHelper.Instance.Init(s2, new HttpClient())).Item1;

string uniqueidentifierName = null;
Dictionary<string, string> attributeTypes = new();
{
    var attributesJsonString = await http.GetAsync();
    var attributesJsonObject = Newtonsoft.Json.Linq.JObject.Parse(attributesJsonString);
    uniqueidentifierName = (attributesJsonObject["value"] as Newtonsoft.Json.Linq.JArray).Where(attr => attr["AttributeType"].ToString() == "Uniqueidentifier").Select(attr => attr["SchemaName"].ToString().ToLower()).FirstOrDefault();
    var attributes = (attributesJsonObject["value"] as Newtonsoft.Json.Linq.JArray).Select(attr => (attr["SchemaName"], attr["@odata.type"])).ToArray();
    foreach (var attr in attributes.Where(attr => attr.Item1 != null && attr.Item2 != null))
        attributeTypes[attr.Item1.ToString().ToLower()] = attr.Item2.ToString();
}

foreach (var json in jsons)
    foreach (var attr in json.Properties().ToArray())
    {
        if (attributeTypes.TryGetValue(attr.Name.ToString(), out var type))
        {
            var value = attr.Value.ToString(); if (string.IsNullOrWhiteSpace(value)) { attr.Value = null; continue; }
            switch (type)
            {
                case "#Microsoft.Dynamics.CRM.DecimalAttributeMetadata":
                case "#Microsoft.Dynamics.CRM.DoubleAttributeMetadata":
                    if (string.IsNullOrWhiteSpace(value)) attr.Value = null; else attr.Value = decimal.Parse(value);
                    break;
                case "#Microsoft.Dynamics.CRM.IntegerAttributeMetadata":
                case "#Microsoft.Dynamics.CRM.BigIntAttributeMetadata":
                    if (string.IsNullOrWhiteSpace(value)) attr.Value = null; else attr.Value = long.Parse(value);
                    break;
                case "#Microsoft.Dynamics.CRM.DateTimeAttributeMetadata":
                    if (string.IsNullOrWhiteSpace(value)) attr.Value = null; else attr.Value = DateTime.Parse(value);
                    break;
                default:
                    break;
            }
        }
        else if (attr.Name?.ToLower() == uniqueidentifierName?.ToLower())
        {
            continue;
        }
        else
            attr.Remove();
    }

http.InitPath(s1);
foreach (var json in jsons)
{
    if (uniqueidentifierName != null && !string.IsNullOrWhiteSpace(json[uniqueidentifierName]?.ToString()))
    {
        var content = new StringContent(json.ToString(), Encoding.UTF8, "application/json");
        var response = await http.HttpClient.PatchAsync(http.Path + $"({json[uniqueidentifierName]?.ToString()})", content);
        var jsonContent = await response.Content.ReadAsStringAsync();
        if (response.IsSuccessStatusCode)
        {
            Console.WriteLine($"Success: {response.StatusCode} {response.ReasonPhrase} {jsonContent}");
        }
        else
        {
            Console.WriteLine($"Error: {response.StatusCode} {response.ReasonPhrase} {jsonContent}");
        }
    }
    else
    {
        json[uniqueidentifierName]?.Parent?.Remove();
        var content = new StringContent(json.ToString(), Encoding.UTF8, "application/json");
        var response = await http.HttpClient.PostAsync(http.Path, content);
        var jsonContent = await response.Content.ReadAsStringAsync();
        if (response.IsSuccessStatusCode)
        {
            Console.WriteLine($"Success: {response.StatusCode} {response.ReasonPhrase} {jsonContent}");
        }
        else
        {
            Console.WriteLine($"Error: {response.StatusCode} {response.ReasonPhrase} {jsonContent}");
        }
    }
}

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

using System.Text;
using static System.Net.WebRequestMethods;

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!);
    }

    public async Task<string> GetAsync()
    {
        if (HttpClient != null)
        {
            var response = await HttpClient.GetAsync(Path);
            if (response.IsSuccessStatusCode)
            {
                return await response.Content.ReadAsStringAsync();
            }
            else
            {
                throw new Exception($"Error: {response.StatusCode} {response.ReasonPhrase} {BaseAddress} {Path}");
            }
        }
        return "";
    }
}

CSV を読んで JSON にしたりなど。

namespace DataverseClientShared;

public class CSV
{
    public static CSV Instance { get; } = new CSV();
    private CSV() { }

    public IEnumerable<Newtonsoft.Json.Linq.JObject> ToJosnArrayFromFile(string path)
    {
        var csvString = System.IO.File.ReadAllText(path, System.Text.Encoding.UTF8);
        var result = ToJsonArray(csvString);
        return result;
    }

    public IEnumerable<Newtonsoft.Json.Linq.JObject> ToJsonArray(string csvString)
    {
        var (header, data) = CsvReader.ReadHeaderAndData(csvString);
        var list = new List<Newtonsoft.Json.Linq.JObject>();
        foreach (var row in data)
        {
            var obj = new Newtonsoft.Json.Linq.JObject();
            for (int i = 0; i < header.Length && i < row.Length; i++)
            {
                obj[RemoveTailCR(header[i])] = RemoveTailCR(row[i]);
            }
            list.Add(obj);
        }
        return list;
    }

    string RemoveTailCR(string s) => s.EndsWith("\r") ? s[..^1] : s;
}

public static class CsvReader
{
    public static string[][] Read(string csvString) => _Read(csvString);

    public static (string[] header, string[][] data) ReadHeaderAndData(string csvString)
    {
        var values = Read(csvString);
        return (values.FirstOrDefault() ?? Array.Empty<string>(), values.Skip(1).ToArray());
    }

    private static string[][] _Read(string csvString)
    {
        List<List<List<char>>> data = new();
        (bool isLineHead, bool isQuoted, char quote, bool isEscaped) = (true, false, '"', false);
        foreach (var c in csvString)
        {
            if (isLineHead) { data.Add(new List<List<char>>()); data.LastOrDefault()?.Add(new List<char>()); isLineHead = false; }
            if (data.LastOrDefault()?.LastOrDefault()?.Count == 0 && !isQuoted && (c == '"' || c == '\''))
            {
                (isQuoted, quote, isEscaped) = (true, c, false);
                continue;
            }
            if (!isEscaped && isQuoted && c == quote) { isEscaped = true; continue; }
            if (c == ',' && (!isQuoted || isEscaped)) { data.LastOrDefault()?.Add(new List<char>()); (isQuoted, isEscaped) = (false, false); continue; }
            if (c == '\n' && (!isQuoted || isEscaped)) { isLineHead = true; (isQuoted, isEscaped) = (false, false); continue; }
            isEscaped = false;
            data.LastOrDefault()?.LastOrDefault()?.Add(c);
        }
        return data.Select(line => line.Select(cell => new string(cell.ToArray())).ToArray()).ToArray();
    }
}

難しいですね。

■ GitHub

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