gmail の SMTP を使ってメールを送信する

クライアントからメールを送信しようとするとき、何処の SMTP を踏み台にして送信しようか悩む。SMTP によって、一度 POP3 しないと駄目だったり、セキュリティ上外部受付していなかったりして、汎用的なモノがない…のだが、結構以前から gmail の smtp.gmail.com が使われていたりする。で、もう一度試しておこうと思って、System.Net.Mail.SmtpClient を使って送っていたのだが、

MailKitが公式に.NETのSmtpClientを置き換えることを明らかにした。
https://www.infoq.com/jp/news/2017/04/MailKit-MimeKit-Official

の問題もあったり、何故か gmail のほうで受け付けなくなったりしていて。

ということで、MailKit で置き換えたものを晒しておきます。

C#でSMTP(STARTTLS と SMTP over SSL)でメール送信する – YoshinoriN’s Memento
https://yoshinorin.net/2016/10/29/csharp-mail-smtps-starttls/

のコードを gmail に変えただけです。

static void Main(string[] args)
{
    Console.WriteLine("Hello SMTP World!");

    string id = "<gmailのログインID>";
    string pass = "<gmailのパスワード>";
    string from = "<宛先>";
    string to = "<自分のメール>";
    string subject = "送信テスト : " + DateTime.Now.ToString();
    string body = "from t.masuda";

#if false
    var smtp = new System.Net.Mail.SmtpClient();
    smtp.Host = "smtp.gmail.com"; //SMTPサーバ
    smtp.Port = 587;              //SMTPポート
    smtp.EnableSsl = true;
    smtp.Credentials = new System.Net.NetworkCredential(id, pass); //認証
    var msg  = new System.Net.Mail.MailMessage(from, to, subject, body);
    smtp.Send(msg); //メール送信
#else           

    var smtp = new MailKit.Net.Smtp.SmtpClient();
    smtp.Connect("smtp.gmail.com", 587, SecureSocketOptions.Auto);
    smtp.Authenticate(id, pass);

    var mail = new MimeKit.MimeMessage();
    var builder = new MimeKit.BodyBuilder();

    mail.From.Add(new MimeKit.MailboxAddress("", from));
    mail.To.Add(new MimeKit.MailboxAddress("", to));
    mail.Subject = subject;
    builder.TextBody = body + " by gmail";
    mail.Body = builder.ToMessageBody();

    smtp.Send(mail);
    smtp.Disconnect(true);
#endif

    Console.WriteLine("メールを送信しました");
}

NuGet で MailKit をインストールすると、.net standard 対応なので、.net core と .net framework のどちらでもメールを送れるようになります。

カテゴリー: 開発, C# | gmail の SMTP を使ってメールを送信する はコメントを受け付けていません

.NET Coreで作成した Web APIアプリを Windows サービスで動かす

基本は↓に書いてあるのだけど、所々間違っている?というか、バージョンアップあたりで動かなくなっているので、ポイントだけメモ書き。

Windows サービスで ASP.NET Core をホストします。
https://docs.microsoft.com/ja-jp/aspnet/core/host-and-deploy/windows-service?view=aspnetcore-2.0&tabs=aspnetcore2x

https://github.com/aspnet/Docs/tree/master/aspnetcore/host-and-deploy/windows-service/sample

*.csprojでターゲットを.NET Frameworkにする

ASP.NET Core + .NET Core で作成したプロジェクト(*.csproj)を開いて、TargetFramework を「netcoreapp2.0」から「net461」に変える。

  <PropertyGroup>
    <TargetFramework>netcoreapp2.0</TargetFramework>
  </PropertyGroup>

から

  <PropertyGroup>
    <TargetFramework>net461</TargetFramework>
    <RuntimeIdentifier>win7-x64</RuntimeIdentifier>
  </PropertyGroup>

に変更する。

これは Windows サービスを使うために .NET Framework に切り替えているので、Linux上で動かす場合は、netcoreapp2.0 で使うことになる。

NuGetパッケージを変更する

ASP.NET Core を使っているところを、.NET Core から .NET Framework のものに切り替える。あと「Microsoft.AspNetCore.Hosting.WindowsServices」を追加して、Windows サービスで起動するための RunAsService() を使えるようにしておく。

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.8" />
  </ItemGroup>

から

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore" Version="2.0.3" />
    <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.0.4" />
    <PackageReference Include="Microsoft.AspNetCore.Hosting.WindowsServices" Version="2.0.3" />
  </ItemGroup>

Program.csを書き替える

ASP.NET Core + .NET Core の場合には、IWebHost インターフェースを使っているのだが、面倒なので、Main に書き替えてしまう。

public class Program
{
    public static void Main(string[] args)
    {
        BuildWebHost(args).Run();
    }

