10yroの開発日記

福岡にある株式会社10yro(トイロ)のエンジニアが書いています

Filamentの導入と管理画面構築

Filamentの導入方法と管理画面の構築方法について解説します。
Filamentはデータ駆動型の管理パネルの機能とUIをセットで効率的に作ることができます。

Filamentとは

Filamentは、Laravelフレームワーク向けの管理パネルビルダーです。簡単なコマンドや設定を使って、CRUD操作やデータ管理のための管理画面を作成できます。データベースモデルに基づいて自動生成されるため、アプリケーションの管理を容易に行うことができます。

デモページ

導入方法

インストール
composer require filament/filament:"^3.2" -W
php artisan filament:install --panels

上記のコマンドを実行することでFilament Panel Builderがインストールされます。
また、新しいLaravelサービスプロバイダーが作成され、app/Providers/Filament/AdminPanelProvider.phpというファイルに登録されます。

管理者ユーザーの登録
php artisan make:filament-user
日本語化

config/app.phpの言語設定を変更することで日本語化できます。
タイムゾーンも日本時間に設定します。

config/app.php

    'timezone' => 'UTC',
        ↓
    'timezone' => 'Asia/Tokyo',
*********************************
    'locale' => 'en',
        ↓
    'locale' => 'ja',
********************************
    'faker_locale' => 'en',
        ↓
    'faker_locale' => 'ja',
管理画面へのログイン

作成したユーザーアカウントで管理画面にログインすることができます。

http://localhost/admin/login

最後に

Filamentは、迅速かつ効率的に管理画面を構築するための優れたツールです。その使いやすさや柔軟性から、開発者は手間をかけずに豊富な機能を持つ管理パネルを作成できます。
今後も、Filamentリソースの作成方法やカスタマイズ手法に関する情報を更新し、品質の高いアプリケーションを開発するための知識を共有していきたいと考えています。

マインスイーパーを作ってみた

お手頃なゲームを作りたいと思いお題を探したらふとマインスイーパーを思い出したので、C#で作ってみました。
昔はwindowsにも標準搭載されてたあれです。

まずはルール確認(wikipediaより引用 マインスイーパ - Wikipedia )

ゲーム画面は正方形のマスが敷き詰められた長方形のフィールドから構成されている。それぞれのマスは開けることができるが、地雷の置かれているマスを開けると負けとなる。地雷の置かれていないマスを開けたときは、隣接する8方向のマスのいずれかに地雷がある場合はその個数が表示され、隣接するマスに地雷が置かれていないときは、それらが自動的に開けられる。地雷の置かれていないマスをすべて開ければ勝ちとなる。

ルールは仕様は下記とします。

  • 今回は、爆弾のマスは9x9の爆弾の数は10個とする。
  • 各マスは、「爆弾」または「隣接する爆弾の数」で構成される。
  • 爆弾のマスを開いた場合はゲーム終了(負け)とする。
  • すべての爆弾が置かれていないマスを開いた場合はゲーム終了(勝ち)とする。
    • 開いてないマスが全て爆弾マスの場合でもゲーム終了とする。
  • 開いたマスが爆弾のないマスの場合は、隣接する爆弾の数を表示する。
    • →隣接する爆弾がない場合は、隣接する爆弾が存在するマスまで自動的に表示する。
  • 開いたマスの数字と周囲の旗の数が一致している状態で、数字マスをクリックした場合は、周囲のマスを全て空ける。(便利機能)

開発言語

  • .net 8.0 C#
  • タイマー等のデジタル表示には、「SevenSegment 1.0.0」を利用してます。

とりあえず、完成イメージ

ダウンロード

ソース全体は、

github.com

まずはブロック(マス)を作成
ラベルを継承して、作成してます。
ブロックの状態を管理して、状態が変更された際に適切な描画をするようにしています。
画像等は爆弾と旗はここから貰った画像を加工して使ってそれ以外は文字で表示してます。

using minesweeper.Properties;

