.NET Framework 4.7 から .NET6 への移行ポイント

Windows 上の .NET Framework から .NET 6 へ移行するときに、障害となりそうなポイントを挙げておきます。ちょっとした Windows フォームや WPF アプリならばすんなり以降できるのですが、それなりの大きさの C/S 方式で Entity Framework を使っていると(使っているんだが)、移行時に躓きがありそうです。

移行対象

  • WPF アプリで約70画面ある。
  • テーブル数は約20テーブルある。
  • MVVMパターンを使い、View と ViewModel を分離させている
  • Model は .NET Framework の Entity Framework を使っている。コードファースト以前の、*.edmx ファイルを使う方式(SQL Server のテーブルからコードを自動生成するので当時は便利だった)
  • テーブル検索はほぼ LINQ を使っているが、移行前の複雑な SQL を使うため SqlQuery で直接呼び出している部分がる。
  • View にバインドさせるため、Entity クラス(*.edmx による自動生成)に、適宜プロパティをはやしている。
  • Excel のコントロールを埋め込むため、WindowsFormsHost を使っている
  • Excel の帳票をつかうため、Microsoft.Office.Interop.Excel を使っている

ステップ数は数えてはいませんが、大体1万行を超える位です。もともと、ACCESS 版を C# 版に書き直したもので、これを試しに .NET6 に移そうと試しています。

全体の構造

.NET Framework の WPF を .NET6 の WPF に移行するので、全体のフォルダー構造は変えません。 中のコード(特にロジック)には手をいれたくないので、namespace はそのまま使うようにします。

Entity Framework の移行

.NET Framework の System.Data.Entity.DbContext を .NET6 の Microsoft.EntityFrameworkCore. DbContext に移行します。同じ「 DbContext 」となっていますが、中身は別物です。.NET6 のほうは、NuGet で “Microsoft.EntityFrameworkCore.SqlServer” をインストールします。

.NET Framework の EF とは違い、*.edmx のようなテーブルの構造を定義したファイルがありません。コードファースト的にクラスを定義しておくか、データベースファースト的に SQL Server から dotnet ef コマンドを使いテーブルクラスを生成させます。テーブルクラス = Entity クラスは、以前の .NET Framework と同じものを使うので(プロパティ名などが変わるとやっかい).NET Framework 版のものをコピーして Models フォルダに入れておきます。

DbSet クラスは、

  • .NET Framework では System.Data.Entity.DbSet
  • .NET6 では Microsoft.EntityFrameworkCore.DbSet

となり同じ「DbSet」という名前になっていますが、似て非なるものです。

SQL Server に接続する文字列は、.NET6の場合(EFCoreの場合)は OnConfiguring を override して記述します。場合によっては、複数の DbContext を切り替える必要があるので、外部から切り替えができるようにしておきます。

public static 会議室Entities CreateEnt(string dbname, string servername = "(local)")
{
    var builder = new SqlConnectionStringBuilder();
    builder.DataSource = servername;
    builder.InitialCatalog = dbname;
    builder.IntegratedSecurity = true;
    var cnstr = builder.ConnectionString;
    var context = new 会議室Entities();
    context.Database.SetConnectionString(cnstr);
    return context;
}

主キーを付加する

.NET Framework の EF が出力する Entity クラスは主キー(Key属性)がありません。このため、この Entity クラス .NET6 の EFCore で使うと実行エラーが発生します。検索時ではあっても、デフォルトでは主キーが必要になっているためです。.NET Framework の場合はでは、*.edmx ファイルに主キーなどのテーブル構造の情報がはいっているため、Entity ファイルには Key属性等がついていません。

public partial class 会社基本情報
{
    public int id { get; set; }
    public string 会社名 { get; set; }
    public string 肩書き { get; set; }
    public string TEL { get; set; }
    public string FAX { get; set; }
    public string 郵便番号 { get; set; }
    public string 住所1 { get; set; }
    public string 住所2 { get; set; }
    public string 期名前 { get; set; }
    public Nullable<double> 消費税 { get; set; }
    public Nullable<int> 端数処理コード { get; set; }
    public Nullable<int> 年 { get; set; }
    public Nullable<System.DateTime> 期首日 { get; set; }
    public Nullable<System.DateTime> 期末日 { get; set; }
    public string 振込先A { get; set; }
    public string 振込先B { get; set; }
    public string FA { get; set; }
    public string FB { get; set; }
    public string GA { get; set; }
    public string GB { get; set; }
    public string HA { get; set; }
    public string HB { get; set; }
}