    public static IWebHost BuildWebHost(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .UseStartup<Startup>()
            .Build();
}

から

using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.WindowsServices;

public class Program
{
    public static void Main(string[] args)
    {
        bool isService = false;
        if (Debugger.IsAttached || args.Contains("--console"))
        {
            isService = false;
        }
        if ( args.Contains("--service"))
        {
            isService = true;
        }
        // パラメータを消しておく
        args = new string[] { };

        if (isService)
        {
            // サービスで動作させる
            var pathToExe = Process.GetCurrentProcess().MainModule.FileName;
            var pathToContentRoot = Path.GetDirectoryName(pathToExe);
            var host =
                WebHost.CreateDefaultBuilder(args)
                .UseContentRoot(pathToContentRoot)
                .UseStartup<Startup>()
                .Build();
            host.RunAsService();
        }
        else
        {
            // コンソールアプリで起動する
            var host = 
                WebHost.CreateDefaultBuilder(args)
                .UseStartup<Startup>()
                .UseUrls($"http://*:{port}")
                .Build();
            host.Run();
        }
    }
}

起動スイッチを付けて通常起動とサービスの起動に切り替えている。MSのサンプルでは、デフォルトでサービス起動になっているのだが、こちらは「–service」を付けたときだけ、Windows サービスで起動するようにしている。

WebHost.CreateDefaultBuilder に不要なパラメータを渡すと落ちるので、args の中身を消しておく(なんとなくバグ臭いのだが)。

サービスで起動する

サービスを登録するために sc コマンドを管理者権限のコマンドプロンプトで実行する。

sc create UsbService binpath="D:\work\SG\git\usbblocker\UsbBlocker.Web\bin\Debug\net461\win7-x64\UsbBlocker.Web.exe --service"

binpath には、フルパスを指定する。スイッチを付けるためにダブルクォートで囲ってある。サービス名は UsbService としているが、ダブらないように自前で名前をつける。

サービスの起動は sc start でできる。

sb start UsbBlockerService

サービスを削除するときは sc delete する。

sb delete UsbBlockerService
カテゴリー: 開発, C# | .NET Coreで作成した Web APIアプリを Windows サービスで動かす はコメントを受け付けていません

OpenPop.NET を Xamarin.Forms で使う

常々、スパムメールが沢山あってスマホでメールが受け取れないのにイライラしている訳だが。スマホで閲覧するときは、全てを受け取りたいのではなくて重要そうな(最初に決められた相手とか)メールだけ見たいので、スパムもそうだけど広告メールも外したかったりする。いわゆる、自分なりのホワイトリストが作っておきたい。

つまりは自前のメーラーアプリがあればいいのだけど、(確か)iOSの場合は規約上作れないのでストアには出て来ない。
ならば、作ればいいだろう、ということ。

OpenPop.NET

簡単なPOP3の実装ならばそれほど難しくはないのだけど、できるならば既に実績があるものがよい。

OpenPop.NET – Robust POP3 client and MIME parser written in C#
http://hpop.sourceforge.net/

どうやら .NET Framework 2.0 の頃からある古い実装なのだけど、サンプルもあるのでそのまま使えそう。

ってなわけで、.NET Standard 2.0 でビルドし直してみる、と

moonmile/hpop: OpenPOP.NET code repository
https://github.com/moonmile/hpop

あっさりできてしまったので、これを Xamarin.Forms に組み込む。

Xamarin.Forms+OpenPop.NET

using OpenPop.Pop3;
using System.Net.Mail;

public partial class MainPage : ContentPage
{
	public MainPage()
	{
		InitializeComponent();
        this.LayoutChanged += MainPage_LayoutChanged;
	}

    private void MainPage_LayoutChanged(object sender, EventArgs e)
    {
        var client = new Pop3Client();
        client.Connect("ホスト名", 110, false);
        client.Authenticate(
            "ユーザー名", 
            "パスワード", 
            AuthenticationMethod.UsernameAndPassword);
        int messageCount = client.GetMessageCount();

        var items = new List<MailMessage>();
        int cnt = 0;
        for ( int i = messageCount; i>0; i--  )
        {
            /// 30 件で区切る
            if (cnt++ >= 30) break;

            try
            {
                var msg = client.GetMessage(i);
                if (msg != null)
                    items.Add(msg.ToMailMessage());
            }
            catch { }
        }
        this.lv.ItemsSource = items;

    }
}

途中で try-catch しているのは、Form のコード変換あたりでエラーになっているから。デコードがうまくいかないようなので、ToMailMessageメソッドの実装は自前で行ったほうがよいかもしれない(日本語のコード変換だからだろう)。

ひとまず、ListView にバインドさせるとこんな感じになる。

どうやって使うか?

ちゃんとしたメーラーアプリ標準のものを使う…というか標準のしか使えないので、まるっきり自作アプリとして使う。スマホのメーラーは振り分けが貧弱なので、そのあたりを補えばいい。

  • From を見て、特定の人だけを表示させる
  • Subject に特定の文字がでてきたもののみ表示させる。

な感じでいいだろう。返信するときはコピペしてから標準アプリに受け渡せばいい。

カテゴリー: 開発, C#, Xamarin | OpenPop.NET を Xamarin.Forms で使う はコメントを受け付けていません

ADO.NET Entitiy Data Model を F# で使うための裏技

MySQLやSQLiteでEFが使えるならば、F#からもEFを使えるだろう、ということで探していたのだけど、

F# で Entity Framework Coreる – pocketberserkerの爆走
http://pocketberserker.hatenablog.com/entry/2017/12/06/000405

確かに、Entity Classを生成するコードが F# にはない…というか T4 自体が C# だけしか使えないようになっているので、F# では手作業で Model クラスを作らねばいけないっぽい。
のだが、

EFのクラスをMVVMのINotifyPropertyChangedに対応させる裏技 | Moonmile Solutions Blog http://www.moonmile.net/blog/archives/9030

で試したように T4 のファイル(*.tt)を直接書き替えることによって、生成するコードを自前で作ることができるんですよ。となると、ここで C# の Model クラスを出力している部分を、F# に書き替えればよい。

  1. C# のプロジェクトに ADO.NET Entitiy Data Model を追加
  2. *.tt を F# 用に書き替える。
  3. 生成すると *.fs ファイルができる。
  4. 生成した F# のファイルを、F# のプロジェクトにコピーして使う

という手順でいけるだろう、ってことで、作ってみたのがこれ。

http://github.com/moonmile/sample-fsharp-ef/

C# プロジェクトの Model.tt と Model1.Context.tt を F# コードを出力するように書き替える。本来ならば、F# プロジェクトで TextTemplatingFileGenerator が使えればよいのだけど駄目なので、仕方がないので生成後のコード(Model1.Context.fs, projects.fs, issues.fs)を F# プロジェクトにリンクする。

まずは、普通にC#プロジェクトでADO.NET Entitiy Data Modelを追加する

C#プロジェクトを作って、MySQLのprojectsテーブルとissuesテーブルのエンティティクラスを作る

ここで、使われている T4のファイル(Model1.Context.tt, Model1.tt)を書き替える

Model1.Context.tt, Model1.ttを書き替えてF#のコードを出力

Model1.tt

https://github.com/moonmile/sample-fsharp-ef/blob/master/src/SampleFSharpEF.FSharp/SampleFSharpEF.Model/Model1.tt

ちょっと乱暴だが、CodeStringGenerator::PropertyとEntityClassOpeningを書き替えて、F#のレコード型が出力されるようにする。

public class CodeStringGenerator
{
	...
    public string Property(EdmProperty edmProperty)
    {
        return string.Format(
            CultureInfo.InvariantCulture,
            // "{0} {1} {2} {{ {3}get; {4}set; }}",
            "{2} : {1} ",
            Accessibility.ForProperty(edmProperty),
            _typeMapper.GetTypeName(edmProperty.TypeUsage),
            _code.Escape(edmProperty),
            _code.SpaceAfter(Accessibility.ForGetter(edmProperty)),
            _code.SpaceAfter(Accessibility.ForSetter(edmProperty)));
    }
	...
    public string EntityClassOpening(EntityType entity)
    {
        return string.Format(
            CultureInfo.InvariantCulture,
			"[<CLIMutable>]" + Environment.NewLine +
            "type {2}{3} =",
            Accessibility.ForType(entity),
            _code.SpaceAfter(_code.AbstractOption(entity)),
            _code.Escape(entity),
            _code.StringBefore(" : ", _typeMapper.GetTypeName(entity.BaseType)));
    }

Model1.Context.tt

https://github.com/moonmile/sample-fsharp-ef/blob/master/src/SampleFSharpEF.FSharp/SampleFSharpEF.Model/Model1.Context.tt

DbContext を継承するクラスを作成する部分を書き替えてしまう。

type <#=code.Escape(container)#>() = 
    inherit DbContext()
    override x.OnConfiguring( optionsBuilder: DbContextOptionsBuilder ) =
        base.OnConfiguring(optionsBuilder)
        optionsBuilder.UseSqlServer("connection-string") |> ignore

<#
    foreach (var entitySet in container.BaseEntitySets.OfType<EntitySet>())
    {
#>
    <#=codeStringGenerator.DbSet(entitySet)#>
<#
    }