namespace minesweeper
{
    enum BlockState
    {
        Opened, // 開いた状態
        Closed, // 閉じた状態
        Flagged, // 旗が立てられた状態
        Flagged_NG, // 旗が間違って立てられた状態
        Bomb, // 爆弾のブロック
        Bomb_OPENDED, // 開いた爆弾のブロック
    }
    internal class Positon(int x, int y)
    {
        public int X { get; set; } = x;
        public int Y { get; set; } = y;
    }
    internal class Block : Label
    {
        public Block()
        {
            TextAlign = ContentAlignment.MiddleCenter;
            BackgroundImageLayout = ImageLayout.Stretch;
            Font = new Font("Arial", 15, FontStyle.Bold);
            AutoSize = false;
            SetImage();
        }

        private bool _IsPressed = false;
        public bool _IsOpened = false;
        private BlockState state = BlockState.Closed;
        // ブロックの状態
        public BlockState State
        {
            get { return state; }
            set
            {
                state = value;
                SetImage();
            }
        }
        // 周辺の爆弾の数
        public int BombCount = 0;
        // 爆弾のブロックかどうか
        public bool IsBomb = false;
        // ブロックの位置
        public Positon Positon = new Positon(0, 0);
        // ブロックが押されたかどうか
        public bool IsPressed
        {
            get { return _IsPressed; }
            set
            {
                _IsPressed = value;
                SetImage();
            }
        }
        // ブロックの画像を設定
        private void SetImage()
        {
            if (_IsPressed)
            {
                BackgroundImage = Resources.block_pressed;
                return;
            }
            switch (state)
            {
                case BlockState.Opened:
                    BackgroundImage = Resources.block_pressed;
                    SetBombCountText();
                    break;
                case BlockState.Closed:
                    BackgroundImage = Resources.block;
                    break;
                case BlockState.Flagged:
                    BackgroundImage = Resources.flag;
                    break;
                case BlockState.Flagged_NG:
                    BackgroundImage = Resources.flag;
                    Text = "X";
                    Font = new Font("Arial", 18, FontStyle.Bold);
                    ForeColor = Color.Red;
                    break;
                case BlockState.Bomb:
                    BackgroundImage = Resources.bomb;
                    BackColor = ColorTranslator.FromHtml("#c6c6c6");
                    break;
                case BlockState.Bomb_OPENDED:
                    BackgroundImage = Resources.bomb;
                    BackColor = Color.Red;
                    break;
            }
        }
        private void SetBombCountText()
        {
            if (BombCount == 0) return;
            Text = BombCount.ToString();
            switch (BombCount)
            {
                case 1:
                    ForeColor = ColorTranslator.FromHtml("#0000F7");
                    break;
                case 2:
                    ForeColor = ColorTranslator.FromHtml("#007C00");
                    break;
                case 3:
                    ForeColor = ColorTranslator.FromHtml("#EC1F1F");
                    break;
                case 4:
                    ForeColor = ColorTranslator.FromHtml("#00007C");
                    break;
                case 5:
                    ForeColor = ColorTranslator.FromHtml("#7C0000");
                    break;
                case 6:
                    ForeColor = ColorTranslator.FromHtml("#007C7C");
                    break;
                case 7:
                    ForeColor = ColorTranslator.FromHtml("#000000");
                    break;
                case 8:
                    ForeColor = ColorTranslator.FromHtml("#7C7C7C");
                    break;

            }
        }
    }
}

次はゲーム画面の作成

InitGameFiled()で盤面を作成しています。
動的に上記で作成したBlockを並べてランダムで爆弾の位置を設定しています。
細かい動作はソースコメントで。
ドラッグ中はドラッグを開始したコントロールが以外イベントが発生しないようなので、
マウス座標から現在のコントロール(マス)を特定するようにしてます。

using minesweeper.Properties;
using Timer = System.Windows.Forms.Timer;