ここの id 部分に Key属性をつけてもよいのですが、このファイル自体はさわりたくないので、DbContext の OnModelCreating メソッドで調節します。

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        modelBuilder.Entity<ホームページ>().HasKey(x => x.ID);
        modelBuilder.Entity<確認>().HasKey(x => x.日付);
        modelBuilder.Entity<期首>().HasKey(x => x.ID);
        modelBuilder.Entity<県>().HasKey(x => x.ID);
        modelBuilder.Entity<顧客>().HasKey(x => x.ID);
        modelBuilder.Entity<顧客SUB>().HasKey(x => x.ID);
        modelBuilder.Entity<予約>().HasKey(x => x.ID);
        modelBuilder.Entity<予約SUB>().HasKey(x => x.ID);
        modelBuilder.Entity<予約確認>().HasKey(x => x.ID);
        modelBuilder.Entity<予約状況>().HasKey(x => x.ID);
        modelBuilder.Entity<会社基本情報>().HasKey(x => x.id);

まめに HasKey で主キーを設定してやれば ok です。

生 SQL を呼び出したときの戻り値のクラスでは、主キーがないことを設定する

実テーブルを参照する場合には主キーがあるのですが、生 SQL を使って検索結果を返すときには主キーはありません。なので、.NET6 の EFCore では、HasNoKey を使って明示的に設定しないといけません。

全て LINQ で済めばよいのですが、複雑な join が発生する場合、生 SQL を使ったようが良い場合があります。例えば、以下のような SQL を LINQ に書き直すのはかなり困難ですし、移行時に間違いが発生します。なので、そのまま SQL を使います。

        var SQL = $@"
select
予約.ID,
顧客.会社名,
予約.日付,
部屋.名称 as 会議室,
予約状況.開始時刻 as 開始,
予約状況.終了時刻 as 終了,
顧客sub.部署名 as 部署名,
顧客sub.担当者A as 担当者,
顧客sub.TEL as TEL,
顧客sub.FAX as FAX,
予約.予約 as 状態,
予約.契約日 as 契約日,
予約.確認票最終処理日 as 確認日,
請求.日付 as 請求日,
X.利用料,
isnull(入金.入金額,0) as 入金額,
予約.備考B as 注意事項,
予約.案内名称 as 案内板名称
from 
( 
select
予約.ID as ID,
cast(ISNULL(sum(予約sub.単価*予約sub.数量),0) as money) as 利用料
from 予約
left join 予約sub on 予約sub.予約ID = 予約.id
where 予約.顧客id = {顧客ID}
group by 
予約.ID 
) X
inner join 予約 on 予約.id = X.ID
inner join 顧客 on 顧客.ID = 予約.顧客ID
inner join 顧客sub on 顧客sub.ID = 予約.顧客SUBID
left join 請求 on 請求.id = 予約.請NO
left join 入金 on 入金.請求ID = 請求.ID
inner join 部屋 on 部屋.id = 予約.会議室
inner join 予約状況 on 予約状況.予約ID = 予約.ID
order by 予約.日付 desc 
";
this.Items = App.ent.Database.SqlQuery<結果>(SQL).ToList();

この時、戻り値として使っている「結果」クラスは、.NET Framework の場合は適当に値クラスを作って検索結果を受け取ればいいのです(いちいち値をクラスを作るのが面倒ではあるのですが)。

しかし、.NET6 の EFCore には Database.SqlQuery に相当するものがなく、DbSet<T>.FromSqlRaw を使うことになっています。使い方は、以下のようにいったん DbContext に「結果」コレクションをはやすことになります。

            var SQL = $@"
select
...
";
this.Items = App.ent.結果.FromSqlRaw(SQL).ToList();

