Azure FunctionsでEF操作する

# はじめに
Azure FunctionsでEntity Frameworkを使ってDB操作をしてみました
Entity Frameworkを使うにあたり、テストコードが書きにくかったため、DI(Dependency Injection)というソフトウェアデザインを使いました
わかりやすい記事がありましたので、以下にリンクを記載します
(自分もDIについて完全に理解しているとは言えない😶)

https://qiita.com/hshimo/items/1136087e1c6e5c5b0d9f

https://learn.microsoft.com/ja-jp/azure/azure-functions/functions-dotnet-dependency-injection

https://learn.microsoft.com/ja-jp/dotnet/architecture/modern-web-apps-azure/architectural-principles#dependency-inversion

サンプルコードをGitHubに公開してます🎉

https://github.com/Kazunari001/omochabako/tree/main/AzureFuncDITest

# 環境
* Visual Studio Code
* Windows11 Pro
* SQL Server Management Service

# 作業手順
1. Visual Studio CodeでAzure Functionsのプロジェクトを作成します
1. Startup.csを作成します
1. モデルを作成します
1. DataContext環境を作成します
1. 関数でDB操作を行います
1. 稼働確認します
1. テストコードを書きます
1. テストします

# ①Visual Studio CodeでAzure Functionsのプロジェクトを作成します
Azureマークをクリックしその後、WorkSpaceのFunctionマークをクリックします。
その後、以下の順にクリックしていきます。

C#
.Net6.0
HTTP trigger
HTTPTrigger(機能名、今回はデフォルトのままです)
Company.Function(namespace、今回はデフォルトのままです)
Function

# ②Startup.csを作成します
nugetパッケージをインストールします
```powershell
dotnet add package Microsoft.Azure.Functions.Extensions
dotnet add package Microsoft.NET.Sdk.Functions
dotnet add package Microsoft.Extensions.DependencyInjection --version 6.0.1
```

①で作成されたプロジェクト直下にStartup.csを作成します
```csharp
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

[assembly: FunctionsStartup(typeof(Company.Function.Startup))]
namespace Company.Function
{
    public class Startup : FunctionsStartup
    {
        public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder)
        {
            base.ConfigureAppConfiguration(builder);
        }

        public override void Configure(IFunctionsHostBuilder builder)
        {
            builder.Services.AddLogging(loggingBuilder =>
            {
                loggingBuilder.AddFilter(level => true);
            });

            // 後でDBContext記載
        }
    }
}
```

# ③モデルを作成します
nugetパッケージをインストールします
```powershell
dotnet add package Microsoft.EntityFrameworkCore --version 6.0.20
dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 6.0.20
```

①で作成されたプロジェクト直下にModelsフォルダを作成します
その下にUserMaster.csを作成します
```csharp
using System;
using System.ComponentModel.DataAnnotations.Schema;

namespace Company.Function.Models
{
    [Table("UserMaster")]
    public class UserMaster
    {
        [Column("Id")]
        public int Id { get; set; }
        [Column("UserName")]
        public string Name { get; set; }
        [Column("Email")]
        public string Email { get; set; }
        [Column("Tel")]
        public string Tel { get; set; }
        [Column("Address")]
        public string Address { get; set; }
        [Column("Password")]
        public string Password { get; set; }
        [Column("IsAdmin")]
        public bool IsAdmin {get; set;}
        [Column("EntryDateTime")]
        public DateTime EntryDateTime {get; set;}
        [Column("UpdateDateTime")]
        public DateTime UpdateDateTime {get; set;}
        [Column("IsDeleteFlag")]
        public bool IsDeleteFlag {get; set;}
    }
}
```
# ④DataContext環境を作成します
インタフェースを実装します
```csharp
using System.Collections.Generic;

namespace Company.Function.Models
{
    public interface IUserRepository
    {
        IEnumerable<UserMaster> GetAll();
        void SaveChanges();
    }
}
```

DbContextを作成します
```csharp
using Microsoft.EntityFrameworkCore;

namespace Company.Function.Models
{
    public class UserDbContext : DbContext
    {
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(@"Persist Security Info=False;Trusted_Connection=True;database=Poc;server=localhost\SQLEXPRESS;");
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<UserMaster>().Property(p => p.Id);
        }

        public DbSet<UserMaster> UserItems { get; set; }
    }
}
```