    foreach (var edmFunction in container.FunctionImports)
    {
        WriteFunctionImport(typeMapper, codeStringGenerator, edmFunction, modelNamespace, includeMergeOption: false);
    }
#>

これらの *.tt ファイルは保存すると即実行されるので、試行錯誤しながらできる。コンバートされる F# コードを見てちまちまと *.tt ファイルを変更する。

そうすると、こんなコードが出力される。

namespace SampleFSharpEF.Model

    open System
    open System.Collections.Generic
    
    [<CLIMutable>]
    type issues = {
        id : int 
        tracker_id : int 
        project_id : int 
        subject : string 
        description : string 
        due_date : Nullable<System.DateTime> 
        category_id : Nullable<int> 
        status_id : int 
        assigned_to_id : Nullable<int> 
        priority_id : int 
        fixed_version_id : Nullable<int> 
        author_id : int 
        lock_version : int 
        created_on : Nullable<System.DateTime> 
        updated_on : Nullable<System.DateTime> 
        start_date : Nullable<System.DateTime> 
        done_ratio : int 
        estimated_hours : Nullable<float> 
        parent_id : Nullable<int> 
        root_id : Nullable<int> 
        lft : Nullable<int> 
        rgt : Nullable<int> 
        is_private : bool 
        closed_on : Nullable<System.DateTime> 
    }

DbContext を継承した redmineEntities クラス

namespace SampleFSharpEF.Model

open System
open Microsoft.EntityFrameworkCore

type redmineEntities() = 
    inherit DbContext()

    override x.OnConfiguring( optionsBuilder: DbContextOptionsBuilder ) =
        base.OnConfiguring(optionsBuilder)
        optionsBuilder.UseMySQL("connection-string") |> ignore

    [<DefaultValue>] val mutable _issues : DbSet<issues>
    member this.issues with get() = this._issues and set v = this._issues <- v
    [<DefaultValue>] val mutable _projects : DbSet<projects>
    member this.projects with get() = this._projects and set v = this._projects <- v

F# のほうは、MySQLとMicrosoft.EntityFrameworkCoreを使うので、OnConfiguringメソッドをオーバーライドして接続文字列を指定する形にしている。

F#プロジェクトにエンティティクラスを含める

F#のプロジェクトにリンクでエンティティクラスを含めておく。F#の場合、ファイルが前方参照にする必要があるので、順番を調節する。

ここでは、.NET Core な F# プロジェクトを作って試している。

エンティティクラスを使うコードを書く

手作業になると100行以上書かないといけないエンティティクラスを、T4 で吐き出すようにしたので、使うのは結構楽になる。


open System
open System.Linq
open SampleFSharpEF.Model


[<EntryPoint>]
let main argv =
    printfn "Hello World from F#!"

    let ent = new redmineEntities()
    for it in ent.projects.ToList() do
        Console.WriteLine(String.Format("{0} : {1}", it.id, it.name ))

    0 // return an integer exit code

NuGet で MySql.Data.EntityFrameworkCore をインストールした後に、普通にデータベースにアクセスコードを書けばいい。

dotnet run するとこんな感じになる。

C#から使ってみる

試しに、F#で出力されたエンティティクラスをC#で使ってみる。
コマンドライン版の SampleFSharpEF.FSharp プロジェクトを参照設定して、SampleFSharpEF.Modelの中が使えるようにしていまう。本来ならば、別途F#のモデルプロジェクトを用意すると思う。

using System;
using System.Linq;
using SampleFSharpEF.Model;


namespace SampleFSharpEF.CSharp
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello C# World!");
            var ent = new redmineEntities();
            foreach ( var it in ent.projects.ToList())
            {
                Console.WriteLine(String.Format("{0} : {1}", it.id, it.name));
            }
        }
    }
}

結果はF#と同じになる。

以前は、C#プロジェクトからF#プロジェクトを参照する場合、FSharp.Core を参照設定する必要があった(PCLの関係だと思う)のだが、.NET Standard化されているのか、.NET Coreなプロジェクトだと FSharp.Core への参照設定はいらない。

サンプルコード

http://github.com/moonmile/sample-fsharp-ef/

おまけ

F#では動かないT4なんだけど、C#プロジェクトでF#コードを出力するという裏技を使うと、なんとか使えるようになる。とは言え、いちいちC#プロジェクトを作るのもアレなんだが。

Templatus/README.md at master ・ kerams/Templatus
https://github.com/kerams/Templatus/blob/master/README.md

