20210909のC#に関する記事は4件です。

【C#】初めてのC#コンソールアプリ作成

概要 VisualStudio2019を使って初めてC#のCUIアプリを作ったときのメモ 対象読者: 他言語の経験はあるけどC#触ったことない人 この記事で学べること↓ クラスや非同期処理その他の文法 コマンドライン引数の解析 CSVファイルの読み込み MySQLとの接続&SQL実行 ※ORMではないです 準備 インストール VisualStudio2019をインストール MySQLをインストール プロジェクト初期化 VisualStudioを開いて新しいプロジェクトの作成 > コンソールアプリ(.NET Core) こんな感じのプロジェクトができる 設計 ざっくりと↓のようなアプリを作ってみたい CUIで動く CSVファイルからユーザーデータを読み込める MySQLにユーザーデータの作成と削除ができる 実装 コマンドライン引数解析機能 ライブラリをインストール ツール > NuGetパッケージマネージャー > パッケージマネージャーコンソールをクリック コンソールが開かれるので↓のコマンドを実行 PM> Install-Package System.CommandLine -Version 2.0.0-beta1.21308.1 コマンドライン引数解析クラス作成 /Services/Command.csクラスを作成して編集 using System; using System.Threading.Tasks; using System.CommandLine; using System.CommandLine.Invocation; namespace ConsoleApp1.Services { public class Command { private RootCommand _command; public Command(Func<string, string, Task<int>> handler) { this._command = this.InitCommand(handler); } // 非同期処理 public async Task<int> Execute(string[] args) { // InvokeAsync()の処理が完了するまで待機 return await this._command.InvokeAsync(args); } private RootCommand InitCommand(Func<string, string, Task<int>> handler) { var rootCommand = new RootCommand(); rootCommand.Description = "C# Console App."; // コマンドライン引数の定義 rootCommand.AddArgument(new Argument<string>("mode", "App mode e.g. create")); rootCommand.AddArgument(new Argument<string>("filePath", "CSV file path e.g. C:\\tmp\\test.csv")); // コマンドライン引数をもとに実行する処理の定義 rootCommand.Handler = CommandHandler.Create<int, string, string>(async (int _, string mode, string filePath) => { await handler(mode, filePath); }); return rootCommand; } } } 補足 クラスのコンストラクタ クラス名のメソッド例) public Command() { }がコンストラクタになる 非同期処理 非同期処理の戻り値は(例えばstringの場合)Task<string>という型にする 非同期処理のメソッドにはasync修飾子をつける 非同期処理が完了するまで待機させたい箇所ではawaitをつける CSVファイル読み込み機能 ライブラリをインストール PM> Install-Package CsvHelper ユーザーデータクラス作成 /Models/UserCSV.csを作成して編集 using CsvHelper.Configuration.Attributes; namespace ConsoleApp1.Models { public class UserCSV { [Name("名前")] public string Name { get; set; } [Name("年齢")] public int Age { get; set; } } } CSV読み込みクラス作成 /Services/CSV.csを作成して編集 using System.Collections.ObjectModel; using System.Threading.Tasks; using CsvHelper; using ConsoleApp1.Models; namespace ConsoleApp1.Services { public class CSV { private CsvReader _reader; public CSV(CsvReader reader) { this._reader = reader; } public async Task<ObservableCollection<UserCSV>> ReadUserCSV() { // UserCSVデータを入れるコレクションを生成 var userCsvs = new ObservableCollection<UserCSV>(); await Task.Run(() => { // usingステートメント using (this._reader) { this._reader.Read(); this._reader.ReadHeader(); var records = this._reader.GetRecords<UserCSV>(); foreach (var record in records) { userCsvs.Add(new UserCSV() { Name = record.Name, Age = record.Age }); } }; }); return userCsvs; } } } 補足 usingステートメント アンマネージドリソース(ファイルなど)の使用権取得→破棄の処理を自動でやってくれる 今回の場合CSVファイルの使用権取得から破棄までをusingステートメントで制御させている MySQL操作機能 本当はORM使いたかったけどどれがいいのかよくわからなかったので愚直にSQL文叩いていく (そのうちC#の良さげなORM調査してみる) ライブラリをインストール PM> Install-Package MySql.Data MySQL操作クラス /Services/MySQL.csを作成して編集 using MySql.Data.MySqlClient; using ConsoleApp1.Models; namespace ConsoleApp1.Services { // struct public struct MySQLConfigs { public string server; public string database; public string user; public string password; public string characterCode; } public class MySQL { private MySQLConfigs _configs; private MySqlConnection _connection; public MySQL(MySQLConfigs configs) { this._configs = configs; this._connection = new MySqlConnection(this.BulildConnectionString()); } public void CreateTable(string tableName) { this._connection.Open(); var command = new MySqlCommand(); command.Connection = this._connection; command.CommandText = $"CREATE TABLE IF NOT EXISTS {this._configs.database}.{tableName} (id INT NOT NULL PRIMARY KEY AUTO_INCREMENT, name VARCHAR(32), age INT)"; command.ExecuteNonQuery(); this._connection.Close(); } public void CreateUser(string tableName, UserCSV user) { this._connection.Open(); var command = new MySqlCommand(); command.Connection = this._connection; command.CommandText = $"INSERT INTO {tableName} (name, age) VALUES (\"{user.Name}\", {user.Age})"; command.ExecuteNonQuery(); this._connection.Close(); } public void DeleteUser(string tableName, UserCSV user) { this._connection.Open(); var command = new MySqlCommand(); command.Connection = this._connection; command.CommandText = $"DELETE FROM {tableName} WHERE name = \"{user.Name}\" AND age = {user.Age}"; command.ExecuteNonQuery(); this._connection.Close(); } private string BulildConnectionString() { string connectionString = $"Server={this._configs.server};Database={this._configs.database};Uid={this._configs.user};Pwd={this._configs.password};Charset={this._configs.characterCode}"; return connectionString; } } } やっていることは大体↓みたいな感じ MySQLのコンフィグ作成 DBとコネクション貼る コネクションをOpenにする SQL実行する コネクションをCloseする 補足 struct(構造体) いくつかの変数をひとまとめにして扱うためのもの 今回はMySQLのコンフィグをstructで定義している Main関数 Program.csを編集 using System; using System.IO; using System.Text; using System.Globalization; using System.Threading.Tasks; using CsvHelper; using ConsoleApp1.Services; namespace ConsoleApp1 { class Program { static async Task Main(string[] args) { var command = new Command(Handler); await command.Execute(args); } private static async Task<int> Handler(string mode, string filePath) { if (mode != "create" && mode != "delete") { Console.WriteLine($"Appliction mode \"{mode}\" is invalid."); return 1; } var cultureInfo = new CultureInfo("ja-JP"); var streamReader = new StreamReader(filePath, Encoding.GetEncoding("utf-8")); var csvReader = new CsvReader(streamReader, cultureInfo); var csv = new CSV(csvReader); var userRecords = await csv.ReadUserCSV(); Console.WriteLine($"{userRecords.Count} users found"); var configs = new MySQLConfigs() { database = "csharp_test", server = "localhost", user = "root", password = "password", characterCode = "utf8" }; var mysql = new MySQL(configs); mysql.CreateTable("users"); foreach (var user in userRecords) { if (mode == "create") { Console.WriteLine($"Create user \"{user.Name}\""); mysql.CreateUser("users", user); } else if (mode == "delete") { Console.WriteLine($"Delete user \"{user.Name}\""); mysql.DeleteUser("users", user); } } return 0; } } } 動作確認 コマンドライン引数の設定 アプリ実行時の引数をVisualStudio上で設定できる ソリューションエクスプローラー > ConsoleApp1右クリック > プロパティ > デバッグクリック アプリケーション引数に引数を入力(複数入力する場合は半角スペースを挿入) 今回は↓を入力 create ${CSVファイルのパス} ビルド ビルド > ソリューションのビルドをクリックして実行 実行前の準備 MySQLでcsharp_testという名前のデータベースを作る ↓のようなCSVファイルを作る "名前","年齢" "太郎",20 "次郎",19 実行 テーブルにユーザーが追加されていればOK 参考記事 大変参考になりました!? https://qiita.com/Shiho_anyplus/items/0f5bfbf73d9512802d0a https://qiita.com/TsuyoshiUshio@github/items/02902f4f46f0aa37e4b1 https://qiita.com/4_mio_11/items/145c658078a7fe5f36a7 https://qiita.com/tnishiki/items/bfe0978592e023099588 https://qiita.com/hdk-t/items/403b3c479eb4197a92b1
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

