Claris FileMakerをC# .NETでいい感じ扱えるFMDataの紹介と使い方

# はじめに
WindowsとC#でFileMakerの連携を行う必要があり、よいライブラリがないかと探していたところ、[FMData](https://fmdata.io/)という素晴らしいライブラリに出会いました。

公式サイトのサンプルプログラムだけでは使いこなすのが難しく、忘備録も兼ねて使い方をまとめました。

:::note info
**サンプルプログラムについて**
公式のサンプルプログラムをベースに不足部分を補う形でサンプルプログラムを載せています。

* 使い方を検索エンジンなどで調べてみてもヒットしないので、実装方法の調査に苦労しました。
* 使い方などは自分でソースを解析して調べています。（もしかすると、もっとよい実装方法があるかもしれません。）
:::

# FMDataとは

> .NET から RESTful API を利用する方法はたくさんありますが、このプロジェクトの目標は、FileMaker データベースからのデータを .NET アプリケーションで利用する開発者に、FileMaker の慣用的なエクスペリエンスと>  .NET の慣用的なエクスペリエンスを融合させたエクスペリエンスを提供することです。

※ 公式より引用

### FMDataを使うとなにがいいのか
FileMakerにはFileMaker Data APIというものがあり、Rest API経由でデータをやりとりする仕組みがあります。

FileMaker Data APIの仕組みを理解して１から実装していくのはかなりの時間を有しますが、この面倒な実装を解決してくれるものがFMDataになります **（私は非常に助かりました）**

 **Claris FileMaker 2023 Data API ガイド** 

https://help.claris.com/ja/data-api-guide/

## 構成
OS：Windows 11 Pro
Claris FileMaker 2023
Visual Studio 2022
FMData v5.2.0
C#
.NET8

## 今回の範囲
今回はローカルにあるFileMakerへデータの検索、登録、更新を行う内容となります。

:::note info
 **FMDataはFileMaker Cloudにも対応**
今回は環境がないので調査していませんが、実装に関してもローカル版と変わらない感じで使えます。
:::

``` 公式のサンプルプログラム.cs
var conn = new ConnectionInfo();
conn.FmsUri = "https://{NAME}.account.filemaker-cloud.com";
conn.Username = "user@domain.com";
conn.Password = "********";
conn.Database = "Reporting";
```

 **FileMaker Cloud** 

https://www.claris.com/ja/filemaker/cloud/

:::note info
 **ユーザーについて** 

ユーザーは、FileMaker Data APIが利用可能でないとエラーになるので、FileMakerアカウントの拡張アクセス権を設定する必要があります。
**FileMaker Data API でのアクセス–FMSのみ（fmrest）** にチェックを付けてください。
:::


# 使い方
はじめに、nugetでFMDataとFMData.Restをインストールしてください。

## FileMakerのテーブルを作成する。
よくあるRestAPIでデータをシリアライズ、デシリアライズする仕組みがFMDataにもあります。

``` 公式のサンプルプログラム.cs
// DataContract 属性を使用して、モデルをレイアウトにリンクします
[DataContract(Name="NameOfYourLayout")]
public class Model
{
    [DataMember]
    public string Name { get; set; }

    // モデル名が一致しない場合は DataMember を使用します
    [DataMember(Name="overrideFieldName")] // 使用する内部データベースフィールド
    public string Address { get; set; }

    [DataMember]
    public string SomeContainerField { get; set; }

    // ContainerDataFor 属性を使用して、コンテナデータを byte[] にマッピングします
    [ContainerDataFor("SomeContainerField")] // C# モデルの名前を使用します
    public byte[] DataForSomeContainerField { get; set; }

    // マッピングしないプロパティがモデルにある場合は、
    [IgnoreDataMember] // フィールドのマッピングをスキップするために使用します
    public string NotNeededField { get; set; }
}

```

公式のサンプルプログラムでは細かな内容までは記されていませんが、このタグがかなり重要な役割になっており、FileMakerのテーブル名と一致しないといけません。

DataContract：FileMakerのテーブル名
DataMemberのName：列名

``` イメージ.cs

    [DataContract(Name = "FileMakerのテーブル名")]
    public class FM_Table : RecordBase<string, string>
    {
        [DataMember(Name = "氏名")]
        public string col1 { get; set; }
        [DataMember(Name = "郵便番号")]
        public string col2 { get; set; }
        [DataMember(Name = "住所")]
        public string col3 { get; set; }
        [DataMember(Name = "電話番号")]
        public string col4 { get; set; }
    }

```