namespace minesweeper
{
    public partial class Form1 : Form
    {
        enum GameStatus
        {
            Ready,
            Playing,
            GameEnd,
        }
        // 横のブロック数
        const int xLength = 9;
        // 縦のブロック数
        const int yLength = 9;
        // 爆弾の数
        const int bombCount = 10;
        // 1ブロックのサイズ
        const int BlockSixe = 50;
        // タイマー
        Timer timer = new Timer();

        GameStatus gameState = GameStatus.Ready;

        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            timer.Interval = 1000; // 1秒
            txtBombCount.Value = "";
            timer.Tick += Timer_tick;
            InitGameFiled();
        }

        /// <summary>
        /// ゲームの経過時間のタイマー
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void Timer_tick(object? sender, EventArgs e)
        {
            this.txtBombTimer.Value = (int.Parse(this.txtBombTimer.Value) + 1).ToString();
        }

        /// <summary>
        /// ゲームフィールドの初期化
        /// </summary>
        private void InitGameFiled()
        {
            this.txtBombCount.Value = bombCount.ToString();
            this.txtBombTimer.Value = "0";
            timer.Stop();
            this.gameState = GameStatus.Ready;
            blockArea.Controls.Clear();
            for (int x = 0; x < xLength; x++)
            {
                for (int y = 0; y < yLength; y++)
                {
                    var label = new Block();
                    label.Name = "Block" + x + y;
                    label.Size = new Size(BlockSixe, BlockSixe);
                    label.Location = new Point(x * BlockSixe, y * BlockSixe);
                    label.Positon = new Positon(x, y);
                    label.MouseUp += Block_MouseUp;
                    // マウスダウン時のイベント。
                    label.MouseDown += (sender, e) =>
                    {
                        if (this.gameState == GameStatus.GameEnd) return;
                        if ((e.Button & MouseButtons.Left) != MouseButtons.Left) return;
                        // ゲーム開始
                        if (this.gameState == GameStatus.Ready)
                        {
                            this.gameState = GameStatus.Playing;
                            timer.Start();
                        }
                        PressBlock(label);
                    };
                    label.MouseMove += Block_MouseMove;

                    blockArea.Controls.Add(label);
                }
            }
            // 爆弾の位置をランダムに決定
            Random rnd = new Random();
            var setBombCount = 0;
            while (setBombCount != bombCount)
            {
                int x = rnd.Next(0, xLength);
                int y = rnd.Next(0, yLength);
                var block = (Block?)blockArea.Controls["Block" + x + y];

                // 既に設定されている場合はスキップ
                if (block.IsBomb)
                {
                    continue;
                }
                setBombCount++;
                block.IsBomb = true;
            }

            // 爆弾の周囲のマスに数字を表示
            foreach (Control control in blockArea.Controls)
            {
                if (control == null && (control is not Block)) return;
                Block block = (Block)control;
                foreach (var target in GetAroundBlok(block))
                {
                    if (target.IsBomb) block.BombCount++;
                }
            }

            this.Size = new Size(BlockSixe * xLength + 20, BlockSixe * yLength + infoArea.Height + SystemInformation.CaptionHeight + 20);
            this.FormBorderStyle = FormBorderStyle.FixedSingle;
        }

        /// <summary>
        /// 指定したブロックの周囲のブロックを取得
        /// </summary>
        /// <param name="block">基準のブロック</param>
        /// <returns></returns>
        private List<Block> GetAroundBlok(Block block)
        {
            List<Block> blocks = new List<Block>();
            for (int x = -1; x <= 1; x++)
            {
                for (int y = -1; y <= 1; y++)
                {
                    if (block.Positon.X + x < 0 || block.Positon.X + x >= xLength || block.Positon.Y + y < 0 || block.Positon.Y + y >= yLength) continue;
                    var target = (Block?)blockArea.Controls["Block" + (block.Positon.X + x) + (block.Positon.Y + y)];
                    if (target == null) return blocks;
                    blocks.Add(target);
                }
            }
            return blocks;
        }