突然 Clean Architecture って言われても

2021.1 前振り コロナなご時世なので、ずっと、リモートワークなのに、急に呼び出されんたですよね。 進捗会議の日程だったので深く考えず、出かけてみたら… 次期開発に先だって、現状の作りである、Clean Architectureについて現状に沿って説明してほしいとか! いゃ、突然ですか? 実装が、この図のどこに相当するか説明してってて言われても… 無理です で、こっちで Javaによるwebアプリの標準的なインプリメントなので、そんなには乖離がないんですね。 でも、みんなどうしてるんだろうと、ブログを徘徊してみました 現状の実装 DIの登録はStartupにべた書きです なんちゃってSingle Pageで、WebとAPIを分けています 現場の決定なので、Entity Frameworkです Dapper 使ったことないからダメなんだそうです でも、誰かが生のado.net 使っています でも、Entity Frameworkはrepository層でラップしてあるので とはいえ、DbContextはDIしているので、見えちゃうんですがw ていうか、もともとあるクラス名が意味のない機能IDなので… クラス間の受け渡しのデーターがPOCOじゃなくってinterfaceだったりしたのは書き直したのですが まあ、どのフォルダーに何を置くのかという、レベルの理解でしかない?ので。 あとは、どのクラスに何を追加していくのか? いゃ、まとめるのではなく、分けるのだというと 要は、utilとかcommonがないというのが… あと、定数クラスは作らないというのが、理由から説明しないと駄目で。 Deep changeを目指してのトライアルなのだけと、今までの、汎用コンピューター的な、仕事の進め方と違いすぎるから、きっと、作業が進むにつれて劣化してい行くのでしょう
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

