C# - Azure FunctionsでEntity Frameworkを利用する

Entity Frameworkはデータベースへのアクセスを容易にしてくれるライブラリです。

.NET Coreを利用している場合はそれに対応した Entity Framework Core を使います。

 ー 開発環境 ー 

Visual Studio 2019
.NET Core 3.1

NuGetからパッケージをインストールする

NuGetから以下のパッケージをインストールしましょう。

  • Microsoft.EntityFrameworkCore
  • Microsoft.EntityFrameworkCore.Tools

 

また、今回はデータベースとしてSQL Serverを利用するので以下のパッケージもインストールします。

  • Microsoft.EntityFrameworkCore.SqlServer

 

Visual Studioの NuGetパッケージマネージャーからインストールすることができます。

 

メニューから「プロジェクト(P)」→「NuGet パッケージの管理(N)...」を選択します。

Microsoft.EntityFrameworkCore」を検索し必要なパッケージをインストールします。

バージョンに注意!

Entity Framework Core の最新バージョンは 5.0.x ですが、Azure Functions の対象フレームワークが .NET Core 3.1 の場合は Entity Framework Core も 3.1.x を使用する必要があります。

 

誤って、最新バージョンをインストールすると実行時に以下のようなエラーが発生してしまいます。

A host error has occurred during startup operation 'xxxxxxxxxxxxxxxxxx'.
System.Private.CoreLib: Could not load file or assembly 'Microsoft.Extensions.Logging.Abstractions, Version=5.0.0.0, Culture=neutral, PublicKeyToken=xxxxx'.
指定されたファイルが見つかりません。.

データベースからモデルを作成する

Entity Frameworkではデータベースのテーブル構造をモデルにしたクラスを作成する必要があります。

既にデータベースがある場合にはデータベースから自動的にクラスを生成することが可能です。

 

Visual Studioのパッケージマネージャーコンソールから自動生成を実行できます。

 

メニューから「ツール(T)」→「NuGet パッケージ マネージャー(N)」→「パッケージ マネージャー コンソール(O)」を選択します。

下記の例のようにコンソールからScaffold-DbContextコマンドを実行します。

※自身の環境に合わせて書き換えてください

Scaffold-DbContext -Connection "xxxx" -Provider Microsoft.EntityFrameworkCore.SqlServer -OutputDir "Models\TestDatabase" -Context "TestDbContext" -DataAnnotations -UseDatabaseNames -Force

Scaffold-DbContextコマンドのパラメータは以下のような意味になります。

https://docs.microsoft.com/ja-jp/ef/core/cli/powershell#scaffold-dbcontext

 

-Connection

データベースの接続文字列を指定します。

SQL Serverの場合は

Data Source=[サーバー名];Database=[データベース名];user id=[ユーザー名];password=[パスワード]

のようになります。

 

-Provider

利用するデータベースのプロバイダを指定します。

SQL Server の場合は Microsoft.EntityFrameworkCore.SqlServer です。

 

-OutputDir

ファイルを格納するディレクトリを指定します。

プロジェクトディレクトリの相対パスを指定します。 

 

 -Context

DbContextを継承したクラスが生成されるので、そのクラス名を指定します。 

 

-DataAnnotations

属性を使用してモデルを構成します (可能な場合)。

このパラメーターを省略した場合は、fluent API のみが使用されます。 

 

-UseDatabaseNames

テーブル名と列名はデータベースに表示されるとおりに使用します。

このパラメーターを省略した場合、C# の名前のスタイル規則により厳密に準拠するように変更されます。 

 

-Force

既存のファイルを上書きします。

完了すると、DbContextクラスを継承するクラスと、データベースのテーブルに対応するクラスが生成されます。

ソースコードに書かれたデータベース接続文字列を消す

DbContextクラスを継承したクラス(例ではTestDbContextクラス)が自動生成されますが、このままではビルド時に次のような警告が出てしまいます。

#warning To protect potentially sensitive information in your connection string, you should move it out of source code. See http://go.microsoft.com/fwlink/?LinkId=723263 for guidance on storing connection strings.

データベース接続文字列のような機密性の高い情報はソースコードには書かないで!」という警告です。

 