        /// <summary>
        /// 現在のマウスカーソルにあるブロックを取得
        /// </summary>
        /// <returns>対象のブロックのリスト</returns>
        private Block? GetActiveBlock()
        {
            Control? control = blockArea.GetChildAtPoint(blockArea.PointToClient(Cursor.Position));
            if (control == null && (control is not Block)) return null;
            return (Block)control;
        }

        /// <summary>
        /// マウスアップ時のイベント
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Block_MouseUp(object? sender, MouseEventArgs e)
        {
            if (this.gameState == GameStatus.GameEnd) return;
            ResetPreessedBlock(null);
            var block = this.GetActiveBlock();
            if (block == null) return;

            // 左クリックで旗以外をクリックした時
            if ((e.Button & MouseButtons.Left) == MouseButtons.Left && block.State != BlockState.Flagged)
            {
                // 爆弾の場合ゲームオーバー
                if (block.IsBomb)
                {
                    GameOver(block);
                    return;
                }

                // 数字ブロックの場合は一括で開く
                if (block.State == BlockState.Opened && CanBlockOpen(block))
                {
                    OpenBlock(block, true);
                    CheckGameClear();
                    return;
                }

                if (block.BombCount == 0)
                {
                    OpenBlock(block, false);
                }
                else
                {
                    block.State = BlockState.Opened;
                }
            }

            // 右クリックの時は旗をON・OFFする
            if ((e.Button & MouseButtons.Right) == MouseButtons.Right && block.State != BlockState.Opened)
            {
                if (block.State == BlockState.Flagged)
                {
                    block.State = BlockState.Closed;
                }
                else
                {
                    block.State = BlockState.Flagged;
                }
            }

            // クリア判定
            CheckGameClear();
        }

        /// <summary>
        /// 左クリック押した状態でマウス移動時はブロックを押下状態にする
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Block_MouseMove(object? sender, MouseEventArgs e)
        {
            if (this.gameState == GameStatus.GameEnd) return;
            if ((Control.MouseButtons & MouseButtons.Left) != MouseButtons.Left) return;
            var block = GetActiveBlock();
            if (block == null) return;
            if (block.State == BlockState.Opened) return;
            block.IsPressed = true;
            ResetPreessedBlock(block);
        }

        /// <summary>
        /// ブロックの押下状態をリセット
        /// </summary>
        /// <param name="current">対象外にするブロック</param>
        private void ResetPreessedBlock(Block? current)
        {
            foreach (Control control in blockArea.Controls)
            {
                if (control == null && (control is not Block)) return;
                Block block = (Block)control;
                if (block.State == BlockState.Closed && current != control)
                {
                    ((Block)control).IsPressed = false;
                }
            }
        }

        /// <summary>
        /// ブロックを押す(クリックが確定してない情報)
        /// </summary>
        /// <param name="block"></param>
        private void PressBlock(Block block)
        {
            if (block == null) return;
            if (block.State == BlockState.Closed)
            {
                block.IsPressed = true;
            }
            else
            {
                foreach (var target in GetAroundBlok(block))
                {
                    if (target.State == BlockState.Closed)
                    {
                        target.IsPressed = true;
                    }
                }
            }
        }
        /// <summary>
        /// 周囲のブロックを開く。
        /// </summary>
        /// <param name="block">基準のブロック</param>
        /// <param name="isOpenBomb">爆弾のあるブロックは開くか</param>
        private void OpenBlock(Block block, bool isOpenBomb)
        {
            if (block == null) return;

            foreach (var target in GetAroundBlok(block))
            {
                if (target.State != BlockState.Closed) continue;
                if (isOpenBomb)
                {
                    if (target.IsBomb)
                    {
                        GameOver(target);
                        return;
                    }
                }
                else
                {
                    if (target.IsBomb) continue;
                }

                target.State = BlockState.Opened;
                if (target.BombCount == 0)
                {
                    OpenBlock(target, isOpenBomb);
                }
            }
        }