#PowerApps カスタムコネクタのC#カスタムコードで遊んでみた

概要 プレビュー段階ですが、PowerApps/Automateのカスタムコネクタ内でC#コードを使って処理が記述できるようになりました。 C#のコードで外部APIからの応答を整形したり、APIへの要求はせずC#内で処理して応答するといったようなことが可能です。 これにより、以前はAzure Functionsで実装したようなAPI処理がカスタムコネクタ内で完結できてしまいます(簡単なものなら)。 やってみた事例 Power Appsからテキストを受け取ってオウム返しする。 Power Appsから画像を受け取り簡単な画像処理を行ってPower Appsに応答する。 外部APIへのリクエスト時にAuthorizationヘッダを生成して付加、カスタムコネクタでOAuth1.0のAPIに接続する。 呼び出し初回に数秒の起動時間あり、裏側はAzure Functions無料枠? 制約条件など カスタムコードには以下のような制約事項がありできることが限られます。 C#の中でも比較的簡単なコードしか書けないということで、これもローコードの一つですね(違 コードはScriptクラス内に書く必要がある 使用可能なnamespaceに制限がある 5 秒以内に実行を終了、コードサイズが1MBまでなどの制限がある それからカスタムコネクタなので、PowerAppsの開発者環境か有償プランが必要です。 試しに作ってみる 今回のコード リクエストボディに入力されたテキストをそのまま返す。 追加でContextやRequestの内容を確認用に表示する。 カスタムコネクタの登録時にバックエンドAPIのURLを登録しますが、今回の例ではバックエンドにはアクセスしません。処理内容はコードでオーバーライドします。 コード public class Script: ScriptBase { public override async Task<HttpResponseMessage> ExecuteAsync() { // Create a new response var response = new HttpResponseMessage(); var contentAsJson = JObject.Parse(await Context.Request.Content.ReadAsStringAsync().ConfigureAwait(false)); // Set the content // Initialize a new JObject and call .ToString() to get the serialized JSON response.Content = CreateJsonContent(new JObject { ["message"] = (string)contentAsJson["message"], ["request_uri"] = Context.Request.RequestUri, ["operation_id"] = Context.OperationId, ["correlation_id"] = Context.CorrelationId, ["content"] = await Context.Request.Content.ReadAsStringAsync() }.ToString()); return response; } } 簡単な解説 ScriptBaseクラスを継承したScriptクラスがあり、カスタムコネクタが呼び出されるとExecuteAsyncメソッドが実行されます。 ExecuteAsyncはoverrideしているため、カスタムコネクタの処理内容はこのメソッド内のコードに上書きされます。 ContextやCreateJsonContentなどが登場しますが、これはベースクラスで定義されているものです。 Contextにはカスタムコネクタの情報やリクエスト情報が格納されています。 ScriptBaseの中身が気になるところです。これは公式で公開されています。 以下の開発環境を準備すると、開発時にインテリセンスが表示されます。 カスタムコードの開発環境 現状はカスタムコネクタ上でコードを書いてもコンパイルエラーの内容を教えてくれないため、公式に従い自分で開発環境を整えます。 今回はVisualStudioでテスト環境を整えてみます。 方法は以下にのブログに投稿しました。 またはこちらのリポジトリをクローンで https://github.com/Rambosan/CustomCodeTestEnv カスタムコネクタの作成 カスタムコネクタの基本操作についてはこちらが詳しいです。 https://qiita.com/MiyakeMito/items/800018d7642c0e4e86f3 https://www.youtube.com/watch?v=APNjMks5tRw カスタムコネクタの画面で一から作成を選択します。 全般画面 ホストの部分は架空の「api.contoso.com」としておきます。 今回は実際のAPIは呼び出さずC#内で処理を行って応答しますが、形式上指定する必要があるためです。 ※設定保存のやり方によってはC#コードが無効になり入力したURLにリクエストされるため、送信されてもいいURLで。 セキュリティ →無しでOKです。 定義画面 新しいアクションを追加し「Hello」などのアクションを作成します。 要求内容はサンプルからのインポートで以下のように設定します。 5.コード画面 コードが有効をオンにして、作成したコードを貼り付けます。 特定のアクションのみコードを有効にできるようですが、今回は指定しません。 ここで必ずコネクタの更新をクリックします。 カスタムコードを有効にしていますと表示され、その後正常に保存できたことを確認します。 現状、他の画面でコネクタの更新を行ってしまうと、コードが無効になり指定のURIに要求されてしまいます。 なので最後は必ずこの画面で、一旦コードを編集してからコネクタの更新を行います。 ※コードを編集せず更新をすると反映されない場合あり もしコードが反映されていないとテストのときに失敗し、api.contoso.comの名前解決ができないと表示されます。 6.テスト画面 接続が作成されていない場合は接続を作成。 テストの前には上記の通り、最後はコード画面で編集し、コネクタの更新をクリックしてコードを反映させます。 パラメータを適当に設定して、テスト操作します。 以下のように応答が返ってくれば成功です。 OperationIdは操作IDが入るようです。CorrelationIdは分かりません・・ 7.応答の定義 先ほどのボディをコピーし、応答部分にインポートして定義を作成します。 最後は必ずコード画面でコードを編集して更新します。 PowerAppsからの呼び出し アプリを作成し、先ほど作成したカスタムコネクタへの接続を作成します。 以下のような感じで呼び出すと、応答結果が得られました! その他 Jsonをクラスにデシリアライズする Script以外にClassを書けないため、普通にJsonConvert.DeserializeObject(body)を使用するとエラーとなります。 読みにくいですが以下のようにClassの入れ子を使用すると通常通りデシリアライズできるようです。 public class Script : ScriptBase { public override async Task<HttpResponseMessage> ExecuteAsync() { // Parse request body var entity = JsonConvert.DeserializeObject<PersonEntity>(await Context.Request.Content.ReadAsStringAsync().ConfigureAwait(false)); // Create a new response var response = new HttpResponseMessage(); response.Content = CreateJsonContent(new JObject { ["fullname"] = entity.FullName }.ToString()); return response; } class PersonEntity { public string SurName { get;} public string GivenName { get;} public string FullName => $"{SurName} {GivenName}"; [JsonConstructor] public PersonEntity(string surname, string givenname) { SurName = surname; GivenName = givenname; } } } あとがき カスタムコネクタ内でAPIを完結させるとPowerAppsの料金だけでよいですし、リソース管理が楽になりますね。 色々な活用の可能性がありそうです。 値下げして大丈夫なのか・・ 次回の予定 APIへリクエストする場合の例 画像処理の例 カスタムコネクタでOAuth1.0のAPIに接続してみた
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