F#でT4みたいなのが動く Templatus というのがあるので、これを使ってみるとよいかもしれん…が、Model1.tt の中身を全て F# で書き替えなければいかんというのが結構な手間のような気もする。要はエンティティクラスのマイグレーションがやりたいだけなので、適当に MySQLやSQLiteに接続してテーブル構造を引っ張り出して、コードを出力するだけでいいんだが。コードファーストのマイグレーションのような積み重ね方式じゃなくて、単純なスナップショット版でよいし(データは消えるので、実際にあれこれやる場合は、積み重ね方式が必須になるだけど)。

カテゴリー: 開発, F# | ADO.NET Entitiy Data Model を F# で使うための裏技 はコメントを受け付けていません

PHP で Slack に投稿する

C# で Slack に投稿する | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/9117

はコマンドラインやWindowsのデスクトップアプリから投稿することを想定しているけど、じゃあ WordPress のアドインみたいなのを作って Slack に投稿するにはどうすればよいのか?ってのが、これ。

PHPでSlackにメッセージをポストするクラスのサンプル – Qiita
https://qiita.com/monhan/items/d95ad6e4e698da9e6593

Slack APIを直接扱うのは手間なので、Incoming WebHooks を使って投稿してみる。
あれこれ作ろうと思ったけど、SlackBot と SlackBotInfo をそのまま流用する。

PHP-7.2.5 だと動かなかったので、一か所だけ修正する。

    protected function create_options($info)
    {
        return array(
            CURLOPT_URL            => $info['url'],
            CURLOPT_POST           => true,
            CURLOPT_POSTFIELDS     => $info['body'],
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HEADER         => true,
            CURLOPT_SSL_VERIFYPEER => false,	// ★
        );
    }

引数でメッセージを指定するにしてあるので、適当に WordPress から呼び出せる

include_once __DIR__.'/SlackBot.php';
include_once __DIR__.'/SlackBotInfo.php';

if ( $argc < 2 ) {
    $message = date("Y/m/d H:i:s");
} else {
    $message = $argv[1];
}

$url = "https://hooks.slack.com/services/XXX/XXX/XXX";
// メッセージをポスト
$bot = new SlackBot();
print_r($bot->post_message(new SlackBotInfo($url, $message)));

使い方を考える

こんな感じに、クライアントのPCからブラウザで何らかのメッセージを送る。フォームを使ってサーバーから保守PCへ送るとか、メールに切り替えるとかでもいいのだけど、もっとシンプルに Slack の通知だけを送ってしまう。単純なヘルプボタンみたいなものかな。例えば、なんか障害が起きたとか、定型的なレポートとかをメール経由ではなくて slack 経由にしてしまう感じ。

保守 PC のほうは、何で受けてもよいのだけど、LINE API とか Facebook とかメーラーとか、できるだけ統一して取れるようにするのがベターかなと。

サンプルコード

http://github.com/moonmile/sample-slack

カテゴリー: 開発, Slack | PHP で Slack に投稿する はコメントを受け付けていません

Xamarin.Froms+.NET Standardでローカルな SQLite データベースを使う

Xamarin.FormsでSQLiteを扱う記事はいくつかあるのだけど、.NET Standard 2.0を扱ったものは、以下しかなかったのでメモ代わりに記録しておく。

Xamarin.Forms, NET Standard 2.0 et Entity Framework Core avec SQLite ? Christophe Gigax
http://www.christophe.gigax.fr/2017/11/23/xamarin-forms-net-standard-2-0-et-entity-framework-core-avec-sqlite/

しかも記事がフランス語だという…コードを読めれば十分なんだけど。

ローカルなSQLiteファイルを用意する

大抵の記事がアプリの実行時に新規にSQLiteファイルを作っていて、じゃあ既存のSQLiteファイルを使いたい場合はどうするのか?というのが無かった。Androidの記事で似たものがあったので、これを活用していく。

Android SDK assetsの内容を全てローカルにコピーする – 自堕落なぺぇじ
http://d.hatena.ne.jp/corrupt/20110203/1296695472

Entitiy Model クラスは MySQL の redmine から借りて来る。ひとまず、projects と issues の構造を用意しておく。

public class projects
{
    public int id { get; set; }
    public string name { get; set; }
    public string description { get; set; }
    public string homepage { get; set; }
    public int is_public { get; set; }
    public int? parent_id { get; set; }
    public DateTime? created_on { get; set; }
    public DateTime? updated_on { get; set; }
    public string identifier { get; set; }
    public int status { get; set; }
    public int? lft { get; set; }
    public int? rgt { get; set; }
    public int inherit_members { get; set; }
    public int? default_version_id { get; set; }
}

public class issues
{
    public int id { get; set; }
    public int tracker_id { get; set; }
    public int project_id { get; set; }
    public string subject { get; set; }
    public string description { get; set; }
    public DateTime? due_date { get; set; }
    public int? category_id { get; set; }
    public int status_id { get; set; }
    public int? assigned_to_id { get; set; }
    public int priority_id { get; set; }
    public int? fixed_version_id { get; set; }
    public int author_id { get; set; }
    public int lock_version { get; set; }
    public DateTime? created_on { get; set; }
    public DateTime? updated_on { get; set; }
    public DateTime? start_date { get; set; }
    public int done_ratio { get; set; }
    public double? estimated_hours { get; set; }
    public int? parent_id { get; set; }
    public int? root_id { get; set; }
    public int? lft { get; set; }
    public int? rgt { get; set; }
    public int is_private { get; set; }
    public DateTime? closed_on { get; set; }
}

SQLite であらかじめテーブルを作成しておく。

create table issues (
	id int PRIMARY KEY AUTOINCREMENT,
	tracker_id int,
	project_id int,
	subject text,
	description	text,
	due_date text,
	category_id int,
	status_id int,
	assigned_to_id int,
	priority_id int,
	fixed_version_id int,
	author_id int,
	lock_version int,
	created_on text,
	updated_on text,
	start_date text,
	done_ratio int,
	estimated_hours real,
	parent_id int,
	root_id int,
	lft int,
	rgt int,
	is_private int,
	closed_on text
);
create table projects (
	id integer PRIMARY KEY AUTOINCREMENT,
	name text,
	description text,
	homepage text,
	is_public int,
	parent_id int,
	created_on text,
	updated_on text,
	identifier text,
	status int,
	lft int,
	rgt int,
	inherit_members int,
	default_version_id int,
	default_assigned_to_id int
);

