20190710のC#に関する記事は2件です。

C#er pay.jp APIをたたく

今日はウェブ決済サービスを提供するpay.jpのAPIをたたいてみます。管理画面からだと取引履歴が10件づつしか見れなく・・不便すぎて今ここにいます。。

オフィシャルドキュメントはこちら
https://pay.jp/docs/api/

決済一覧を取得

時間を指定して決済データ一覧を取得してみます。

こちらが決済データ一覧を取得するメソッドのドキュメント
https://pay.jp/docs/api/#%E6%94%AF%E6%89%95%E3%81%84%E3%83%AA%E3%82%B9%E3%83%88%E3%82%92%E5%8F%96%E5%BE%97

リクエスト

API側に渡せるパラメターは、下記。

名前 タイプ 備考
imit Integer 取得するデータ数の最大値(1~100まで)。指定がない場合は 10 となる。
offset Integer 基準点からのデータ取得を行う開始位置。指定がない場合は 0 となる。
since Integer タイムスタンプ、指定したタイムスタンプ以降に作成されたデータのみ取得
until Integer タイムスタンプ、指定したタイムスタンプ以前に作成されたデータのみ取得
customer String 絞り込みたい顧客ID
subscription String 絞り込みたい定期課金ID

レスポンス

レスポンスオブジェクトは下記

public class Card
{
    public string address_city { get; set; }
    public string address_line1 { get; set; }
    public string address_line2 { get; set; }
    public string address_state { get; set; }
    public string address_zip { get; set; }
    public string address_zip_check { get; set; }
    public string brand { get; set; }
    public string country { get; set; }
    public int created { get; set; }
    public string cvc_check { get; set; }
    public object customer { get; set; }
    public int exp_month { get; set; }
    public int exp_year { get; set; }
    public string fingerprint { get; set; }
    public string id { get; set; }
    public string last4 { get; set; }
    public string name { get; set; }
    public string @object { get; set; }
}

public class Datum
{
    public int amount { get; set; }
    public int amount_refunded { get; set; }
    public bool captured { get; set; }
    public int captured_at { get; set; }
    public Card card { get; set; }
    public int created { get; set; }
    public string currency { get; set; }
    public string customer { get; set; }
    public string description { get; set; }
    public object expired_at { get; set; }
    public object failure_code { get; set; }
    public object failure_message { get; set; }
    public string id { get; set; }
    public bool livemode { get; set; }
    public object metadata { get; set; }
    public string @object { get; set; }
    public bool paid { get; set; }
    public object refund_reason { get; set; }
    public bool refunded { get; set; }
    public object subscription { get; set; }
}

public class TransactionList
{
    public int count { get; set; }
    public List<Datum> data { get; set; }
    public bool has_more { get; set; }
    public string @object { get; set; }
    public string url { get; set; }
}

オーソライズ、認証

PAY.JPのAPIを利用するには、ユーザー登録を行い、APIページからAPIキーを取得してください。 テスト用のキーでは、本番の支払い処理を行うサーバーへは接続されず、実際の支払い情報として計上されることもありません。本番用のキーは、本番申請を行うことで利用できるようになります。

とのことです。

パブリックキー HTML内に埋め込むトークン生成用のパブリックキー
シークレットキー サーバー側からBasic認証のユーザーネームとして渡すシークレットキー
通常の認証は、シークレットキーをユーザーネームとして扱い、Basic認証経由で行われます。パブリックキーは、あなたの決済画面のHTML内に組み込む公開用のAPIキーで、クレジットカードのトークンを生成する際に使用します。 シークレットキーは、全てのAPIリクエスト操作が可能となる重要なキーなので、くれぐれ も取扱いにご注意ください。

シークレットキーとパスワードをBasic認証経由で行うとのことで、下記のコードを用意

// Set Token
var base64authorization = Convert.ToBase64String(Encoding.ASCII.GetBytes(token + ":" + pass));

APIを叩いてみる

今回はシンプルにペラアプリケーションとしてHomeコントローラー/Indexビューページに作成しました。一度100件づつしか取得できないので、100以上ある場合はhas_moreがfalseになるまで100件づつ取得しています。あまり大量のデータを取得しようとするとPay.jpへの負担となるかもしれないので利用上の注意が必要です。今回は制限をかけていませんが、なにかしらの制限を設ける事をお勧めします。

