Viewのマイグレーションを考える

2019年3月18日 コメントを残す

異なるスキーマを参照するViewを作って、Entity Frameworkから参照したいという要件があって、Viewの更新管理をどうにかしたいなーと思っていたんだけれど、だいたい固まったのでメモしておく

方針としてはこんな感じ

  1. Viewの形のEntityを作り、DbContextにDbQueryとして公開する
  2. OnModelCreatingで、Query~ToViewで関連付ける
  3. 空のマイグレーションファイルを作り、upでcreate viewを、downでdrop viewを行う。スキーマの差し替えもこのタイミングで行う。

サンプル
https://github.com/karuakun/EntityFrameworkSchemaAcrossDbViewSample

EntityとDbContext

1,2はササッとこんな感じで
Data/Entities/QueryTest.cs

    public class Test1
    {
        [Key]
        public string Id { get; set; }

        public string Name { get; set; }
        public string QueryTest2Id { get; set; }
    }

Data/SampleDbContext.cs

    public class SampleDbContext: DbContext
    {
        public const string DefaultConnectionStringName = "sampledb";

        public SampleDbContext(DbContextOptions options) : base(options)
        {
        }

        public virtual DbQuery QueryTest2 { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
            modelBuilder.Query(entity =>
            {
                entity.ToView("v_seconddb_test2");
            });
        }
    }

マイグレーションの作成

DbContextにマイグレーションに関わる変更がない状態で、マイグレーションファイルを追加すると、空のマイグレーションファイルが出来上がるので、この子にViewの作成と削除を書いてあげる。

dotnet migrations add addsampleview

追加されたマイグレーションファイル

    public partial class addview : Migration
    {
        protected override void Up(MigrationBuilder migrationBuilder)
        {
        }

        protected override void Down(MigrationBuilder migrationBuilder)
        {
        }
    }

まずは単純に

create view にスキーマ名が入っているので気持ち悪い。。。

    public partial class addview : Migration
    {
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.Sql(
                "create view v_seconddb_test2 as select Id, Name, Property1 from schema2.test2");
        }

        protected override void Down(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.Sql("drop view v_seconddb_test2");
        }
    }

Configurationもしくは環境変数から、マッピングを取得する

Startup時に作るIConfigurationRootにアクセスできれば良いんだけれど、MigrationだとDIでConfigurationを受け取ることができない。
ConfigurationをStartupのstaticにしてあげるのは嫌なので、、、DesignTimeContextを使う

こんなクラスをプロジェクトのルートに置いておくと、dotnet efで起動した場合にこっちのコンテキストで初期化してくれる。これなら多少無茶してもいいかなぁ、、、と言うことで、IConfigurationRootをstaticで生やしてあげる。

    public class DesignTimeSampleDbContextFactory : IDesignTimeDbContextFactory
    {
        public static IConfigurationRoot DesignTimeConfiguration { get; private set; }

        public SampleDbContext CreateDbContext(string[] args)
        {
            DesignTimeConfiguration = DesignTimeConfigurationBuilder.BuildConfiguration(args);
            var builder = new DbContextOptionsBuilder()
                .UseMySql(DesignTimeConfiguration.GetConnectionString(SampleDbContext.DefaultConnectionStringName)
                );
            return new SampleDbContext(builder.Options);
        }
    }

Configurationを参照できれば、あとはJSONと環境変数の世界なので、appsettings.jsonにこんな感じでエントリを追加して

  "MigrationSettings": {
    "MigrationType": "Local",

    "SchemaMappings": {
      "Local": {
        "AppDb": "appdb", 
        "SecondDb": "seconddb"
      },
      "Stating": {
        "AppDb": "stg_appdb",
        "SecondDb": "stg_seconddb"
      }
    }
  },

設定を受けるようにこんなクラスを作っておく

    public class MigrationSchemaMappings
    {
        public string AppDb { get; set; }
        public string SecondDb { get; set; }
    }

で、手作業で作ったSQLがマイグレーションの海に埋もれるのは悲しいので、手作業で作った子はマイグレーション配下にこんな感じでmanualフォルダーを作って、そこに物理ファイルとしておいておきたい。
無題