型の変換ルールは、

  • MySQLのvarcharをSQLiteでtextへ
  • MySQLのdatetimeをSQLiteでtextへ

ことになる。SQLiteには日付型がないので文字列(text型)として扱うのだが、Microsoft.EntityFrameworkCore.Sqlite がうまいこと日付と文字列を相互変換してくれるので、Entitiy のほうは DateTime のままで良い。

MySQLからSQLiteへコピーする

ちょっと乱暴だが、.NET Core で Console アプリを使って、NuGet で MySql.Data.EntityFrameworkCore と Microsoft.EntityFrameworkCore.Sqlite を追加することで EF を使って MySQL から SQLite へコピーするツールが作れる。

MySQLのEF

public partial class RedmineEntities : DbContext
{
    public RedmineEntities()
    {

    }
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        base.OnConfiguring(optionsBuilder);

        var fname = "redmine.sqlite3";
        var docs = System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyDocuments);
        var path = System.IO.Path.Combine(docs, fname);

        optionsBuilder.UseSqlite($"Data Source={path}");
    }

    public DbSet<projects> projects { get; set; }
    public DbSet<issues> issues { get; set; }
}

SQLiteのEF

public partial class RedmineEntitiesMySql : DbContext
{
    public RedmineEntitiesMySql()
    {

    }
        
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        base.OnConfiguring(optionsBuilder);
        // SSH フォワーディングしておく
        // ssh -L 19000:localhost:3306 pi@raspi3.local
        optionsBuilder.UseMySQL(@"server=localhost;user id=redmine;password=redmine;database=redmine;port=19000;sslmode=None");
    }

    public DbSet<projects> projects { get; set; }
    public DbSet<issues> issues { get; set; }
}

main 関数

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("copy MySQL to SQLite");

        var mysql = new RedmineEntitiesMySql();
        var sqlite = new RedmineEntities();

        // projectsとisseusの中身を消す
        sqlite.Database.ExecuteSqlCommand("delete from projects");
        sqlite.Database.ExecuteSqlCommand("delete from issues");
        // データをコピーする
        sqlite.projects.AddRange(mysql.projects.ToListAsync().Result);
        sqlite.issues.AddRange(mysql.issues.ToListAsync().Result);
        sqlite.SaveChanges();
        Console.WriteLine("コピーしました");
    }
}

DB Browser for SQLite で中身を確認しておく。ファイル名は「redmine.sqlite3」としている。

Xamarin.Fromsにredmine.sqlite3を追加する

さて、Android限定になるのだが(iOSはまだ調べていない)、SQLiteファイルを指定するためには、どこか読み込めるところにファイルをコピーしておかないといけない。これには2つの方法があって、

  • 起動時に1回だけネットワークを通じて取ってくる。
  • 起動時に1回だけリソースからコピーする。

ことになる。ゲームなどの画像ファイルや更新データなどは、ネットワークから取ってきて Android 内に置くパターンが多いのだが、今回はリソースからコピーしてみる。

Android SDK assetsの内容を全てローカルにコピーする – 自堕落なぺぇじ
http://d.hatena.ne.jp/corrupt/20110203/1296695472

ここの記事を参考にして、Assets/redmine.sqlite3 ファイルを、/data/user/0/…/files/redmine.sqlite3 へコピーする。これは、Xamarin.FormsのAndroid プロジェクトのほうに作っておく。

private string copyLocal( string fname )
{
    var st = new System.IO.BinaryReader( this.Resources.Assets.Open(fname));
    var docs = System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyDocuments);
    var ofname = System.IO.Path.Combine(docs, fname);
    var ofs = System.IO.File.OpenWrite(ofname);
    int DEFAULT_BUFFER_SIZE = 1024 * 4;
    byte[] buf  = new byte[DEFAULT_BUFFER_SIZE];
    int n = 0;
    int nn = 0;
    while ((n = st.Read(buf, 0, buf.Length)) > 0)
    {
        ofs.Write(buf, 0, n);
        System.Diagnostics.Debug.WriteLine(string.Format("cnt: {0}", nn));
        nn++;
    }
    ofs.Close();
    st.Close();
    return ofname;
}

Xamarin.Formsに「Microsoft.EntityFrameworkCore.Sqlite」を追加する

Xamarin.Formsの.NET Standardプロジェクトのほうに、Nuget で Microsoft.EntityFrameworkCore.Sqlite を追加する。
EF の接続文字列は、SQLiteファイルへのパスになるので、コピー先のディレクトリを指定させる。

ちなみに「Microsoft.EntityFrameworkCore.Sqlite.Core」というのもあるが、この .Core 付きとないのとどう違うのかわからない。Xamarin.Forms の場合は、.Core 付きでは動かなかったので、.NET Core 専用なのか?依存関係が

  • Microsoft.EntityFrameworkCore.Sqlite は、SQLitePCLRaw.bundle_green
  • Microsoft.EntityFrameworkCore.Sqlite.Coreは、Microsoft.Data.Sqlite.Core

となっている。

public partial class RedmineEntities : DbContext
{
    public RedmineEntities()
    {

    }
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        base.OnConfiguring(optionsBuilder);

        var fname = "redmine.sqlite3";
        var docs = System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyDocuments);
        var path = System.IO.Path.Combine(docs, fname);

        optionsBuilder.UseSqlite($"Data Source={path}");
    }

    public DbSet<projects> projects { get; set; }
    public DbSet<issues> issues { get; set; }
}

試しに、MainPageクラスのコンストラクタで、SQLiteに接続し、ListView に設定する。

public partial class MainPage : ContentPage
{
	public MainPage()
	{
		InitializeComponent();
        // redmine.sqlite3 をロードする
        _ent = new RedmineModel.RedmineEntities();
        this.lv.ItemsSource = _ent.issues.ToList();
	}

    RedmineModel.RedmineEntities _ent;
	...
}

ちなみに、XAML はこんな感じ。