        /// <summary>
        /// 旗と周囲の爆弾の数が一致しているかどうか
        /// </summary>
        /// <param name="block"></param>
        /// <returns></returns>
        private Boolean CanBlockOpen(Block block)
        {
            if (block == null) return false;
            int flagCount = 0;
            foreach (var target in GetAroundBlok(block))
            {
                if (target.State == BlockState.Flagged)
                {
                    flagCount++;
                }
            }
            return block.BombCount == flagCount;
        }

        /// <summary>
        /// ゲームクリアかどうか
        /// </summary>
        private void CheckGameClear()
        {
            int flagCount = 0;
            int openCount = 0;
            int unmacthCount = 0;
            foreach (Control control in blockArea.Controls)
            {
                if (control == null && (control is not Block)) return;
                Block block = (Block)control;
                if (block.State == BlockState.Flagged)
                {
                    flagCount++;
                }
                if (block.State == BlockState.Opened)
                {
                    openCount++;
                }

                if (block.State == BlockState.Flagged && !block.IsBomb)
                {
                    unmacthCount++;
                }
            }
            // 間違った旗の数がないかつ全ての爆弾以外のブロックが開かれている場合はクリア
            if (unmacthCount == 0 && openCount == xLength * yLength - bombCount)
            {
                timer.Stop();
                this.btnGame.BackgroundImage = Resources.button_clear;
                this.gameState = GameStatus.GameEnd;

                foreach (Control control in blockArea.Controls)
                {
                    if (control == null && (control is not Block)) return;
                    Block block = (Block)control;
                    if (block.IsBomb)
                    {
                        block.State = BlockState.Flagged;
                    }
                }
                this.txtBombCount.Value = "0";
            }
            else
            {
                this.txtBombCount.Value = (bombCount - flagCount).ToString();
            }
        }

        /// <summary>
        /// ゲームオーバー
        /// </summary>
        /// <param name="currentBlock"></param>
        private void GameOver(Block currentBlock)
        {
            timer.Stop();
            foreach (Control control in blockArea.Controls)
            {
                if (control == null && (control is not Block)) return;
                Block block = (Block)control;

                // 爆発したブロックを赤くする
                if (block == currentBlock)
                {
                    block.State = BlockState.Bomb_Opened;
                    continue;
                }

                // 間違いの旗を表示
                if (block.State == BlockState.Flagged && !block.IsBomb)
                {
                    block.State = BlockState.Flagged_NG;
                    continue;
                }

                // 未開放の爆弾の位置を表示
                if (block.State != BlockState.Flagged && block.IsBomb)
                {
                    block.State = BlockState.Bomb;
                    continue;
                }
                // 閉じているブロックを開く
                if (block.State == BlockState.Closed)
                {
                    block.State = BlockState.Opened;
                    continue;
                }
            }
            this.btnGame.BackgroundImage = Resources.button_gameorver;
            this.gameState = GameStatus.GameEnd;
        }

        /// <summary>
        /// ゲームリセット
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnGame_Click(object sender, EventArgs e)
        {
            InitGameFiled();
            this.btnGame.BackgroundImage = Resources.button_default;
        }
    }
}

Laravelで外部APIへアクセスする方法

弊社ではフォトシンス社のAkerunというスマートロックと連携するシステムの開発をしています。

akerun.com

AkerunにはAPIが用意されているので、そのAPIを利用してAkerunの情報を取得する等が可能です。
今回はLaravelを利用して外部APIへアクセスする方法についてご紹介いたします。
LaravelにはGuzzleというライブラリがあるので、それを利用してAPIへアクセスします。
docs.guzzlephp.org

Guzzleのインストール

以下のコマンドでGuzzleをインストールします。 (composerはインストールされている前提です)

composer require guzzlehttp/guzzle

API呼び出しの実装

以下に実装例を記載します。

<?php
use GuzzleHttp\Client;

