.NET MAUI (ローカルDBとしてEF.Coreの利用、サービスの依存性注入)

# はじめに

[前回](https://qiita.com/kashin777/items/03bcfa6c0f17178911e2)、.NET MAUIのデータストアについて調査しました。

https://qiita.com/kashin777/items/03bcfa6c0f17178911e2

ローカルDBとしてsqlite-net-pclを利用することができましたが、EntityFramework.Coreについては未調査だったので、試してみました。

また、サービスのDI(Dependency Injection = 依存性注入)が使えることが分かったので、それについても記載しておきます。

# 開発環境
* Win11
* VS2022 Preview Version 17.3.0 Preview 2.0

環境構築＆プロジェクト作成の流れはこちらの記事でどうぞ。

https://qiita.com/kashin777/items/eae43a555dc28867926f

# サンプルソース
[以前](https://qiita.com/kashin777/items/bb249717d65b492ab126)作った、データ検証(Validation)のソースの疑似ログイン処理を拡張していきます。

https://qiita.com/kashin777/items/bb249717d65b492ab126

一式GitHubに公開してます。

https://github.com/kashin777/MauiAppDemo

## NuGet

Microsoft.EntityFrameworkCore.Sqlite
EF Core のSQLite実装をNuGetします。

https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.Sqlite

## Table
ユーザーテーブルを作成します。

[User.cs](https://github.com/kashin777/MauiAppDemo/blob/main/Models/User.cs)

```C#
using MauiAppDemo.Pages;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MauiAppDemo.Models
{
    [Table(nameof(User))]
    public class User : ValidationPropertyModel
    {
        private int _No;
        private string _Name;
        private string _Password;

        [Display(Order = 1, Name = nameof(Messages.User_No), ResourceType = typeof(Messages))]
        [Key]
        [Required(ErrorMessageResourceName = nameof(Messages.Error_Required), ErrorMessageResourceType = typeof(Messages))]
        [Range(1, 9999, ErrorMessageResourceName = nameof(Messages.Error_Range), ErrorMessageResourceType = typeof(Messages))]
        public int No
        {
            get => _No;
            set => SetProperty(ref _No, value);
        }

        [Column(nameof(Name))]
        [Display(Order = 2, Name = nameof(Messages.User_Name), ResourceType = typeof(Messages))]
        [Required(ErrorMessageResourceName = nameof(Messages.Error_Required), ErrorMessageResourceType = typeof(Messages))]
        [MinLength(1, ErrorMessageResourceName = nameof(Messages.Error_Required), ErrorMessageResourceType = typeof(Messages))]
        public string Name
        {
            get => _Name;
            set => SetProperty(ref _Name, value);
        }

        [Column(nameof(Password))]
        [Display(Order = 3, Name = nameof(Messages.User_Password), ResourceType = typeof(Messages))]
        [Required(ErrorMessageResourceName = nameof(Messages.Error_Required), ErrorMessageResourceType = typeof(Messages))]
        [MinLength(1, ErrorMessageResourceName = nameof(Messages.Error_Required), ErrorMessageResourceType = typeof(Messages))]
        public string Password
        {
            get => _Password;
            set => SetProperty(ref _Password, value);
        }
    }
}
```

## Database
User テーブルのみのデータベースを作成します。

[MauiAppDemoDbContext.cs](https://github.com/kashin777/MauiAppDemo/blob/main/Models/MauiAppDemoDbContext.cs)

```C#
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MauiAppDemo.Models
{
    public class MauiAppDemoDbContext : DbContext
    {
        public DbSet<User> Users { get; set; }

        private string connStr;

        public MauiAppDemoDbContext()
        {
            connStr = Path.Combine(FileSystem.AppDataDirectory, "MauiAppDemo.sq3");
        }

        protected override void OnConfiguring(DbContextOptionsBuilder options)
            => options.UseSqlite($"Data Source={connStr}");
    }
}
```

## DI(Dependency Injection) in .NET MAUI
アプリケーション起動時にDbContextを作ったら、いろんなページから利用したいところです。
そこで、.NET MAUIのサービスのDIの仕組みを利用します。

### サービスを作成

DbContext取得サービスのインターフェース

[IMauiAppDemoService.cs](https://github.com/kashin777/MauiAppDemo/blob/main/Services/IMauiAppDemoService.cs)
```C#
using MauiAppDemo.Models;

namespace MauiAppDemo.Services;

public interface IMauiAppDemoService
{
    public MauiAppDemoDbContext DBContext { get; }
}
```

DbContext取得サービスの実装
※DBを作成、テーブルがなければ作成、初期ユーザーがいなければ追加

[MauiAppDemoService.cs](https://github.com/kashin777/MauiAppDemo/blob/main/Services/MauiAppDemoService.cs)
```C#
using MauiAppDemo.Models;
using MauiAppDemo.Utils;

namespace MauiAppDemo.Services;

public class MauiAppDemoService : IMauiAppDemoService
{
	private MauiAppDemoDbContext ctx;

	public MauiAppDemoDbContext DBContext
	{
		get
		{
			if (ctx == null)
			{
				ctx = new MauiAppDemoDbContext();

				ctx.Database.EnsureCreated();

				using (var t = ctx.Database.BeginTransaction())
				{
					var count = ctx.Users.Count();
					if (count == 0)
					{
						ctx.Users.Add(new User() { No = 1234, Name = "Kashin777", Password = Hashs.Sha256("1234", "1234") });
						ctx.Users.Add(new User() { No = 9999, Name = "User9999", Password = Hashs.Sha256("9999", "Password") });
					}

					t.Commit();
				}
			}

			return ctx;
		}
	}
}
```

### DIの設定
DbContext取得サービス、それを利用したいクラス(ViewModel、ContentPage)をサービスに追加します。
※ソースの★の部分

[MauiProgram.cs](https://github.com/kashin777/MauiAppDemo/blob/main/MauiProgram.cs)
```C#
using Microsoft.Maui.Platform;
using MauiAppDemo.Pages;
using MauiAppDemo.Services;

namespace MauiAppDemo;

public static partial class MauiProgram
{
	public static MauiApp CreateMauiApp()
	{
		var builder = MauiApp.CreateBuilder();
		builder
			.UseMauiApp<App>()
			.ConfigureFonts(fonts =>
			{
				fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
				fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
			});

		// ★ Depedency Injection
        builder.Services
			.AddTransient<LoginPage>()
			.AddTransient<LoginPageViewModel>()
			.AddTransient<MainPage>()
			.AddTransient<MainPageViewModel>()
			.AddSingleton<IMauiAppDemoService, MauiAppDemoService>();

		// プラットフォームに応じたカスタマイズ
        Microsoft.Maui.Handlers.EntryHandler.Mapper.ModifyMapping(nameof(IEntry.Background), (handler, entry, action) => 
		{
#if ANDROID
            handler.PlatformView.BackgroundTintList = Android.Content.Res.ColorStateList.ValueOf(Colors.Gray.ToPlatform());
#endif
		});

        return builder.Build();
	}
}
```

Add～によって、DIされるインスタンスのライフサイクルが変わります。
```C#
// 都度生成
builder.Services.AddTransient()
// スコープ間で1回生成
builder.Services.AddScoped()
// 常に1個
builder.Services.AddSingleton()
```

### DI適用後のログイン処理
ContentPage -> ViewModel -> DbContextサービスの流れでDIします。
コントラクタ引数を追加することで、勝手にインスタンスが割り当てられます。
※ソースの★部分

[LoginPage.xaml.cs](https://github.com/kashin777/MauiAppDemo/blob/main/Pages/LoginPage.xaml.cs)
```C#
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Windows.Input;
using MauiAppDemo.Models;
using MauiAppDemo.Utils;
using MauiAppDemo.Services;

namespace MauiAppDemo.Pages;

/// <summary>
/// ログインページ。
/// </summary>
public partial class LoginPage : ContentPage
{
    //★ LoginPageにViewModelをDI
    public LoginPage(LoginPageViewModel viewModel)
	{
        // 言語を変更
        var culture = new CultureInfo(Preferences.Get("Language", "ja"));
        CultureInfo.CurrentCulture = culture;
        CultureInfo.CurrentUICulture = culture;

        InitializeComponent();

        //★ DIされたViewModelをBindingContextに設定
        BindingContext = viewModel;
    }
}

/// <summary>
/// ログインページのビューモデル。
/// </summary>
public class LoginPageViewModel : ValidationPropertyViewModel
{
    private StringKeyValuPair _Language = new StringKeyValuPair() { Key = Preferences.Get("Language", "ja") };
    private int? _No;
    private string _Password;

    /// <summary>
    /// 社員番号
    /// </summary>
    [Display(Order = 1, Name = nameof(Messages.Login_No), ResourceType = typeof(Messages))]
    [Required(ErrorMessageResourceName = nameof(Messages.Error_Required), ErrorMessageResourceType = typeof(Messages))]
    [Range(1, 9999, ErrorMessageResourceName = nameof(Messages.Error_Range), ErrorMessageResourceType = typeof(Messages))]
    public int? No {
        set
        {
            SetProperty(ref _No, value);
            ChangeCanExecute(LoginCommand);
        }
        get => _No;	
    }

    /// <summary>
    /// パスワード
    /// </summary>
    [Display(Order = 2, Name = nameof(Messages.Login_Password), ResourceType = typeof(Messages))]
    [Required(ErrorMessageResourceName = nameof(Messages.Error_Required), ErrorMessageResourceType = typeof(Messages))]
    [MinLength(1, ErrorMessageResourceName = nameof(Messages.Error_Required), ErrorMessageResourceType = typeof(Messages))]
    public string Password
    {
        set
        {
            SetProperty(ref _Password, value);
            ChangeCanExecute(LoginCommand);
        }
        get => _Password;
    }

    /// <summary>
    /// 言語
    /// </summary>
    public StringKeyValuPair Language {
        set { 
            SetProperty(ref _Language, value);
            ChangeCanExecute(LanguageCommand);
        }
        get => _Language;
    }

    /// <summary>
    /// 言語変更コマンド。
    /// </summary>
    public ICommand LanguageCommand { protected set; get; }

    /// <summary>
    /// ログインコマンド。
    /// </summary>
    public ICommand LoginCommand { protected set; get; }

    //★ ViewModelにDbContext取得サービスをDI
    public LoginPageViewModel(IMauiAppDemoService service)
    {
        // 言語変更コマンドの実装
        LanguageCommand = new Command(() => 
        {
            // 言語を保存
            Preferences.Set("Language", Language.Key);

            // 画面を初期化
            App.Current.MainPage = new AppShell();
        }, 
        () => { 
            return !Language.Key.Equals(CultureInfo.CurrentUICulture.TwoLetterISOLanguageName); 
        });

        // ログインコマンドの実装
        LoginCommand = new Command(async() =>
        {
            // データ検証
            if (Validate())
            {

                //★ DbContextを取得
                var ctx = service.DBContext;

                // 社員番号とパスワードが一致するか？
                var user = await ctx.FindAsync<User>(No);
                if (user != null && user.Password.Equals(Hashs.Sha256(user.No.ToString(), Password)))
                {
                    // メインページへ移動
                    await Shell.Current.GoToAsync($"///MainPage");

                    // ログインユーザを通知
                    MessagingCenter.Send<User>(user, "Login");

                    // 初期化
                    No = null;
                    Password = null;
                    ClearErrors();
                }
                // 一致しない場合
                else
                {
                    AddModelError(Messages.Error_Comman_Login);
                }
            }

            ChangeCanExecute(LoginCommand);
        },
        () =>
        {
            // 画面にエラーが出ている場合は無効
            return !HasErrors;
        });

        // コマンドを登録
        AddCommands(LanguageCommand, LoginCommand);
    }
}
```

# ご参考

https://www.syncfusion.com/blogs/post/learn-how-to-use-dependency-injection-in-net-maui.aspx