マイグレーションの拡張メソッドとして、こんな感じのものを作っておけば、

    public static class MigrationExtensions
    {
        public static string GetSqlText(this Migration source, string migrationId, string fileName)
        {
            var sqlPath = Path.Combine(Environment.CurrentDirectory, "Migrations", "manual", migrationId, fileName);
            if (!File.Exists(sqlPath))
                throw new FileNotFoundException(sqlPath);

            var sqlText = File.ReadAllText(sqlPath);
            if (string.IsNullOrEmpty(sqlText))
                throw new InvalidOperationException($"sqlfile is empty: {sqlText}");

            var settings = GetMigrationSettings();
            var schemaReplacedSqlText = sqlText.Replace("$SecondDb$", settings.SecondDb);
            return schemaReplacedSqlText;
        }

        private static MigrationSchemaMappings GetMigrationSettings()
        {
            var configuration = DesignTimeSampleDbContextFactory.DesignTimeConfiguration;
            var migrationType = configuration.GetValue("MigrationSettings:MigrationType");
            return configuration.GetSection($"MigrationSettings:SchemaMappings:{migrationType}")
                .Get();
        }
    }

マイグレーションスクリプトはこうかける。

    public partial class addview : Migration
    {
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.Sql(this.GetSqlText(this.GetId(), "up.v_seconddb_test2.sql"));
        }

        protected override void Down(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.Sql(this.GetSqlText(this.GetId(), "down.v_seconddb_test2.sql"));
        }
    }
広告
カテゴリー:ASP.NET, Visual Studio

asp.net core を linux 上で動かす場合、環境変数でConfigurationを上書きする場合はコロンではなく、ダブルアンダースコアを利用する。

asp.net core を利用する場合、設定情報を jsonファイルだけでなく、環境変数やコマンドライン引数からも読み込みマージして利用することができます。

例えばこんな appsettings.json にこんな設定を書いた場合、

{
  "HogeConfig": {
    "Value": "ValueHoge",
    "ValueList": [
      "a", "b", "c"
    ]
  } 
}

Program.cs でこんな感じに設定ファイル>環境変数>コマンドラインから読み込むように定義して置くと、

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

        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .ConfigureAppConfiguration(cfg =>
                {
                    cfg.SetBasePath(Directory.GetCurrentDirectory())
                        .AddJsonFile("appsettings.json")
                        .AddEnvironmentVariables()
                        .AddCommandLine(args);
                })
                .UseStartup();
    }

利用側は環境変数や、コマンドライン引数で HogeConfig:Value=Hogegege のように設定値を上書きすることができます。
ただし、linux などの環境では、 export HogeConfig:Value=Hogegege や HogeConfig:0=abc とかすると、識別子が不正(not a valid identifier)とかいって怒られてしまいます。
(docker-compose の environment で指定する分にはコロン区切りでも使えるのに、、、)

この場合、コロンの代わりにアンダーバーを2つ()使うことで回避できるようです。
ASP.NET Coreのドキュメントを見ると、
はすべての環境で利用できると書いてあるので、できるだけこっちを使ったほうが良いのかもしれませんね。
https://docs.microsoft.com/ja-jp/aspnet/core/fundamentals/configuration/index?tabs=basicconfiguration&view=aspnetcore-2.2#conventions

カテゴリー:ASP.NET

自分なりにRailsの開発用ベースコンテナを作った

2019年2月24日 コメントを残す

Rails自体はなんとなく基礎は知っていて、入門本を一通り読んだぐらいの知識しかないわけだけれど、コードレビューできるぐいらにはなっときたいなということで、とりあえず環境構築を簡略化するためのDockerコンテナを自分なりに整理したので、とりあえず公開しておく。

バージョンの違いなんかで動いた、動かないは辛いし、他の人に環境を作ってっていう場合にフォローするのが大変なので、この頃は基本的にDockerでの開発イメージをベースにしている。
https://github.com/karuakun/ruby-practise-base

利用方法はREADME.mdに書いたけれど、同じコンテナに対して2回 Docker Build しかも2回目は –no-cache しないと行けないのが気になる。。。

カテゴリー:未分類