:::note info
**その他調査結果**
* 列名は大文字、小文字を区別しません。
* FileMakerのテーブル列をすべて作成しないといけない訳ではありません。必要な列だけあれば問題ありません。
* FileMaker側で存在しない列をDataMemberのNameにセットして登録するとエラーになります。
* 公式のサンプルプログラムにはありませんが、**FileMaker側のレコードIDを取得するにはRecordBaseを継承する必要があります。**
:::

## 接続方法
接続に関してはFileMaker側でSSLの設定ができていれば、公式のサンプルプログラム通りで問題なく動作します。

``` 公式のサンプルプログラム.cs
services.AddSingleton<FMData.ConnectionInfo>(ci => new FMData.ConnectionInfo
{
    FmsUri = "https://example.com",
    Username = "user",
    Password = "password",
    Database = "FILE_NAME"
});
services.AddHttpClient<IFileMakerApiClient, FileMakerRestClient>();

```

しかし、FileMaker側でSSLの設定ができていない場合、HttpClientで無効な証明書を無視する必要があります。

``` サンプルプログラム.cs
public static ConnectionInfo Connection => new() { FmsUri = Server, Database = File, Username = User, Password = Pass };
public static readonly string Server = "https://xxx.xxx.xxx.xxx/";
public static readonly string File   = "データベース名";
public static readonly string User   = "ユーザーID";
public static readonly string Pass   = "パスワード";


using (var httpClientHandler = new HttpClientHandler())
{
    httpClientHandler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => { return true; }; // これ
    using (var httpclient = new HttpClient(httpClientHandler))
    {
        var client = new FileMakerRestClient(httpclient, Connection);
        // ここからデータの更新などを行う。
    }
}

```

## 検索の実行
検索に関しては公式のサンプルプログラムで特に問題なく取得できるとおもいます。

``` 公式のサンプルプログラム.cs
var toFind = new Model { Name = "someName" };
var results = await client.FindAsync(toFind);

```

## （重要）FileMakerのレコードIDを取得する。
公式のサンプルプログラムにはFileMakerのレコードIDを取得する方法が書かれておらず、使い方が分からず挫折するのではないかと思います（私がそうでした）

ここでは、そのサンプルとなります。

まず初めにFileMaker側のテーブルを作成します。こちらは「FileMakerのテーブルを作成する。」で紹介したものとなります。

``` FileMaker側のテーブル.cs

    [DataContract(Name = "ユーザー一覧")]
    public class FM_Table : RecordBase<string, string> // RecordBaseが必要
    {
        [DataMember(Name = "氏名")]
        public string col1 { get; set; }
        [DataMember(Name = "住所")]
        public string col2 { get; set; }
    }

```

FileMakerよりレコードIDを取得するプログラムになります。

``` サンプル.cs

var toFind = new FM_Table { col1 = "田中　太郎" };

// RecordId と ModId をマッピングする関数の定義
Func<FM_Table, int, object> recordIdMapper = (entity, recordId) =>
{
    entity.RecordId = recordId;
    return entity;
};

Func<FM_Table, int, object> modIdMapper = (entity, modId) =>
{
    entity.ModId = modId;
    return entity;
};

var results = await client.FindAsync(
                        request: toFind,
                        skip: 0,               // 最初から
                        take: 10,              // 最大10件のレコードを取得
                        script: null,          // スクリプトは実行しない
                        scriptParameter: null,
                        fmIdFunc: recordIdMapper,
                        fmModIdFunc: modIdMapper
                    );

if (results == null)
{
    return;
}

// キーが複数ある場合エラーとする（検索して１つしか存在しない場合を想定しています）
if(results.Count() > 1)
{
    return;
}

// FileMakerのレコードIDを取得する。
// 0の場合は新規となる。
int recordId = 0;

foreach (var result in results)
{
    recordId = result.RecordId; // この値がFileMakerでいう所の行IDとなる。
}

```

この、result.RecordIdの値が更新時に必要となります。

## データを更新する
データを更新するとき、result.RecordIdの値が必要となります。

``` 公式のサンプルプログラム.cs
var fileMakerRecordId = 1;
var toUpdate = new Model { Name = "someName", Address = "123 Main Street" };
var results = await client.EditAsync(fileMakerRecordId, toUpdate);

```

公式のサンプルプログラムではfileMakerRecordIdに1をセットして更新していますが、この1の値がresult.RecordIdの値となります。

※ なんでこんな重要な事が書かれていないのかは不明。


## 新しいレコードを作成する
新しいレコードを作成する場合のサンプルプログラムですが、こちらは公式のサンプルプログラムで十分だと思います。
※ 特に苦労したことはないです。

``` 公式のサンプルプログラム.cs
var toCreate = new Model { Name = "someName", Address = "123 Main Street" };
var results  = await client.CreateAsync(toCreate);
```