これに伴い、DbContext クラスには、戻り値のクラスも DbSet<T> で定義しないといけません。

        // 戻り値用
        public virtual DbSet<ViewModels.VM請求一覧.請求一覧> 請求一覧 { get; set; }
        public virtual DbSet<ViewModels.VM顧客履歴.結果> 結果 { get; set; }
        public virtual DbSet<ViewModels.VM予約一覧.Data> 予約一覧Data { get; set; }
        public virtual DbSet<ViewModels.VM予約詳細入力.引合> 予約詳細入力引合 { get; set; }
        public virtual DbSet<ViewModels.VM属性別集計.属性> 属性別集計属性 { get; set; }
        public virtual DbSet<ViewModels.VM年間グラフ.年間売上> 年間売上 { get; set; }
        public virtual DbSet<ViewModels.VM月別売上.月別売上> 月別売上 { get; set; }
        public virtual DbSet<ViewModels.VM請求集計.請求> 請求集計請求 { get; set; }
        public virtual DbSet<ViewModels.VM過去予約状況.結果> 過去予約状況結果 { get; set; }
        public virtual DbSet<ViewModels.VM顧客別利用状況集計.結果> 顧客別利用状況集計結果 { get; set; }
        public virtual DbSet<ViewModels.VM顧客利用回数一覧.利用回数> 顧客利用回数一覧利用回数 { get; set; }
        public virtual DbSet<ViewModels.VM顧客利用状況.利用明細> 顧客利用状況利用明細 { get; set; }

実は、戻り値クラスはそれぞれの ViewModel クラス内でしか使わないので private になっているのですが、.NET6 の場合には DbContext から参照できるように public(あるいはinternal)の変更が必要になります。はっきり言って、この FromSqlRaw の仕組みはよくないです。もともと内部クラスになるので、名前の重複が激しくなっています。

この戻り値専用のクラスは、主キーがありません。これを明示的に示すために OnModelCreating 内で HasNoKey を指定します。

            modelBuilder.Entity<ViewModels.VM請求一覧.請求一覧>().HasNoKey();
            modelBuilder.Entity<ViewModels.VM顧客履歴.結果>().HasNoKey();
            modelBuilder.Entity<ViewModels.VM予約一覧.Data>().HasNoKey();
            modelBuilder.Entity<ViewModels.VM予約詳細入力.引合>().HasNoKey();
            modelBuilder.Entity<ViewModels.VM属性別集計.属性>().HasNoKey();
            modelBuilder.Entity<ViewModels.VM年間グラフ.年間売上>().HasNoKey();
            modelBuilder.Entity<ViewModels.VM月別売上.月別売上>().HasNoKey();
            modelBuilder.Entity<ViewModels.VM請求集計.請求>().HasNoKey();
            modelBuilder.Entity<ViewModels.VM過去予約状況.結果>().HasNoKey();
            modelBuilder.Entity<ViewModels.VM顧客別利用状況集計.結果>().HasNoKey();
            modelBuilder.Entity<ViewModels.VM顧客利用回数一覧.利用回数>().HasNoKey();
            modelBuilder.Entity<ViewModels.VM顧客利用状況.利用明細>().HasNoKey();

Entity クラスに NoKey 属性のようなものがあればよいのですが、どうも無いみたいですね。

データベース上のカラム名と一致させる

.NET6 の EFCore では、データベース上もカラム名と Entity クラスのカラム名が完全に一定していないといけません。よって、次のようなデータベース上では、正式な名前の場合、ちょっと困ることがおこります。

これは前回の移植上、予約テーブルに「予約」という名前のカラムがついています。

実は、Entity Framework では、テーブル名とカラム名が同じのは使えません。以前から問題だと思っているのですが、テーブル名もカラム名も同じ名前空間で混在させてしまっているからなのです。