<ContentPage.Content>
    <StackLayout>
        <ListView x:Name="lv" VerticalOptions="FillAndExpand" HasUnevenRows="true" ItemSelected="OnItemSelected">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <ViewCell>
                        <StackLayout Padding="10">
                            <Label Text="{Binding id}" LineBreakMode="NoWrap" Style="{DynamicResource ListItemTextStyle}" FontSize="16"/>
                            <Label Text="{Binding subject}" LineBreakMode="NoWrap" Style="{DynamicResource ListItemDetailTextStyle}" FontSize="13"/>
                        </StackLayout>
                    </ViewCell>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </StackLayout>
</ContentPage.Content>

実行すると、こんな風になる。

ローカルなSQLiteファイルを何に使うか?

スマホでデータベースに繋げるのだから、AzureかWeb APIでいいような気もするのだが、いくつか内部でローカルなデータベースとして持っておいたほうが良いパターンが考えられる。

  • スマホで外部ネットワークに繋がらない、あるいは極端に遅い場合
  • スマホを社内WiFiのみに限る場合
  • タブレットをマニュアルっぽく使う

今回 SQLite に持ってきたのは redmine なので、あまり意味はないのだが、wordpress のデータをスマホの SQLite に持ってきて、単体でブラウジングできるようにすると電子書籍っぽく使えるのではないか?と思ったり。
まあ、ひとまず、非クラウドなスタンドアローン環境でスマホを使う技になる。

サンプルコード

github sample-sqlite-ef

カテゴリー: 開発, SQLite, Xamarin | Xamarin.Froms+.NET Standardでローカルな SQLite データベースを使う はコメントを受け付けていません

独自にポートフォワード作成し、ラズパイの MySQL へ LINQ で接続する

さて、MySQL Workbench から SSH ポートフォワーディングでラズパイの MySQL へ接続できることが分かったが、じゃあ独自の C# プログラムから接続する場合はどうするのか?という問題がある。

この場合、workbench が立ててくれていた ssh を自前で建てることになる。いくつか方法があるのだけど(Putty や Tera Term で建てることもできる)、今だと Ubuntu on Windows を使って ssh を立ててしまったほうが早いみたい。

image

左にある緑色の SSH を自前で建てることになる。

ssh -L 19000:localhost:3306 pi@raspi3.local

ちょっと順番がややこしいのだけど、MySQL Workbench の設定に合わせると、

  • SSH Hostname: raspi3.local
  • SSH Username: pi
  • MySQL Hostname: localhost

となる、自前で建てる SSH のポート番号は 19000 番になるので、localhost:19000 に接続すると、raspi3.local:3306 へ接続したのと同じことになる。

image

この 19000番に使って MySQL workbench から接続することもできる。当然、Ubuntu on Windows の画面を閉じると SSH が止まるので、このウィンドウは立ち上げっぱなしにする。

image

ポートフォワードを使って MySQL へ接続する

LINQを使ってMySQLにアクセスする | Moonmile Solutions Blog http://www.moonmile.net/blog/archives/9093

.NET Core上でLINQを使ってMySQLにアクセスする | Moonmile Solutions Blog http://www.moonmile.net/blog/archives/9102

ローカルな MySQL に接続する場合のプログラムで接続文字列を変えればよい。

public partial class RedmineEntities : DbContext
 {
     public RedmineEntities()
     {
              
     }
     protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
     {
         base.OnConfiguring(optionsBuilder);
         optionsBuilder.UseMySQL(@"server=localhost;user id=redmine;password=redmine;database=redmine;port=19000;sslmode=None");
     }
  
     public DbSet<projects> projects { get; set; }
     public DbSet<issues> issues { get; set; }
}

ポート番号をフォワードしている 19000 に変える。

.NET Core 版だとこんな感じに実行ができる。

image

WPF 版の場合は、app.config に接続文字列が書いてあるだろうから、そこを直接書き替える。EF のデザイナも使えるので便利だろう。

<connectionStrings>
   <add name="RedmineEntities" connectionString="server=localhost;user id=redmine;password=redmine;database=redmine;port=19000;sslmode=None" providerName="MySql.Data.MySqlClient" />
</connectionStrings>
カテゴリー: 開発, C#, MySQL | 独自にポートフォワード作成し、ラズパイの MySQL へ LINQ で接続する はコメントを受け付けていません

MySQL Workbenchのポートフォワード機能を使いラズパイのMySQLに接続する

外部のデータベースサーバーに触るときに気を付けないといけないのが、ログインユーザーとデータの漏洩なんだけど、データベースのポートを公開してしまうとちょっと危ない。社内ネットワークならばいいのだけど、流石に公開済みの WEB サーバーのデータベースのポートを開いてしまうのはかなり危険。

なので、MySQLの場合、phpmyadmin を使ってアクセスをするか、という話が多いのだが、実は、MySQL Workbench から SSH のポートフォワード機能が使えるのを最近知った。最近のレンタルサーバーは SSH が公開されている場合が多いので、この 22 ポートを通して内部の MySQL に接続する。

例えば、先日ラズパイに導入した redmine が使っている MySQL に PC から接続する場合は、こんな感じになる。

image

どうせ、ローカルなネットワークでしか動かさないので、PC から直接 MySQL に接続してもいいのだが(実際、ちょっと前まではそれしかないと思っていた)、クライアント PC で SSH を使って、サーバー(この場合はラズパイ)で sshd が動いていれば、この ssh 同士でポートフォワードしてくれる。動的に作成される(と思う)ローカルな SSH のポートを通して、ラズパイの22番を呼び出す。ラズパイの ssh がこれを受けて、MySQL の 3306番に接続してくれる。

MySQL Workbench で接続を作るときに、connection method で “Standard TCP/IP over SSH” を選択して各種設定を入力する。通常の SSH と同じように key file も使える。

image

“Test Connection” で接続を試す。

image

これが出なくて、ダンマリになる場合は、SSH サーバー名やポートが間違っている可能性が高い。うちの場合、avahi-daemon で *.local で繋がるようにしているのだけど、raspi3.local は接続できるのだけど、VMWare 上の ubuntu.local には接続できなかった。でも、IP アドレスを指定すると通るので、そのあたりもチェックが必要。

image

これでローカルな MySQL やポートを空けた DB サーバーと同じ用に MySQL Workbench から接続ができるようになる。ちなみに、DB サーバーが WEB サーバーとは別にある場合は、こんな設定になる。

image