#PowerApps カスタムコネクタのC#カスタムコードで遊んでみる

概要 プレビュー段階ですが、PowerApps/Automateのカスタムコネクタ内でC#コードを使って処理が記述できるようになりました。 C#のコードで外部APIへの要求/応答に手を加えたりすることが可能です。 更に、外部APIには要求せずC#内で処理して応答といったことも可能で、以前はAzure Functionsで実装したような処理をカスタムコネクタ内で完結させるといったことも可能です! やってみた事例 Power Appsからテキストを受け取ってオウム返しする。 Power Appsから画像を受け取り簡単な画像処理を行ってPower Appsに応答する。 外部APIへのリクエスト時にAuthorizationヘッダを生成して付加、カスタムコネクタでOAuth1.0のAPIに接続する。 制約条件など カスタムコードには以下のような制約事項がありできることが限られます。 C#の中でも比較的簡単なコードしか書けないということで、これもローコードかもしれません(違 コードは全てScriptクラス内に書く必要がある 使用可能なクラスライブラリに制限がある 5 秒以内に実行を終了、コードサイズが1MBまでなどの制限がある 呼び出し初回に数秒の起動時間あり、裏側はAzure Functions無料枠? それからカスタムコネクタなので、PowerAppsの開発者環境か有償プランが必要です。 試しに作ってみる 今回のコード リクエストボディに入力されたテキストをそのまま返す。 追加でContextやRequestの内容を確認用に表示する。 カスタムコネクタの登録時にバックエンドAPIのURLを登録しますが、今回の例ではバックエンドにはアクセスしません。処理内容はコードでオーバーライドします。 コード public class Script: ScriptBase { public override async Task<HttpResponseMessage> ExecuteAsync() { // Create a new response var response = new HttpResponseMessage(); var contentAsJson = JObject.Parse(await Context.Request.Content.ReadAsStringAsync().ConfigureAwait(false)); // Set the content // Initialize a new JObject and call .ToString() to get the serialized JSON response.Content = CreateJsonContent(new JObject { ["message"] = (string)contentAsJson["message"], ["request_uri"] = Context.Request.RequestUri, ["operation_id"] = Context.OperationId, ["correlation_id"] = Context.CorrelationId, ["content"] = await Context.Request.Content.ReadAsStringAsync() }.ToString()); return response; } } 簡単な解説 ScriptBaseクラスを継承したScriptクラスがあり、カスタムコネクタが呼び出されるとExecuteAsyncメソッドが実行されます。 ExecuteAsyncはoverrideしているため、カスタムコネクタの処理内容はこのメソッド内のコードに上書きされます。 ContextやCreateJsonContentなどが登場しますが、これはベースクラスで定義されているものです。 Contextにはカスタムコネクタの情報やリクエスト情報が格納されています。 https://docs.microsoft.com/ja-jp/connectors/custom-connectors/write-code#definition-of-supporting-classes-and-interfaces 以下に開発環境を用意しました。 カスタムコードの開発環境 現状はカスタムコネクタ上でコードを書いてもコンパイルエラーの内容を教えてくれないため、公式に従い自分で開発環境を整えます。 今回はVisualStudioでテスト環境を整えてみます。 方法は以下にのブログに投稿しました。 またはこちらのKUSOリポジトリをクローンで カスタムコネクタの作成 カスタムコネクタの基本操作についてはこちらが詳しいです。 カスタムコネクタの画面で一から作成を選択します。 全般画面 ホストの部分は架空の「api.contoso.com」としておきます。 今回は実際のAPIは呼び出さずC#内で処理を行って応答しますが、形式上指定する必要があるためです。 ※設定保存のやり方によってはC#コードが無効になり入力したURLにリクエストされるため、送信されてもいいURLで。 セキュリティ →無しでOKです。 定義画面 新しいアクションを追加し「Hello」などのアクションを作成します。 要求内容はサンプルからのインポートで以下のように設定します。 5.コード画面 コードが有効をオンにして、作成したコードを貼り付けます。 特定のアクションのみコードを有効にできるようですが、今回は指定しません。 ここで必ずコネクタの更新をクリックします。 カスタムコードを有効にしていますと表示され、その後正常に保存できたことを確認します。 現状、他の画面でコネクタの更新を行ってしまうと、コードが無効になり指定のURIに要求されてしまいます。 なので最後は必ずこの画面で、一旦コードを編集してからコネクタの更新を行います。 ※コードを編集せず更新をすると反映されない場合あり もしコードが反映されていないとテストのときに失敗し、api.contoso.comの名前解決ができないと表示されます。 6.テスト画面 接続が作成されていない場合は接続を作成。 テストの前には上記の通り、最後はコード画面で編集し、コネクタの更新をクリックしてコードを反映させます。 パラメータを適当に設定して、テスト操作します。 以下のように応答が返ってくれば成功です。 OperationIdは操作IDが入るようです。CorrelationIdは分かりません・・ 7.応答の定義 先ほどのボディをコピーし、応答部分にインポートして定義を作成します。 最後は必ずコード画面でコードを編集して更新します。 PowerAppsからの呼び出し アプリを作成し、先ほど作成したカスタムコネクタへの接続を作成します。 以下のような感じで呼び出すと応答結果が得られました。 以下その他 クラスを利用する Script以外にClassを書いて呼び出してもエラーとなります。 読みにくいですがClassの入れ子なら利用可能です。以下はJSONをデシリアライズする例です。 public class Script : ScriptBase { public override async Task<HttpResponseMessage> ExecuteAsync() { // Parse request body var entity = JsonConvert.DeserializeObject<PersonEntity>(await Context.Request.Content.ReadAsStringAsync().ConfigureAwait(false)); // Create a new response var response = new HttpResponseMessage(); response.Content = CreateJsonContent(new JObject { ["fullname"] = entity.FullName }.ToString()); return response; } class PersonEntity { public string SurName { get;} public string GivenName { get;} public string FullName => $"{SurName} {GivenName}"; [JsonConstructor] public PersonEntity(string surname, string givenname) { SurName = surname; GivenName = givenname; } } } APIを呼び出して応答を変形する(情報を削減) QiitaAPIを呼び出し、渡されたワードで記事を検索して結果を返す例です。 標準の応答スキーマに不要の情報がありPowerAppsで扱いにくいといった場合に、カスタムコネクタ側で応答を加工し情報を削減します。 QiitaAPIの呼び出し定義はカスタムコネクタ側で設定します。 要求の例 情報を削減した応答 public class Script : ScriptBase { public override async Task<HttpResponseMessage> ExecuteAsync() { // カスタムコネクタで定義したAPIを呼び出し var responseMessage = await Context.SendAsync(Context.Request, CancellationToken).ConfigureAwait(false); if (responseMessage.StatusCode !=HttpStatusCode.OK ) { throw new ArgumentException(responseMessage.ReasonPhrase); } // レスポンスのコンテンツを取得 var requestBody = await responseMessage.Content.ReadAsStringAsync(); // Parse request body var entities = JsonConvert.DeserializeObject<List<QiitaEntity>>(requestBody); // Create a new response var response = new HttpResponseMessage(); response.Content = CreateJsonContent(JsonConvert.SerializeObject(entities)); return response; } //特定のフィールドしか用意しないことで不要なフィールドを削除 class QiitaEntity { public string Title { get; private set; } public string Body { get; private set; } public DateTime UpdateAt { get; private set; } public int LikesCount { get; private set; } public string Url { get; private set; } [JsonConstructor] public QiitaEntity(string title, string body, DateTime updated_at, int likes_count, string url) { Title = title; var singleLineBody = Regex.Replace(body, @"\t|\n|\r", ""); Body = SubstringSafe(singleLineBody, 0, 100); UpdateAt = updated_at; LikesCount = likes_count; Url = url; } static string SubstringSafe(string value, int startIndex, int length) { return new string((value ?? string.Empty).Skip(startIndex).Take(length).ToArray()); } } } あとがき カスタムコネクタ内でAPIを完結させるとPowerAppsの料金だけでよいですし、リソース管理が楽になりますね。 色々な活用の可能性がありそうです。 値下げして大丈夫なのか・・ PowerAppsにない関数をカスタムコードで実現!と思いましたがPowerAppsでもかなりのことができるようですので、カスタムコードでしかできないことが思いつきませんね・・ 次回の予定 画像処理の例 カスタムコネクタでOAuth1.0のAPIに接続してみた
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む