/// <summary>
/// Get specific result
/// </summary>
/// <param name="token"></param>
/// <param name="pass"></param>
/// <param name="fromDateTime"></param>
/// <param name="toDateTime"></param>
/// <param name="inCsv"></param>
/// <returns></returns>
[HttpPost]
public async Task<IActionResult> Index(string token, string pass, DateTime fromDateTime, DateTime toDateTime, bool inCsv)
{

  // Set Date, if not set
   if (fromDateTime <= DateTime.MinValue && toDateTime <= DateTime.MinValue)
   {
     fromDateTime = DateTime.UtcNow.AddHours(-4);
       toDateTime = DateTime.UtcNow.AddHours(-1);
   }

   // Set ViewData
   ViewData["FROMDATETIME"] = fromDateTime;
   ViewData["TODATETIME"] = toDateTime;
   ViewData["TOKEN"] = token;
   ViewData["PASS"] = pass;

   // Token must be set first
   if (string.IsNullOrEmpty(token))
   {
      return View();
   }

   // Token
   var payjpToken = token + ":" + pass;

   // Convert to DateTimeOffset
   var fromDateTimeOffset = new DateTimeOffset(fromDateTime.Ticks, new TimeSpan(+09, 00, 00));
   var toDateTimeOffset = new DateTimeOffset(toDateTime.Ticks, new TimeSpan(+09, 00, 00));

   // To Unix seconds
   var fromDateUnixSeconds = fromDateTimeOffset.ToUnixTimeSeconds();
   var toDateUnixSeconds = toDateTimeOffset.ToUnixTimeSeconds();

   // Set Path
   var path = "charges?";
   path += "since=" + fromDateUnixSeconds + "&until=" + toDateUnixSeconds;
   path += "&offset=0&limit=100";

   // Set Token
   var base64authorization = Convert.ToBase64String(Encoding.ASCII.GetBytes(payjpToken));

   // Call API
   var client = new HttpClient();
   var result = new ChargeListResponse();
   client.BaseAddress = new Uri(payJpBaseUrl);
   client.DefaultRequestHeaders.Accept.Clear();
   client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
   client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", base64authorization);

   HttpResponseMessage response = await client.GetAsync(path);

   // Make sure the call was a success
   if (response.IsSuccessStatusCode)
   {
     result = await response.Content.ReadAsAsync<ChargeListResponse>();
   }

   // see if there are more
   for (int i = 1; result.has_more; i++)
   {

     path = "charges?";
     path += "since=" + fromDateUnixSeconds + "&until=" + toDateUnixSeconds;
     path += "&offset=" + i * 100 + "&limit=100";

     response = await client.GetAsync(path);

     // Make sure the code was a success
     if (response.IsSuccessStatusCode)
     {
       var tresult = await response.Content.ReadAsAsync<ChargeListResponse>();

       // Add to result
       result.count += tresult.count;
       result.data.AddRange(tresult.data);
       result.has_more = tresult.has_more;
     }
   }

   client.Dispose();

   return View(result);
}

結果を表示する

Viewです。

@model ChargeListResponse
@{
    ViewData["Title"] = "レポート";
}
<div class="container-fluid py-2">
  <h1>@ViewData["Title"]</h1>
  <hr />
  @using (Html.BeginForm("Index", "PayJp", FormMethod.Post))
  {
    <div class="input-group mb-3">
      @Html.TextBox("token", ViewData["TOKEN"], new { @class = "form-control", @placeholder = "本番秘密鍵" })
      @Html.TextBox("pass", ViewData["PASS"], new { @type = "password", @class = "form-control", @placeholder = "パスワード" })
    </div>
    <div class="input-group mb-3">
      @Html.TextBox("fromDateTime", ViewData["FROMDATETIME"], new { @class = "form-control" })
      @Html.TextBox("toDateTime", ViewData["TODATETIME"], new { @class = "form-control" })
    </div>
    <div class="small">
      in JST
    </div>
    <div class="form-group">
      <button class="btn btn-primary rounded-0" type="submit">検索</button>
    </div>
}