TestDbContextクラスの中を見てみると、以下のようにデータベース接続文字列がソースコードの中に書かれてしまっています。 とりあえずテスト的に動作させることが出来るようにソースコードに書かれていますが危険なので消しましょう。

namespace AzureFunctionTest.Models.TestDatabase
{
    public partial class TestDbContext : DbContext
    {
        public TestDbContext()
        {
        }

        public TestDbContext(DbContextOptions<TestDbContext> options) : base(options)
        {
        }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            if (!optionsBuilder.IsConfigured)
            {
#warning To protect potentially sensitive information in your connection string, you should move it out of source code. See http://go.microsoft.com/fwlink/?LinkId=723263 for guidance on storing connection strings.
                optionsBuilder.UseSqlServer("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
            }
        }
    }
}

このオーバーライドされた OnConfiguringメソッドごと消してしまいましょう。

 

データベース接続文字列を環境変数に定義

Azureで機密性の高い情報を管理するには環境変数を使用します。 

参考:C# - Azure Functionsで環境変数を利用する

 

ローカルテスト環境用の環境変数

ローカル環境では「local.settings.json」というJSON形式のファイルを使って環境変数を定義します。
「local.settings.json」を開いてValuesに接続文字列を定義します。

 

以下の例では「TestDBContextConnectionString」という名前の定義を追加しています。

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet",
    "TestDBContextConnectionString": "xxxxxxxxxxxxxxxx"
  }
}

Azureサーバー環境用の環境変数

Azureポータルサイトからでも設定できますが、ここではVisual Studioから設定してみます。

 

メニューから「ビルド(B)」→「発行(H)」を選択して公開画面を表示します。
ホスティング欄にある「…」ボタンをクリックして「Azure App Serviceの設定を管理する」を選択しましょう。

                     ↓
local.settings.jsonに記述した内容がリストアップされます。
ローカル欄にはlocal.settings.jsonに指定した値が入っているはずです。
リモート欄にも接続文字列を入力して「OK」をクリックしましょう。

環境変数からデータベース接続文字列を取得する

先ほどTestDbContextクラスからデータベース接続文字列を消してしまったので、改めて接続文字列をセットするように改良します。

 

環境変数からデータベース接続文字列を取得し、TestDbContextクラスへセットするにはDI(Dependency injection)という設計モデルを使って行うのが良いようです。

参考: C# - DI(Dependency injection)依存性の注入とは

C# - DI(Dependency injection)コンテナを使う

C# - Azure FunctionsでDI(Dependency injection)を利用する

 

FunctionsStartupクラスを継承したクラスを作成する

Startupクラスを作成してTestDbContextクラスをDIコンテナへ登録します。

using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.EntityFrameworkCore;
using AzureFunctionTest.Models.TestDatabase;

[assembly: FunctionsStartup((typeof(AzureFunctionTest.Startup)))]
namespace AzureFunctionTest
{
    public class Startup : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
            var connectionString = System.Environment.GetEnvironmentVariable("TestDBContextConnectionString");
            builder.Services.AddDbContext<TestDbContext>(options => options.UseSqlServer(connectionString));
        }
    }
}

環境変数から値を取得するには Environment.GetEnvironmentVariableメソッドを使います。

(13行目)

 

DIコンテナへDbContextクラスを登録するにはAddDbContextメソッドを使います。(14行目)

データベースにSQL Serverを利用する場合にはAddDbContextメソッドの引数となる options のUseSqlServerメソッドから接続文字列をセットします。

※Microsoft.EntityFrameworkCoreをusingする必要があります

DIコンテナからDbContextインスタンスを取得する

FunctionsクラスのコンストラクタでTestDbContextのインスタンスを取得します。

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 AzureFunctionTest.Models.TestDatabase;

namespace AzureFunctionTest
{
    public class TestFunction
    {
        private readonly TestDbContext TestDB;


        public TestFunction(TestDbContext db)
        {
            TestDB = db;
        }

        [FunctionName("TestFunction")]
        public async TaskGlt;IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
            ILogger log)
        {

            return new OkResult();
        }
    }
}