OutServer とあるのが、いわゆる「踏み台」のサーバーで、SSH を公開しているところ。このサーバーから DB Server に接続するという形になる。この SSH によるポートフォワード機能を使うと、MySQL だけじゃなくてリモートデスクトップにも使えるので、難しい VPN の設定をしなくてもいいんじゃないかなと思ったり。

カテゴリー: 開発 | MySQL Workbenchのポートフォワード機能を使いラズパイのMySQLに接続する はコメントを受け付けていません

C# で Slack に投稿する

いままで忘備録的に、twitter にメモを投稿していたのだけど、半年ほど独り slack に投稿するということをやっている。ツイッターの自分の投稿をググるのも大変というのもあり、仕事に関係するものを投稿して、逆に仕事自体を推測されるのもアレなので(そういアピールだったらいいんだけど、実際、そういうこともやるし)、手元のものは手元でやるということで。

で、せっかくのクローズドな独り slack なのだから、redmine への投稿も slack 経由でやったり、逆に新規のチケットがでてきたら slack のほうに投げ返したりしたらどうか?と思ったりする。

slack の場合、http://slack.com/apps に各種アプリケーションがアップしてあって(twitter の連携アプリのような感じか?)、直接 slack api を触れなくて、適当な連携アプリを探してくるのがベターだったりするみたい。

Incoming WebHooks を使う

そんな中で、Incoming WebHookを使って、C#でSlackに投稿してみる – なか日記 というのがあったので、Incomming WebHook を使ってみる。

Incoming WebHooks | Slack App Directory
https://slack.com/apps/A0F7XDUAZ-incoming-webhooks?page=1

image

  1. Add Configuration する
  2. 投稿先のチャンネルを選んで、Add Incoming WebHooks Integration する
  3. Webhook URL が作成されるので、これを自前アプリで使う。

なか日記さんのほうでは、WebClient が使われているので、HttpClient を使うように書き替えてみる。JSON を送るときの content-type は StringContent を作成するときに指定すれば ok. DynamicJson を使っているけど、Newtonsoft.Json でシリアライズしてもよい。

class Program
 {
     static string WEBHOOK_URL = “”;
    static void Main(string[] args)
     {
         var hc = new HttpClient();
         var json = DynamicJson.Serialize(new
         {
             text = args.Length == 0 ? 
                     DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss"):
                     args[0],
             icon_emoji = ":ghost:", //アイコンを動的に変更する
            username = "slackpost"  //名前を動的に変更する
        });

        var content = new StringContent(json, Encoding.UTF8, "application/json");
        var res = hc.PostAsync(WEBHOOK_URL, content).Result;
        Console.WriteLine("投稿しました");
    }
}

ちなみに、WebHook URL を取得するときにチャンネルを指定するけど、登校時に JSON で channel プロパティを指定すれば、指定したチャンネルが使われるので、実質何処にでも投稿ができる。

詳しい解説は Incoming Webhooks | Slack にある。curl を使って post すればよいので、わざわざツールを作るまでもないんだけど、まあ、あれこれ複雑なオプションを外してしまった単発ツールをスクリプトで作ってもよいだろう。

逆にメッセージを監視するときは、Outgoing WebHookを使ってSlackのメッセージを監視する – なか日記 に Outgoing WebHooks の使い方がある…のだけど、別途 Web API を立てる必要がある。外部サーバーで何かをやる場合はこれでいいのだが、結局のところ手元なPCへ通知を出す場合が多いので、真面目に slack api を使ってメッセージをポーリングするのが良いかなと思ったり。message event | Slack このあたり。

カテゴリー: 開発, Slack | C# で Slack に投稿する はコメントを受け付けていません

Xamarin.Formsでの共有部分をPCLから.NET Standardに変える

去年の6月の時点では、.NET Standard化されていなかったので、第6章の「共通ロジックを作成する」で PCL を使っていたのだけど、プロジェクト作成時に「共有プロジェクト」と「.NET Standard」になっているので、そのあたりのサンプルを書き替る。

Xamarinプログラミング入門 C#によるiOS、Androidアプリケーション開発の基本
https://www.amazon.co.jp/dp/4822253503/

コード共有方法で「.NET Standard」を選ぶ

「共有プロジェクト」のほうは、従来通りのコードを共有する方式なので #if を使って iOS/Android/UWP と書き分けられる。これでも、まあ十分なのだけど、ある程度コードが多くなってくるとロジック部分を別途テストしたいときがあるので、ライブラリ化しておくほうが良かったりする。

でもって、PCL(Portable Class Library)で作るときと .NET Standard で作るときとどう違うのかというと端的に言えば、PCLの場合は最小公約数になていて、.NET Standard の場合が最大公約数(というほど最大でもないが、いちおうな標準化)な意味合いで使える。
書籍の説明上、第6章でPCLで共通化させておいて、第8章で個別の動作を DependencyService を使うことになるのだが、.NET Standard の範囲内であればここの DependencyService が要らなくなる。

特に .NET Standard 2.0 の場合は、

  • ファイルまわり
  • HTTPまわり

が共通コードで書ける(System.IO.Fileとか、HttpListenerとか)ので、場合のよっては従来 NuGet でとってきた PCL なライブラリの組み込みが必要なくなる。あと、PCL の場合は、対応するプラットフォームによって条件がかなり厳しくなっていて Profile の番号体系も混乱しているので、さっくりと、.NET Standard 2.0 に対応させてしまったほうがよかったりする。

SampleTodo.XForms.Std

.NET Standard 版の SampleTodo.XForms.Std を新しく作成する。

moonmile/xamarin-samples
https://github.com/moonmile/xamarin-samples

プロジェクトを新しく作成しなくても、既存の PCL のプロジェクトを、

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Xamarin.Forms" Version="3.0.0.482510" />
  </ItemGroup>
</Project>

な風に書き替えればよいのだが、Xamarin.Forms のバージョンが「3」になったり(つい5月にver3になっている)していたので、作り直してみた。

小さいサンプルプロジェクトなので、あとは PCL 版の SampleTodo.XForms からコードを移していくだけでよい。

ちなみに、本格的な ToDo アプリを作りたい場合は、テンプレートの Master-Detail から書き起こしていったほうがよい。ViewModel 間のメッセージのやりとりが MessagingCenter.Subscribe になっているのはこれに準じているため。