class ApiAccessor
{
    public function getAccesses()
    {
        $url = "https://api.akerun.com/v3/organizations/{ORGANIZATION_ID}/accesses";

        // Guzzleを使ってAPIに接続
        $client = new Client();
        $response = $client->request(
            'GET',
            $url, // URLを設定
            [
                'headers' => 
                [
                    'Authorization' => "Bearer {$accessToken}"
                ],
                'debug' => false,
                'query' => $params, // パラメーターがあれば設定 ?foo=barの場合は $params = ['foo' => 'bar']
            ]
        );

        // レスポンスボディを取得
        $responseBody = $response->getBody()->getContents();
        
        return json_decode($responseBody);
    }
}

GuzzleのClientクラスのrequestメソッドを利用してアクセスします。
requestメソッドには、メソッド(今回はGET)、APIのURL、ヘッダー情報、パラメータ(query)をセットしています。
アクセスして取得したResponseBodyのContentsのJsonをデコードします。
簡単ですね。

最後に

弊社ではAkerunと連携したシステム開発のご対応が可能です。
もし、Akerunと連携したシステム開発についてご相談やご質問・その他ご不明な点などございましたら、以下の問合せフォームよりお気軽にご連絡ください。
その他システム開発についてもお気軽にご連絡ください。
10yro.co.jp

C# Pollyを使った回復力の高いAPIアクセス方法

Pollyとは、APIへのアクセス時のリトライの実装などを助けてくれるライブラリです。
Pollyを使えば、APIのアクセス時に問題が発生した場合に、自動的にリトライすることができます。
この記事では、C#でPollyを使った基本的なAPIアクセス方法について説明します。

NuGetを使ってPollyをインストール

いつものようにNuGetパッケージマネージャーから、Pollyをインストールします。
コマンドでインストールする場合は以下のコマンドを実行してください。

Install-Package Polly

HttpClientの作成

APIアクセスにはHttpClientを使用します。
以下のようにHttpClientを生成します。

var httpClient = new HttpClient();

Policyの作成

Pollyを使うためには、Policyを作成する必要があります。
Policyは、リトライや回復の方法を定義するものです。

以下は、リトライの回数を3回に設定するPolicyの作成例です。

var retryPolicy = Policy
    .Handle<HttpRequestException>()
    .RetryAsync(3);

APIアクセス

Policyを作成したら、APIにアクセスする準備が整いました。
以下は、Pollyを使ったAPIアクセスの例です。

var result = await retryPolicy.ExecuteAsync(async () =>
{
    var response = await httpClient.GetAsync("https://example.com/api/users");
    response.EnsureSuccessStatusCode();
    return await response.Content.ReadAsStringAsync();
});

この例では、HttpClientを使ってhttps://example.com/api/usersにアクセスしています。
リトライの回数が3回に設定されているため、APIアクセスに失敗した場合には、3回まで自動的にリトライされます。

リトライの条件の指定

Pollyでは、リトライの条件を指定することができます。
以下は、ステータスコードが500番台の場合にリトライするPolicyの作成例です。

var retryPolicy = Policy
    .Handle<HttpRequestException>()
    .OrResult<HttpResponseMessage>(r => (int)r.StatusCode >= 500)
    .RetryAsync(3);

このようにすることで、APIアクセスに失敗した場合に、ステータスコードが500番台の場合にはリトライするように設定することができます。

その他のPolicy

以下の公式ドキュメントを御覧ください。

github.com

まとめ

以上が、C#でPollyを使ったAPIアクセス方法の紹介です。
Pollyを使うことで、APIアクセスの際に問題が発生した場合に、自動的にリトライすることができるため、アプリケーションの回復力を高めることができます。

C# SendGridのAPIを利用してメールを送信する

何かしらプログラムからSMTPサーバー経由でメールを送信することがあるかと思いますが、Microsoft365等のサービスだとスパム対策が強化され迷惑メールに振り分けられることが多くなったようです。