<hr />
@if (Model != null)
{
  <div class="d-flex justify-content-between">
    <div>
      @Model.count<text>データ</text>
    </div>     
  </div>
  <hr />
  @if (Model.data != null)
  {
    <div class="table-responsive">
      <table class="table table-bordered table-striped">
        <thead>
          <tr>
            <th>Amount</th>
            <th>Captured</th>
            <th>Captured_at</th>
            <th>Card.address_city</th>
            <th>Card.address_line1</th>
            <th>Card.address_line2</th>
            <th>Card.address_state</th>
            <th>Card.address_zip</th>
            <th>Card.address_zip_check</th>
            <th>Card.brand</th>
            <th>Card.country</th>
            <th>Card.created</th>
            <th>Card.customer</th>
            <th>Card.cvc_check</th>
            <th>Card.exp_month</th>
            <th>Card.exp_year</th>
            <th>Card.fingerprint</th>
            <th>Card.id</th>
            <th>Card.name</th>
            <th>Card.last4</th>
            <th>Created</th>
            <th>Currency</th>
            <th>Customer</th>
            <th>Description</th>
            <th>Expired_at</th>
            <th>Failure_code</th>
           <th>Failure_message</th>
            <th>Id</th>
            <th>Livemode</th>
            <th>Metadata</th>
            <th>Paid</th>
            <th>Refunded</th>
            <th>Refund_reason</th>
            <th>Subscription</th>
          </tr>
        </thead>
        <tbody>
          @foreach (var record in Model.data)
          {
            <tr>
              <td>@record.amount</td>
              <td>@record.captured</td>
              <td>@record.captured_at</td>
              <td>@record.card.address_city</td>
              <td>@record.card.address_line1</td>
              <td>@record.card.address_line2</td>
              <td>@record.card.address_state</td>
              <td>@record.card.address_zip</td>
              <td>@record.card.address_zip_check</td>
              <td>@record.card.brand</td>
              <td>@record.card.country</td>
              <td>@record.card.created</td>
              <td>@record.card.customer</td>
              <td>@record.card.cvc_check</td>
              <td>@record.card.exp_month</td>
              <td>@record.card.exp_year</td>
              <td>@record.card.fingerprint</td>
              <td>@record.card.id</td>
              <td>@record.card.name</td>
              <td>@record.card.last4</td>
              <td>@record.created</td>
              <td>@record.currency</td>
              <td>@record.customer</td>
              <td>@record.description</td>
              <td>@record.expired_at</td>
              <td>@record.failure_code</td>
              <td>@record.failure_message</td>
              <td>@record.id</td>
              <td>@record.livemode</td>
              <td>@record.metadata</td>
              <td>@record.paid</td>
              <td>@record.refunded</td>
              <td>@record.refund_reason</td>
              <td>@record.subscription</td>
            </tr>
          }
        </tbody>
      </table>
    </div>
    }
  }
  else
  {
    <div>
      本番秘密鍵とパスワードを入力してください。
    </div>
  }
</div>

おわり

一旦以上です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

C#のキャスト、isとas

参照型のキャスト

参照型のキャストに悩む必要はない。

//キャストできればその結果が、できなければnullがresultに格納される
TargetType result = obj as TargetType;

//キャストできればtrueが、できなければfalseがisTargetTypeに格納される
bool isTargetType = obj is TargetType;

C#7.0からはisの結果を変数に受けられるようになったので次のような書き方ができる。

//isTargetTypeにキャスト可否が、resultにキャスト結果が格納される
bool isTargetType = obj is TargetType result;

よく使うイディオムはこんな感じだろう。

//目的の型にキャストできたときのみ処理を実行する。
//昔ならこう書いていた
var result1 = obj as TargetType;
if (result1 != null)
{
    result1.SomeMethod();
}

//手を抜けばこれでもよかった
(obj as TargetType)?.SomeMethod();

//C#7.0以降ならこう書ける
if (obj is TargetType result2)
{
    result2.SomeMethod();
}
//ちなみにresult2のブロックはifブロックの外側なのでここでも参照できる
result2 = null;

値型のキャスト

値型のキャストはそうはいかなかった。

object obj = 1;

//NG:asは値型では使えない
int i = obj as int;

//OK
int j = (int)obj;

struct TargetStruct {}
//構文上OK、ただしInvalidCastExceptionが飛ぶ。
TargetStruct target = (TargetStruct)obj;

見ての通りasが使えない。
考えてみれば当然の話で、値型では失敗時のデフォルト値ゼロと成功時の値ゼロが区別できないのだ。
じゃあ安全なキャストにはどうすればいいのか。

//isでの判定はできるので昔ならこう
if (obj is int)
{
    SomeMethod((int)obj);
}

//C#7.0以降ならisの結果を受け取れるので
if (obj is int i)
{
    SomeMethod(i);
}

じゃあasはisの下位互換なのかというと別にそんなわけではない。
従来から

//resultはNullable<int>型
//resultにはキャストに成功すればその値が、失敗すればnullが入る
var result = obj as int?;
if (result != null)
{
    SomeMethod(i.Value);
}

Nullable<T>にはasを使って安全にキャストできていたので参照型同様の手も使えた。
でもわざわざNullable<T>を使うのも面倒なので今後は積極的にisを使っていきたい。

結論

C#7.0以降なら参照型・値型を問わずisを使うと統一した方法でキャストができる。
それ以前の環境で無理をしてでも統一した扱いにしたいならNullable<T>へのキャストを、そこまで頑張りたくなければあきらめて処理を分けよう。

注意点

今回それぞれの方法での処理時間は計測していないので注意。
Nullable<T>へのキャストはボクシングがかかってやや遅くなりそうな気がする。
訂正:Nullable<T>はstructだった。
今回この記事を書こうと思った理由はIValueConverterの実装だったのでそのくらいのコストは無視していいのだけど。

ところで

残念なことに現在参加中のプロジェクトはC#6.0だった。
一刻も早いC#8.0への移行を強く訴えていきたい。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む