Antaris/RazorEngine は Windows 環境以外サポートしていない?

2019年2月20日 コメントを残す

LinuxのDockerコンテナで動かしたら、こんな例外が発生しました。

RazorEngine.Templating.TemplateCompilationException : Errors while compiling a Template.
Please try the following to solve the situation:
* If the problem is about missing/invalid references or multiple defines either try to load
the missing references manually (in the compiling appdomain!) or
Specify your references manually by providing your own IReferenceResolver implementation.
See https://antaris.github.io/RazorEngine/ReferenceResolver.html for details.
Currently all references have to be available as files!
* If you get ‘class’ does not contain a definition for ‘member’:
try another modelType (for example ‘null’ to make the model dynamic).
NOTE: You CANNOT use typeof(dynamic) to make the model dynamic!
Or try to use static instead of anonymous/dynamic types.
More details about the error:
– warning: (6, 4) Unnecessary using directive.
– warning: (5, 4) Unnecessary using directive.
– warning: (9, 4) Unnecessary using directive.
– warning: (7, 4) Unnecessary using directive.
– warning: (8, 4) Unnecessary using directive.
– error: (0, 0) Unexpected error writing debug information — ‘COM Interop is not supported on this platform.’
Temporary files of the compilation can be found in (please delete the folder): /tmp/RazorEngine_xbytpgsp.sjz
The template we tried to compile is:

ん?

Unexpected error writing debug information — ‘COM Interop is not supported on this platform.’

RazorEngine の issue を見ると出てますね。
https://github.com/Antaris/RazorEngine/issues/539

とりあえず他のテキストテンプレートエンジンを探します。
しかし、どこにCOMの相互運用なんて使うんだろう。

カテゴリー:ASP.NET, Docker

NSwagを使って、OpenIdConnectのリクエストを投げる

2019年2月20日 コメントを残す

Docs の内容のそのまま実装すると、バージョンが微妙に古いらしく警告が出るのと、認証が必要なAPIの定義方法が書いていなかったのでメモです。

はじめに

ASP.NET WebApi のコントローラー定義をもとに OpenAPI 形式のドキュメントを出力してくれる拡張としては、Swashbuckle が有名ですが、NSwag という拡張もあります。

Microsoft Docs NSwag と ASP.NET Core の概要
https://docs.microsoft.com/ja-jp/aspnet/core/tutorials/getting-started-with-nswag?view=aspnetcore-2.2

認証の概要

WebAPI を呼び出すSPAがこんな感じで OpenId Connect のサーバーにトークンを要求しているとして、

http://localhost:5000/auth/connect/authorize?client_id=test.app&redirect_uri=http%3A%2F%2Flocalhost%3A5012%2Fcallback.html&response_type=id_token%20token&scope=openid%20profile%test.api&state=4d76e302ed5742afaff207b1aee6710e&nonce=ead99dbe46d442d18c72f3691f2766ca

ここでは http://localhost:5000 が認証サーバーで、http://localhost:5012 が認証を受けるリソースサーバー(本来はSPAがこれに当たるけれど、今回はSwaggerがSPAの代わりに認証を受けるのでWebApi サーバーとイコール)になります。

SPA を想定しているので、レスポンスタイプは implicit(id_token token) を指定しています。
また、WebAPI は test.api というリソースとして定義しているので、scope に test.api を含めています。

SPA は認証サーバーから返ってきた IDトークン を HTTPヘッダー に乗せるいつものやつですね。

パッケージの参照

WebAPI側は次の2つのパッケージを参照します。

dotnet add package IdentityServer4.AccessTokenValidation --version 2.7.0
dotnet add package NSwag.AspNetCore --version 12.0.14

JWTの検証設定

JWT の検証は Startup.cs の ConfigureServices メソッドで下記のように定義されているとします。

services.AddAuthentication("Bearer")
    .AddIdentityServerAuthentication(options =>
    {
        options.Authority = "http://localhost:5000";
        options.RequireHttpsMetadata = false;
        options.ApiName = "test.api";
        options.ApiSecret = "test.api.secret";
    });

NSwagの設定