今回、自社HP(Wordpress)をリニューアルした際に、問い合わせフォームから送信するメールが迷惑メールになってしまうようだったので、SendGridというメール配信サービスを利用しました。
SendGridについては以下を参照ください。
sendgrid.kke.co.jp

C#からもSendGridを利用できるようでしたのでやってみました。
SendGridのアカウントを持っていない方は、SendGridのアカウントを作成してください。
不正利用されないようにちゃんとした審査があります。

SendGridでAPI Keyの発行

  1. SendGridにログインしたら、左のメニューのSettings→API Keysをクリックします。
  2. 次に画面右上の「Create API Key」ボタンをクリックします
  3. API Key Nameを入力して、API Key PermissionはRestricted Accessを選択します
  4. Access DetailsではMail Sendの下にあるMail SendをONにします
  5. Create & ViewsボタンをクリックするとAPI Keyが生成され、パスワードが表示されますのでコピーして保存しておいてください

Visual StudioでMailkit(メール送信ライブラリ)のインストール

メール送信にMailKitというライブラリを利用します。
Visual Studioでコンソールプロジェクトを作成して、NuGetでMailKitをインストールしてください。

メール送信

具体的なメール送信プログラムは以下となります。
送信先メールアドレス、宛先メールアドレス、認証の部分のパスワードにはSendGridで生成したAPI Keyを設定してください。
実行すればメールが送信されます。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp5
{
    internal class Program
    {
        static void Main(string[] args)
        {
            var msg = new MimeKit.MimeMessage();
            msg.From.Add(new MimeKit.MailboxAddress("テスト送信先", "from@example.com"));
            msg.To.Add(new MimeKit.MailboxAddress("テスト宛先", "to@example.com"));
            msg.Subject = "テストメール";

            var text = new MimeKit.TextPart("Plain");
            text.Text = "テスト本文\r\n改行します。";
            msg.Body = text;

            using (var client = new MailKit.Net.Smtp.SmtpClient())
            {
                try
                {
                    Console.WriteLine("メール送信 start");
                    
                    // 接続
                    client.Connect("smtp.sendgrid.net", 587, MailKit.Security.SecureSocketOptions.Auto);

                    // 認証
                    client.Authenticate("apikey", "SendGridで生成したAPI Keyのパスワード");

                    // 送信
                    client.Send(msg);

                    // 切断
                    client.Disconnect(true);

                    Console.WriteLine("メール送信 end");
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.Message);
                }
            }
        }
    }
}

まとめ

C#からSendGridのAPIを使ってメール送信するプログラムをご紹介しました。
SnedGridを利用することでメールの到達率が高くなるのではないでしょうか。

弊社では一緒に働いてくれるプログラマー、エンジニアの方を募集しています。
未経験者も大歓迎ですので、ご興味ある方は以下よりお問い合わせください。

10yro.co.jp

2022年もありがとうございました

こんにちは。株式会社10yro(トイロ)の中原です。

弊社は本日2022年12月28日(水)が仕事納めとなります。
2023年は1月5日(木)からの営業となります。

しかし、一年経つのはあっという間ですね。
この前自分のSNSを見返していたら、久留米にあるミスタージョージというお店で食べたハンバーグの写真があったのですが、日付を見たら2022年2月でした。
つい最近のことだと思ってたんですが、もう10ヶ月も前のことでした。
何で久留米に行ったのか覚えてないのですが、ミスタージョージでお昼ごはんを食べて、帰りの車でメッセンジャーという芸人さんの「それゆけ!メッセンジャー」というラジオをRadikoで聞いたのを思い出します。

話は変わりますが、会社としては2022年は2期目の年でした。
2022年、会社としては以下のような活動を行いました。

2022年の活動

新規オープンのスポーツクラブの会員管理システムの開発と導入

会員管理システム自体は既存店舗で利用しているものがあったのですが、新規店舗のために機能追加を行い導入しました。
中でもAkerunというリモートロックと会員管理システムを連携する部分は大変でしたが、楽しかったです。
新規スポーツクラブの立ち上げという初めての経験でしたが、あまり大きなトラブルもなくできたかなと思っています。