なので、.NET Framework の EF では、以下のように「予約1」という名前が勝手に振られます。

    public partial class 予約
    {
        public int ID { get; set; }
        public int 顧客ID { get; set; }
        public int 顧客SUBID { get; set; }
        public Nullable<int> 予約者 { get; set; }
        public string 予約会社 { get; set; }
        public string 予約担当 { get; set; }
        public string 予約TEL { get; set; }
        public int 担当者ID { get; set; }
        public Nullable<System.DateTime> 日付 { get; set; }
        public Nullable<System.DateTime> 記録日 { get; set; }
        public Nullable<System.DateTime> 契約日 { get; set; }
        public Nullable<System.DateTime> M期限 { get; set; }
        public Nullable<System.DateTime> K期限 { get; set; }
        public Nullable<int> 予約1 { get; set; }
        public Nullable<int> 会議室 { get; set; }
        public Nullable<int> TF { get; set; }
        public Nullable<int> TE { get; set; }

ところが、.NET6 の EFCore では、データベースのカラム名と Entity クラスのプロパティ名は同一であるという前提があるので、以下のように Column 属性で変更しておきます。

    public partial class 予約
    {
        public int ID { get; set; }
        public int 顧客ID { get; set; }
        public int 顧客SUBID { get; set; }
        public Nullable<int> 予約者 { get; set; }
        public string 予約会社 { get; set; }
        public string 予約担当 { get; set; }
        public string 予約TEL { get; set; }
        public int 担当者ID { get; set; }
        public Nullable<System.DateTime> 日付 { get; set; }
        public Nullable<System.DateTime> 記録日 { get; set; }
        public Nullable<System.DateTime> 契約日 { get; set; }
        public Nullable<System.DateTime> M期限 { get; set; }
        public Nullable<System.DateTime> K期限 { get; set; }
        [Column("予約")]
        public Nullable<int> 予約1 { get; set; }
        public Nullable<int> 会議室 { get; set; }

この部分は、実は「予約」プロパティに変更してもよいのですが、既に各種のコードから「予約1」で参照しているので、参照先のコードは変更したくありません。よって、Column 属性でのがれておきます。

拡張したプロパティを DbSet に無視させる

.NET Framework の Entity クラスを MVVM パターンで View にバインドさせる場合、表示上フォーマットを変更させることがよくあります。Converter を作るのもよいのですが、どうせ partial になっているので、適当なプロパティを作って拡張させておくと便利、だったのです。

    public partial class 単発アラーム
    {
        public int ID { get; set; }
        public Nullable<System.DateTime> 日時 { get; set; }
        public string 対象種別 { get; set; }
        public string 対象名 { get; set; }
        public string 表示内容 { get; set; }
        public System.DateTime CreateAt { get; set; }
        public System.DateTime UpdateAt { get; set; }
    }

このような自動生成された Entity クラスとは別に、patial で拡張しておきます。

    /// <summary>
    /// 日時を年月日と時分に分ける拡張
    /// </summary>
    public partial class 単発アラーム
    {
        public Nullable<System.DateTime> 日時_年月日
        {
            get { return this.日時; }
            set
            {
                if ( this.日時.HasValue )
                {
                    DateTime dt = new DateTime(
                        value.Value.Year,
                        value.Value.Month,
                        value.Value.Day,
                        日時.Value.Hour,
                        日時.Value.Minute,
                        0);
                    this.日時 = dt;
                }
            }
        }
        public Nullable<System.DateTime> 日時_時分
        {
            get { return this.日時; }
            set
            {
                if (this.日時.HasValue)
                {
                    DateTime dt = new DateTime(
                        日時.Value.Year,
                        日時.Value.Month,
                        日時.Value.Day,
                        value.Value.Hour,
                        value.Value.Minute,
                        0);
                    this.日時 = dt;
                }
            }
        }
    }

ここで .NET6 への移植時に問題が発生します。

.NET Framework の EF では、テーブル構造が *.edmx ファイルに分離されているので、拡張した日時_年月日プロパティや日時_時分プロパティは更新時に無視されるのですが、.NET6 の EFCoreでは更新対象がEntityクラスの全プロパティとなるため、この拡張したプロパティを「無視」させるようにしなければなりません。

DbContext の OnModelCreating 内で、

            modelBuilder.Entity<単発アラーム>()
                .Ignore("日時_年月日")
                .Ignore("日時_時分");
            modelBuilder.Entity<周期アラーム>()
                .Ignore("周期月一週")
                .Ignore("周期毎年")
                .Ignore("周期毎月")
                .Ignore("周期毎週")
                .Ignore("周期月一曜日")
                ;

のように、拡張したプロパティを ignore するようにしていきます。この部分は、拡張したプロパティのほうに Ignore属性が付けられればよいのですが、無いようです。不便ですね。

問題はこのエラーの発生は、コンパイル時ではなく実行時にしか発生しないので、いちいち確かめないといけません。元の Entity クラスを拡張していると結構やっかいな問題です。

WindowsFormsHost を使う

.NET Framework の WPF アプリケーションでは WindowsFormsHost を使って、Windows フォームの各種コントロールを埋め込むことができます。

これは客先の「カレンダーのフォーマット/色合いを従来のものと同じにしてほしい」という要望を実現したものです。従来は ACCESS のカレンダーを使っているので、WPF カレンダーのものは使えません。

しかたがないので、ユーザーコントロールを作って WindowsFormsHost で埋め込みます。ユーザーコントロールにしたのは、あちこちの画面でこのカレンダーが出てくるので共通化するためです。

<Canvas Grid.Row="1" Grid.Column="0" x:Name="cv">
    <WindowsFormsHost FontSize="20" x:Name="host" >
        <wf:MonthCalendar 
            x:Name="cal" CalendarDimensions="3,1" DateSelected="MonthCalendar_DateSelected" />
    </WindowsFormsHost>
</Canvas>

実は、.NET6 では WindowsFormsHost の部分が使えなくて暫く悩んでいたのですが、

<UseWindowsForms>true</UseWindowsForms>

プロジェクトファイルに UseWindowsForms を設定して解決します。おそらく、.NET6 の WPF アプリケーションでツールボックスから WindowsFormsHost をドロップすると自動的に、UseWindowsForms が true になるはずです。

office.dll への参照を追加する

かなり悩んだのが次のところです。

MsoTriState が存在しないというエラーがでています。これは Excel の Shape を設定している部分なのですが、Excel COM を参照させてはいても、このエラーは取り除けません。おそらく Excel/Word に共通化されている Shape クラスは「office, Version=15.0.0.0」にあるようです。

さて、 「office, Version=15.0.0.0」 は一体どこにあるのでしょうか?

実は、.NET Framework のプロジェクトを見ると「アセンブリ」のタブのほうに「office 15.0.0.0」を参照しているところがあります。これは Office の COM ではなくて、.NET Framework が提供しているアセンブリのほうなんですね。

.NET6 では各種のアセンブリを個別で参照することはできない(自動的にアセンブリ参照が解決されるため)ので、明示的に office.dll を参照させます。

プロジェクトファイルに以下を記述しても ok です。

  <ItemGroup>
    <Reference Include="office">
      <HintPath>c:\Program Files (x86)\Microsoft Visual Studio\Shared\Visual Studio Tools for Office\PIA\Office15\Office.dll</HintPath>
    </Reference>
  </ItemGroup>

その他

その他 Excel COM の参照や Newtonsoft.Json、ClosedXML の参照は問題なく動いています。

EFCore の拡張プロパティの問題は、実際に動かしてみないとエラーが発生しないのでもう少し対処が必要です。

作業時間はおおむね1日がかりなので、移行としては十分可能なレベルですが、問題は FromSqlRaw への書き換えですかね。外部結合を使う場合、LINQ ではかなり手間なので生 SQL で組んでしまうのですが、プロジェクトによって数が多いと大変かと思います。

.NET6 にしておくと何が便利かと言うと TargetFramework を「net6.0-windows10.0.17763.0」にしておいて Windows Runtime(UWP や Windows App SDKの機能)が手軽に使えるようになります。まあ、業務用のデスクトップアプリなので、公開とかしないわけでそれほど利用価値があるわけではないのですが。ふりがな機能とか通知機能、連絡帳などが WinRT のほうに入っていたりします。

カテゴリー: 開発 パーマリンク