リポジトリを作成します
```csharp
using System.Collections.Generic;

namespace Company.Function.Models
{
    public class UserRepository : IUserRepository
    {
        private readonly UserDbContext _context;

        public UserRepository(UserDbContext context)
        {
            _context = context;
        }

        public IEnumerable<UserMaster> GetAll()
        {
            return _context.UserItems;
        }

        public void SaveChanges()
        {
            _context.SaveChanges();
        }
    }
}
```

# ⑤関数でDB操作を行います
HTTPTrigger1.csを編集します。
```csharp
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Company.Function.Models;

namespace Company.Function
{
    public class HttpTrigger1
    {
        private readonly IUserRepository _repo;

        public HttpTrigger1(IUserRepository repo)
        {
            _repo = repo;
        }

        [FunctionName("HttpTrigger1")]
        public async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", Route = null)] HttpRequest req,
            ILogger log)
        {
            var users = _repo.GetAll();
            string responseMessage = JsonConvert.SerializeObject(users);
            return new OkObjectResult(responseMessage);
        }
    }
}
```

Startup.csを編集します
```csharp
using Company.Function.Models;
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

[assembly: FunctionsStartup(typeof(Company.Function.Startup))]
namespace Company.Function
{
    public class Startup : FunctionsStartup
    {
        public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder)
        {
            base.ConfigureAppConfiguration(builder);
        }

        public override void Configure(IFunctionsHostBuilder builder)
        {
            builder.Services.AddLogging(loggingBuilder =>
            {
                loggingBuilder.AddFilter(level => true);
            });

            builder.Services.AddDbContext<UserDbContext>();
            builder.Services.AddScoped<IUserRepository, UserRepository>();
        }
    }
}
```

# ⑥稼働確認します
ステータス200で返ってくることを確認します
```powershell
Invoke-WebRequest -Uri "http://localhost:7071/api/HttpTrigger1" -Method Get
```

# ⑦テストコードを書きます
テストデータはDBにアクセスせずモックデータを作成してモックデータを操作します(リポジトリパターンという設計です)

https://learn.microsoft.com/ja-jp/ef/core/testing/testing-without-the-database

Azure Functionsとは別の階層でテストプロジェクトを作成します
テストプロジェクト作成後にAzure Functionsを参照させます
```powershell
dotnet new xunit -f net6.0 -o PocFunc.Test
cd PocFunc.Test
dotnet add reference <Azure Functionsプロジェクトパス>\PoCFunc.csproj
dotnet add package Moq --version 4.18.4
dotnet add package Microsoft.AspNetCore.Mvc
dotnet add package System.Data.Entity.Repository --version 2.0.0.1
```

モックデータデータを作成します
テストプロジェクト配下にModelsフォルダを作成します
Models配下にTestDbSet.csを作成します
```csharp
using Moq;
using Company.Function.Models;

namespace PocFunc.Test.Models
{
    public class TestDbSet
    {
        public Mock<IUserRepository> _repo = new Mock<IUserRepository>();
        private readonly List<UserMaster> _data = new List<UserMaster>()
        {
            new UserMaster { Id = "1", Name = "Test User 1", Email = "1234567890", Tel = "1234567890", Address = "1234567890", Password = "1234567890", IsAdmin = false, EntryDateTime = DateTime.Now, UpdateDateTime = DateTime.Now, IsDeleteFlag = false },
            // 以下に同じようなデータを追加する
        };
        public TestDbSet()
        {
            _repo.Setup(x => x.GetAll()).Returns(_data);
        }
    }
}
```

UnitTest1.csを編集します
```csharp
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging.Abstractions;
using PocFunc.Test.Models;
using Company.Function;

namespace PocFunc.Test
{
    public class UnitTest1
    {
        [Fact]
        public void Test1()
        {
            var mock = new TestDbSet();
            HttpTrigger1 httpTrigger1 = new HttpTrigger1(mock._repo.Object);
            var logger = NullLoggerFactory.Instance.CreateLogger("Null Logger");
            var result = httpTrigger1.Run(null, logger);
            Assert.IsType<OkObjectResult>(result.Result);
        }
    }
}
```

# ⑧テストします
以下を実行して合格が1件あることを確認します
```powershell
dotnet test <テストプロジェクトのパス>\PocFunc.Test.csproj
```

# 所感
Azure FunctionsでEntity Frameworkを使ってDB操作をしてみました
EFを使うことでコードベースで色々操作できて利便性が増したと感じました
この使い方がベストプラクティスかといわれるとどうかなと思うので、まだまだ勉強を続けたいと思います