BCP通知サービス(安否確認サービス)の開発とリリース

お客様からの依頼で開発元という立場でサービスの開発とリリースをしました。
また、1回のリリースで終わらず、バージョンアップも数回行いました。
まだまだ改善や新規機能追加等出てくるかと思いますので、今後もバージョンアップを繰り返してより良いサービスにしていきたいと思っています。

会社ホームページのリニューアル

10yro.co.jp

ずっと後回しになっていたのですが、会社ホームページも良いものができました。
仙台にあるインテグ様にご提案していただき、素敵なホームページを作っていただきました。

creative-integ.jp

2023年は以下の活動に力を入れて行きます。

2023年の活動予定

プログラマー、エンジニアの採用

弊社ではプログラマー、エンジニを募集しています。
未経験者でもプログラマー、エンジニアになりたいという方大歓迎です。
少しでも興味のある方は以下のお問合せフォームよりご連絡いただけると嬉しいです。

10yro.co.jp

サービス開発

上で書いたBCP通知サービスのバージョンアップに加えて、新しいサービスの開発も行う予定です。
自社サービス開発を通じて、今後力を入れていきたい技術にもチャレンジしたいと思っています。

まとめ

少し長くなりましたが、2022年もありがとうございました。
どうぞ良いお年をお迎えください。

C# Windows Hello認証の実装

みなさんご利用されていると思いますが、Windows 10や11にはWindows HelloというPINや顔認証、指紋認証の仕組みがあります。
そのWindows Helloでの認証をWindowsのアプリでも利用することができます。
今回はその実装方法についてです。言語はC#Windows Formsでサンプルアプリを実装したいと思います。

Microsoft.Windows.SDK.Contractsのインストール

Windows Helloを利用するためにNuGetで「Microsoft.Windows.SDK.Contracts」をインストールします。
いつものようにNuGetの管理画面を表示してインストールします。

インストールできました。

Formの作成

とても簡単な以下のような画面を作ります。

「WindowsHello認証」ボタンをクリックすると、Windows Helloの認証が実行され、
認証OKの場合にはメッセージダイアログで「OK」と表示し、
認証NGの場合にはメッセージダイアログで「NG」と表示します。

Windows Hello認証の実装

実装もとても簡単で、以下のような実装になります。

using System;
using System.Windows.Forms;

namespace WindowsHelloSample
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        /// <summary>
        /// ボタンクリック時の処理
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private async void button1_Click(object sender, EventArgs e)
        {
            var available = await Windows.Security.Credentials.UI.UserConsentVerifier.CheckAvailabilityAsync();
            if (available == Windows.Security.Credentials.UI.UserConsentVerifierAvailability.Available)
            {
                var result = await Windows.Security.Credentials.UI.UserConsentVerifier.RequestVerificationAsync("Sampleアプリの認証を行います");
                if (result == Windows.Security.Credentials.UI.UserConsentVerificationResult.Verified)
                {
                    MessageBox.Show("OK");
                } else
                {
                    MessageBox.Show("NG");
                }
            } else
            {
                MessageBox.Show("Windows Helloに対応していない端末です。");
            }
        }
    }
}

Windows.Security.Credentials.UI.UserConsentVerifier.CheckAvailabilityAsync()Windows Helloに対応しているかをチェックし、チェックOKの場合にWindows.Security.Credentials.UI.UserConsentVerifier.CheckAvailabilityAsync()で認証を実行し、戻り値がVerifiedであれば認証OKとなります。

動かしてみる

ボタンをクリックするとWindows Helloのダイアログが表示され、PIN入力または顔認証、指紋認証を行うことができます。

まとめ

認証機能があるWindowsアプリを開発する際は、Windows Hello認証機能も入れてみてはいかがでしょうか。
Windowsと同じように毎回パスワードを入力する手間が省けるので、便利だと思います。