PCLのコードでは、DependencyServiceを使って、Android/iOSで別のコードを共通化することになっていた。

IToDoStorage storage = DependencyService.Get<IToDoStorage>();
/// <summary>
/// 内部ストレージに保存
/// </summary>

void Save()
{
    using (var st = storage.OpenWriter("save.xml"))
    {
        viewModel.Items.Save(st);
    }
}
/// <summary>
/// 内部ストレージから読み込み
/// </summary>

void Load()
{
    var items = new ToDoFiltableCollection();
    using (var st = storage.OpenReader("save.xml"))
    {
        if (st == null || items.Load(st) == false)
        {
            // 初期データを作成する
            items = ToDoFiltableCollection.MakeSampleData();
        }
    }
    viewModel.Items = items;
}

これを、.NET Standard 版で書き替えると、素直に System.IO.File
が使えるようになる。

/// <summary>
/// 内部ストレージに保存
/// </summary>

void Save()
{
    var docs = System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyDocuments);
    var path = System.IO.Path.Combine(docs, "save.xml");
    using (var st = System.IO.File.OpenWrite(path))
    {
        viewModel.Items.Save(st);
    }
}
/// <summary>
/// 内部ストレージから読み込み
/// </summary>

void Load()
{
    var items = new ToDoFiltableCollection();
    // .NET Standard 版では、
    // Android/iOSのコードも、共有プロジェクトに書ける
    var docs = System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyDocuments);
    var path = System.IO.Path.Combine(docs, "save.xml");
    if (System.IO.File.Exists(path))
    {
        var st = System.IO.File.OpenRead(path);
        if (items.Load(st) == false)
        {
            // 初期データを作成する
            items = ToDoFiltableCollection.MakeSampleData();
        }
    }
    else
    {
        // 初期データを作成する
        items = ToDoFiltableCollection.MakeSampleData();
    }
    viewModel.Items = items;
}

だが、UWPを付け加えるとストレージ絡みはやっぱり、DependencyService を使うという羽目に陥る。

IToDoStorage storage = DependencyService.Get<IToDoStorage>();

/// <summary>
/// 内部ストレージに保存
/// </summary>

void Save()
{
    var docs = System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyDocuments);
    var path = System.IO.Path.Combine(docs, "save.xml");
    try
    {
        using (var st = System.IO.File.OpenWrite(path))
        {
            viewModel.Items.Save(st);
        }
    }
    catch
    {
        // UWPを含める場合は従来通り DependencyService を使う
        using (var st = storage.OpenWriter("save.xml"))
        {
            viewModel.Items.Save(st);
        }
    }
}
/// <summary>
/// 内部ストレージから読み込み
/// </summary>

void Load()
{
    var items = new ToDoFiltableCollection();
    // .NET Standard 版では、
    // Android/iOSのコードも、共有プロジェクトに書ける
    var docs = System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyDocuments);
    var path = System.IO.Path.Combine(docs, "save.xml");
    try
    {
        if (System.IO.File.Exists(path))
        {
            var st = System.IO.File.OpenRead(path);
            if (items.Load(st) == false)
            {
                // 初期データを作成する
                items = ToDoFiltableCollection.MakeSampleData();
            }
        }
        else
        {
            // 初期データを作成する
            items = ToDoFiltableCollection.MakeSampleData();
        }
    }
    catch
    {
        // UWPを含める場合は従来通り DependencyService を使う
        using (var st = storage.OpenReader("save.xml"))
        {
            if (st == null || items.Load(st) == false)
            {
                // 初期データを作成する
                items = ToDoFiltableCollection.MakeSampleData();
            }
        }
    }
    viewModel.Items = items;
}

これは、UWPの場合だけファイルアクセス方法が違っていて、↓な風に Windows.Storage.ApplicationData にアクセスするという制限からきている。これって、System.Environment.GetFolderPath にエイリアスしてくれないですかね?

public Stream OpenWriter(string file)
{
    var folder = Windows.Storage.ApplicationData.Current.LocalFolder;
    var t = folder.OpenStreamForWriteAsync(file, Windows.Storage.CreationCollisionOption.ReplaceExisting);
    t.Wait();
    var st = t.Result;
    return st;
}

既存のPCLプロジェクトを.NET Standard化するべきか?

仕事の場合、iOSとAndroidの共通化に特化しているだろうから(UWPプロジェクトは使っていないだろう)、.NET Standard にしても共通で使える部分が多いと思うのだけど、それぞれのプロジェクトに Xamarin.Forms.Dependency で書いていたものを、共通の .NET Standard プロジェクトに移し替えるか?というとかなりリスキーな気がする。

というのも、

  • そもそも、その Xamarin.Forms.Dependency は .NET Standard に入っているのか?
  • NuGet 等で旧来の PCL を使っていないか?

が問題になってくる。NuGet で提供される PCL プロジェクトが .NET Standard 化されていればよいのだが(去年の夏あたりから順次 .NET Standard 対応になっていたけど)、iOS/Android 個別の動作に特化してる部分は、なかなか難しいのではないかな?と。
なので、サンプル的には「共有プロジェクトは PCL から .NET Standardへ」という形で新規プロジェクトとして作ればよいのだが、既存の巨大となってしまった&現在動いているアプリのコードを PCL から .NET Standard に完全移行するかどうかは微妙な感じになる。

ただし、SampleTodo.XForms から SampleTodo.XForms.Std にプロジェクトのコードを引き継いだように、

  1. PCL プロジェクトの *.csproj を .NET Standard にしてビルドして動作確認。DependencyService 部分はそのまま。
  2. 冗長と思われる iOS/Android 個別の Xamarin.Forms.Dependency なところを、.NET Standard に引っ越しさせる。

という2段階で移行させるのがベターだろう。引っ越しさせなくてもよいのだが、iOSとAndroidでコードが二重化されているよりもひとつにまとまっていたほうが今後のメンテナンスが楽だろうし、コード共有で一生懸命 #if しているよりは、.NET Standard で適宜ライブラリ化しておいたほうがビルド時間も減るというものだ。

カテゴリー: 開発, Xamarin | Xamarin.Formsでの共有部分をPCLから.NET Standardに変える はコメントを受け付けていません