NSwag も JWT の検証と同じように Startup.cs の ConfigureServices メソッドで下記のように設定します。
注意点としては、Scope に WebApi のリソース名を入れてあげるのと、クライアントで指定したレスポンスタイプに応じた Flow を設定することですかね。

services.AddOpenApiDocument(config =>
{
    config.OperationProcessors.Add(new OperationSecurityScopeProcessor("oauth2"));
    config.DocumentProcessors.Add(new SecurityDefinitionAppender("oauth2",
        new SwaggerSecurityScheme
        {
            Type = SwaggerSecuritySchemeType.OAuth2,
            Flow = SwaggerOAuth2Flow.Implicit,
            AuthorizationUrl = $"http://localhost:5000/connect/authorize",
            Scopes = new Dictionary
            {
                { "test.api", "test.api" },
                { "openid", "openid"},
                { "profile", "profile"},
            }
        }));
});

ConfigureServices で設定が終わっているので、Configure メソッドで追加の定義をします。
クライアントで要求したClientIdと同じものをセットするようにします。
微妙に非推奨なI/Fが多くて困りました。

app.UseSwagger(); 
app.UseSwaggerUi3(settings =>
{
    settings.OAuth2Client = new OAuth2ClientSettings
    {
        AppName = "DemoApi",
        ClientId = "test.spa"
    };
});
カテゴリー:ASP.NET

Visual Studioでdockerコンテナのデバック中にError: 。というエラーでデバックが開始できない。

2019年2月20日 コメントを残す

もう少し情報を出してほしいですよね。。。

0>C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\MSBuild\Sdks\Microsoft.Docker.Sdk\build\Microsoft.VisualStudio.Docker.Compose.targets(310,5): Error: 。
トラブルシューティング情報の詳細については、http://aka.ms/DockerToolsTroubleshooting をご覧ください

一応このトラブルシュートに飛ばされます。
ここで挙げられているUserProfile以下のファイルをリネームなどを行ったけれど解決せず。
https://docs.microsoft.com/ja-jp/visualstudio/containers/vs-azure-tools-docker-troubleshooting-docker-errors?view=vs-2017

単品で docker-compose build をすると、正常に終わり、up も問題なくできて動く。
Visual Studioからデバックした時だけの問題っぽい。
今回の docker-compose にはソリューション内で作ったASP.NET Coreのコンテンが5つと、MySQL, Redis用のコンテナの7つからできているんだけれど、どこのコンテナが悪いかわからないので、とりあえずすべてコメントした上で、一つづつコメントを解除していく。

結局、前回話題にした alpineコンテナ をベースにしているDockerファイルが残っていたのが原因でした。

カテゴリー:ASP.NET, Docker

Docker(もしくはdocker-compose) build時に、apk add が失敗する場合は、少し待つかDockerを再起動するか、DockerのDNS設定をFixed 8.8.8.8固定にする。

2019年2月11日 1件のコメント

Docker buildしたら、こんなエラーが出た。

Step 2/17 : RUN apk add –no-cache –virtual .gyp python make g++ && apk –no-cache add avahi-dev
—> Running in ae00cbf46192
fetch http://dl-cdn.alpinelinux.org/alpine/v3.8/main/x86_64/APKINDEX.tar.gz
WARNING: Ignoring http://dl-cdn.alpinelinux.org/alpine/v3.8/main/x86_64/APKINDEX.tar.gz: temporary error (try again later)
fetch http://dl-cdn.alpinelinux.org/alpine/v3.8/community/x86_64/APKINDEX.tar.gz
WARNING: Ignoring http://dl-cdn.alpinelinux.org/alpine/v3.8/community/x86_64/APKINDEX.tar.gz: temporary error (try again later)
ERROR: unsatisfiable constraints:

apk でインストールしようとしているパッケージが見つからないらしい。
試しに chrome で上記のURLからパッケージをダウンロードすると、普通にダウンロードできる。

検索すると docker-alpine の issue が引っかかる。
https://github.com/gliderlabs/docker-alpine/issues/279

ミラーが落ちてるから少し待てとか、DNSのキャッシュがとか話が出てくる。
少し待ってから改めてビルドしても同じ現象になるので、Dockerのサービスを再起動したら先に進んだ。

カテゴリー:Docker