asp.net core 2.1 プロジェクトで docker build すると、CS2017が発生してビルドが失敗する

こんなエラーが出ました。

CSC : error CS2017: Cannot specify /main if building a module or library

検索したらこんなのが
ASP.NET Core 2.1 gives error CS2017: Cannot specify /main if building a module or library
https://github.com/aspnet/websdk/issues/350

指示通り、RazorCompileToolsetを追加したらビルドが通った。

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>netcoreapp2.1</TargetFramework>
    <StartupObject>MockMvc.Program</StartupObject>
    <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
    <RazorCompileToolset>PrecompilationTool</RazorCompileToolset>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.App" />
    <PackageReference Include="Microsoft.AspNetCore.Razor.Design" Version="2.1.2" PrivateAssets="All" />
    <PackageReference Include="Microsoft.Extensions.Http.Polly" Version="2.1.1" />
    <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.0.2105168" />
    <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.1.7" />
    <PackageReference Include="Refit" Version="4.6.58" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\AuthServer.IdentityServer\AuthServer.IdentityServer.csproj" />
  </ItemGroup>

</Project>

 

広告
カテゴリー:ASP.NET, Docker

C#でAWS Lambda

前回StepFunctionsは触ったけれど、素のLambdaってやっていないなということで、今更ながら素のLambdaも触ってみます。

プロジェクトの作成

とりあえず、AWS Toolkit for Visual Studioをインストールしていない人はインストールした後、新しいプロジェクトの作成から、AWS Lambda > AWS Lambda Project with Tests(.NET Core) を選択してOKをクリックします。
2019-01-09-01

Blue Printで、Empty Functionを選択して、Finishをクリックします。
2019-01-09-02

何も考えないでF5からデバック実行すると、ローカルでC#のLambdaを実行するツール(The AWS .NET Mock Lambda Test Tool)が起動するので、
2019-01-09-03
Function Input:に ”test” とかいれて、Execute Function ボタンをクリックすると、Response: に実行結果が表示されます。
2019-01-09-04

最初の関数

素の状態のFunction.csはこんな感じになっています。

namespace AWSLambda2
{
    public class Function
    {
        public string FunctionHandler(string input, ILambdaContext context)
        {
            return input?..ToUpper();
        }
    }
}

単なるstringを受けて、stringを返すサービスも無いので、適当に型をつけてみます。

namespace AWSLambda2
{
    public class FunctionRequest
    {
        public string Keyword { get; set; }
    }

    public class FunctionResponse
    {
        public string Result { get; set; }
    }

    public class Function
    {

        /// <summary>
        /// A simple function that takes a string and does a ToUpper
        /// </summary>
        public FunctionResponse FunctionHandler(FunctionRequest input, ILambdaContext context)
        {
            return new FunctionResponse
            {
                Result = input?.Keyword?.ToUpper()
            };
        }
    }
}

RequestとResponseに型が付きました。
2019-01-09-05

ログを追加

ログを出したいのでNugetから下のライブラリを追加します。今回もserilogです。

  • Microsoft.Extensions.Configuration.FileExtensions
  • Microsoft.Extensions.Configuration.Json
  • Microsoft.Extensions.Configuration.EnvironmentVariables
  • Serilog.Extensions.Logging
  • Serilog.Settings.Configuration
  • Serilog.Sinks.Console

追加した後のソリューションエクスプローラーの依存表示はこんな感じ
2019-01-09-06

FunctionHandlerでserilogの設定をしていきます。

        public FunctionResponse FunctionHandler(FunctionRequest input, ILambdaContext context)
        {
            var configuration = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json")
                .AddEnvironmentVariables()
                .Build();

            Log.Logger = new LoggerConfiguration()
                .ReadFrom.Configuration(configuration)
                .WriteTo.Console(
                    outputTemplate: "[{Timestamp:HH:mm:ss.fff} {Level:u3}] {Message:lj}{NewLine}{Exception}"
                )
                .CreateLogger();

            Log.Logger.Information("hoge");

            return new FunctionResponse
            {
                Result = input?.Keyword?.ToUpper()
            };
        }

ログの出力設定は appsettings.json で行っているのでソリューションエクスプローラーに追加します。

{
  "Logging": {
    "LogLevel": {
      "Default": "Debug",
      "System": "Information",
      "Microsoft": "Information"
    }
  }
}

設定ファイルを追加したら、プロパティーウインドウから 出力ディレクトリにコピー の項目を 新しい場合はコピー にしないと読んでくれないので注意です。
2019-01-09-07

実行するとLog Output:にログが出力されているのがわかります。
2019-01-09-08

データベースに接続して検索する

データベースはMySqlです。
Providerは標準のMySql.Dataでもいいけれど、今回はPomelo.EntityFrameworkCore.MySqlにします。あとでmigrationもするのでMicrosoft.EntityFrameworkCore.Designも合わせて入れておきましょう。

適当にEntityとContextを作ります。

namespace AWSLambda2.Entities
{
    public class User
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }

    public class MyDbContext: DbContext
    {
        public const string DefaultConnectionStringName = nameof(MyDbContext);

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

        public DbSet Users { get; set; }
    }
    public class DesignTimeMyDbContext : IDesignTimeDbContextFactory
    {
        public MyDbContext CreateDbContext(string[] args)
        {
            var configuration = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("connectionStrings.json")
                .Build();
            var builder = new DbContextOptionsBuilder()
                .UseMySql(configuration.GetConnectionString(MyDbContext.DefaultConnectionStringName)
            );
            return new MyDbContext(builder.Options);
        }
    }
}

検索用のサービスを作ります。DbContextはDIでもらう前提です。

    public class UserService
    {
        private readonly MyDbContext _dbContext;

        public UserService(MyDbContext dbContext)
        {
            _dbContext = dbContext;
        }

        public string FindUser(string userName)
        {
            return _dbContext.Users
                .Where(u => u.Name.Contains(userName))
                .Select(u => u.Name)
                .FirstOrDefault();
        }
    }

ソリューションにconnectionStrings.json を追加して、これまたプロパティーウインドウから 出力ディレクトリにコピー の項目を 新しい場合はコピー にします。

{
  "ConnectionStrings": {
    "MyDbContext": "Server=localhost;Port=3306;Uid=user;Pwd=P@ssw0rd;Database=testdb;Convert Zero Datetime=True"
  }
}

FunctionHandlerを修正してDBへの接続情報の設定(①)と、コンテナへのサービスの登録(②)、及び実行(③)を追加します。

        public FunctionResponse FunctionHandler(FunctionRequest input, ILambdaContext context)
        {
            var configuration = new ConfigurationBuilder()
            var configuration = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json")
                .AddJsonFile("connectionStrings.json")  // ①
                .AddEnvironmentVariables()
                .Build();

            Log.Logger = new LoggerConfiguration()
                .ReadFrom.Configuration(configuration)
                .WriteTo.Console(
                    outputTemplate: "[{Timestamp:HH:mm:ss.fff} {Level:u3}] {Message:lj}{NewLine}{Exception}"
                )
                .CreateLogger();

            // ②
            var serviceCollection = new ServiceCollection();
            serviceCollection.AddDbContext(options =>
            {
                options.UseMySql(configuration.GetConnectionString(MyDbContext.DefaultConnectionStringName));
            });
            serviceCollection.AddDbContext(options =>
                options.UseMySql(configuration.GetConnectionString(MyDbContext.DefaultConnectionStringName)));
            serviceCollection.AddTransient();
            serviceCollection.AddLogging(builder => builder.AddSerilog(dispose: true));
            var provider = serviceCollection.BuildServiceProvider();

            // ③
            var service = provider.GetService();
            var userName = service.FindUser(input.Keyword);
            Log.Logger.Information("hoge");

            return new FunctionResponse
            {
                Result = userName
            };
        }

EFがどんなSQLを発行しているのかが気になるので、appsettings.json の Logging 設定に Microsoft.EntityFrameworkCore.Database.Command を追加しておきます。

{
  "Logging": {
    "LogLevel": {
      "Default": "Debug",
      "System": "Information",
      "Microsoft": "Information",
      "Microsoft.EntityFrameworkCore.Database.Command": "Debug"
    }
  }
}

コマンドプロンプトなどで、プロジェクトディレクトリに移動して、migrationファイルを作成しデータベースのマイグレーションを実施します。

> dotnet restore
> dotnet ef migrations add init
Done. To undo this action, use 'ef migrations remove'
> dotnet ef database update
Applying migration '20190109080418_init'.
Done.

適当にデータを突っ込んでから、テスト実行すると結果が帰ってくることがわかります。
SQLのクエリログもデていますね。2019-01-09-10

カテゴリー:AWS, 作ってみた

ソリューションファイルとプロジェクトファイルが同じディレクトリにある状態で、Dockerサポート(Dockerfile)を作成すると、docker build時にビルドが失敗する。

Visual StudioのソリューションエクスプローラーからDockerサポートの追加をして、docker build をしたところ、こんなエラーが出ました。
ログの全体はこんな感じ。

> docker build . -t sample
Sending build context to Docker daemon  339.5MB
Step 1/16 : FROM microsoft/dotnet:2.2-aspnetcore-runtime AS base
 ---> df0d46ff6229
Step 2/16 : WORKDIR /app
 ---> Using cache
 ---> e6407778a2d1
Step 3/16 : EXPOSE 80
 ---> Using cache
 ---> a17ec6fc7197
Step 4/16 : FROM microsoft/dotnet:2.2-sdk AS build
 ---> 343e2dc38168
Step 5/16 : WORKDIR /src
 ---> Using cache
 ---> 064e8cb7ff8d
Step 6/16 : COPY signalsample.csproj ./
 ---> 3361ca72b93c
Step 7/16 : RUN dotnet restore /signalsample.csproj
 ---> Running in d5b618d70751
MSBUILD : error MSB1001: Unknown switch.
Switch: /signalsample.csproj

For switch syntax, type "MSBuild /help"
The command '/bin/sh -c dotnet restore /signalsample.csproj' returned a non-zero code: 1

msbuild 時によくわからないスイッチがあると言われる。

For switch syntax, type “MSBuild /help”
The command ‘/bin/sh -c dotnet restore /signalsample.csproj’ returned a non-zero code: 1

dockerfileを見ると、

FROM microsoft/dotnet:2.2-aspnetcore-runtime AS base
WORKDIR /app
EXPOSE 80

FROM microsoft/dotnet:2.2-sdk AS build
WORKDIR /src
COPY signalsample.csproj ./
RUN dotnet restore /signalsample.csproj
COPY . .
WORKDIR /src/
RUN dotnet build signalsample.csproj -c Release -o /app

FROM build AS publish
RUN dotnet publish signalsample.csproj -c Release -o /app

FROM base AS final
WORKDIR /app
COPY --from=publish /app .
ENTRYPOINT ["dotnet", "signalsample.dll"]

8行目の restore でプロジェクトファイルの指定/から始まっていてこいつがスイッチとして認識されているっぽい。

RUN dotnet restore /signalsample.csproj

たぶん、dotnet コマンドからプロジェクトを作ったあとに、同じフォルダーにソリューションフォルダーを作った事が原因だと思われる。(dotnet build や dotnet publish はソリューションフォルダーが無いことを考慮しているのに、dotnet restore は考慮されていのか…)
とりあえず、/を取るか、./とするとdocker buildもdocker-compose buildもうまく通った。

RUN dotnet restore signalsample.csproj
カテゴリー:Visual Studio タグ:

Trelloを使って振り返りしたときに、単にリストごとにカードの一覧を出力するツールを作った

https://github.com/karuakun/TrelloFurikaerier

Trelloで振り返りをしたんだけれど、やっぱりプレーンテキストにエクスポートしたい。 だけれど、無料のTrelloで出力できるのはJSONファイルだけ。 ボードごとのタイトルと、その発言を作成した人の単純なリストをマークダウン形式で出力するためのツールです。

以前作った https://github.com/karuakun/typetalk-cli-dotnet-cli-sample は McMaster.Extensions.CommandLineUtils の使い方と、.Net Core のコンソールアプリの標準形を作るにはどうしたら良いを考えながら作ったので、必要以上に複雑になってしまった。
今回はコマンドライン解析を標準の ConfigurationBuilder にまかせてしまい、機能は何も無いので超シンプル。

カテゴリー:作ってみた

ReSharper で一度にクラスを別々のファイルに移動する

見失って検索したので、自分向けのメモ
https://pleiades.io/help/resharper/Move_classes_into_separate_files_in_one_go.html

こんな感じのサイトでJSONからC#のコードを生成したんだけれど、もちろんファイルが分かれないので一つのファイルにまとまってしまう。
http://json2csharp.com/#

ReShaperの機能でそんなのがあったなーと検索。
⇒感動する

カテゴリー:プログラミング タグ:

C#とAWSのStepFunctionsを試してみたログ

一旦動くところまで作ったけれど、他の言語になりそうなのでとりあえずログとして残しておく。
AWS Toolkit でパッケージすると、LambdaとStepFunctionsの紐づけがわかりやすくて、AWSにデプロイしたあとにも管理しやすいので良いと思ったんだけれどなー。

C# で AWS Lambdaって記事はよく見るけれど、C# で StepFunctions ってのはあんまり見ない。
検索してみるとこの記事がわかりやすかった。
https://www.red-gate.com/simple-talk/cloud/cloud-development/aws-step-function-serverless-applications/

とりあえずやって見よう

環境を作る

Visual StudioにAWS Toolkitが入っていない場合は、Visual Studioの更新機能と更新プログラムから「AWS Toolkit for Visual Studio 2017」をインストールする。

2018-12-12.0

プロジェクトを作る

新しいプロジェクトに、AWS Lambdaというカテゴリができるので、「AWS Serverless Application (.NET Core)」を選択してプロジェクトを作成する。ここでつけたプロジェクト名がLambdaのハンドラー名になるのでちょっと気にしておきたい(まぁ、あとでserverless.templateをいじれば変えられるけれど)。

下の方に .NET Frameworkのバージョンを選択するところがあるけれど、AWS Lambdaでは .net core しか使えないので、ここのドロップダウンはどこにも使われないので無視してOK。

2018-12-12.1.png

続いて、どのBlueprintを元にプロジェクトを作るかを聞かれるので、「Step Functions Hello World」を選択してFinishボタンを押す。

2018-12-12.2.png

プロジェクトの構成を確認する

こんな感じのプロジェクトができるので、軽く各ファイルの役割を説明すると

2018-12-12.3

aws-lambda-tools-defaults.json

デプロイに使う設定、AWSのリージョンやデプロイ資源を置くS3のバケットなど設定などを記述する。ここで指定しないものは後でデプロイのときに指定することもできるので、s3-buketやstack-nameを空にしておいて、デプロイ時に指定するみたいな使い方をするみたい。特に変更はいらないと思うのでまずは気にしなくて良い。

serverless.template

Cloud Formation Template っぽい記法で、Lambda や Role、StepFunction などのAWSの各リソースの作成方法を定義する。CloudFormationに不慣れだと読むのが辛いけれど、とりあえずLambdaを増やしたら要素をコピーして追加するぐらいの認識で良いかも。

State.cs

StepFunctionで持ち回るStateの定義、基本的に各ステートで実行されるタスクは、このStateを受け取り、Stateを返すようなメソッドのインターフェイスになる。StateMacnineの内部でもここのステートを参照できる。

state-machine.json

ステートマシーンの定義、各ステートでどんなメソッドを実行するとか、Stateの状態を見て分岐させるとか、数秒ウエイトするみたいな状態とその実行方法、次のステートに移る方法をJSONで定義する。

StepFunctionTasks.cs

各ステートでどんな処理をするのかをC#のメソッドで記述する。メソッドがそれぞれ独立したLambdaに展開される。各メソッドは、State.csで定義されたステートと、実行しているLambdaのContext情報を受け、Stateを更新して次のLambdaにわたす処理を定義する。

State.cs

StepFunctionで持ち回るStateの定義、基本的に各ステートで実行されるタスクは、このStateを受け取り、Stateを返すようなメソッドのインターフェイスになる。StateMacnineの内部でもここのステートを参照できる。

ここで定義したステート定義は内部的に Newtonsoft.Json でシリアライズ/デシリアライズされ、各ステップで利用できる。例えば C# で enum で定義したプロパティーなどを、StateMacnine 側で比較の材料にしたい場合などは数値として渡っていってしまうので、定義する型には気をつけておきたい。

namespace AWSServerless1
{
    /// <summary>
    /// The state passed between the step function executions.
    /// </summary>
    public class State
    {
        /// <summary>
        /// Input value when starting the execution
        /// </summary>
        public string Name { get; set; }

        /// <summary>
        /// The message built through the step function execution.
        /// </summary>
        public string Message { get; set; }

        /// <summary>
        /// The number of seconds to wait between calling the Salutations task and Greeting task.
        /// </summary>
        public int WaitInSeconds { get; set; }
    }
}

StepFunctionTasks.cs

各ステートでどんな処理をするのかをC#のメソッドで記述する、メソッドがそれぞれ独立したLambdaに展開される。各メソッドは、State.csで定義されたステートと、実行しているLambdaのContext情報を受け、Stateを更新して次のLambdaにわたす処理を定義する。

この子に処理を全部書くと当たり前のことながら、メンテ不可能になるので適切な設計が必要になる。
DIしたい場合は、コンストラクターでコンテナを作って各メソッドでコンテナから処理を取り出してRunする感じになる。

この例の場合は、GreetingメソッドとSalutationsメソッドがそれぞれ独立したLambdaになり、StepFunctionsのStatemacnineから呼び出される。

using Amazon.Lambda.Core;
// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class.
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.Json.JsonSerializer))]

namespace AWSServerless3
{
    public class StepFunctionTasks
    {
        public StepFunctionTasks() { }

        public State Greeting(State state, ILambdaContext context)
        {
            state.Message = "Hello";

            if(!string.IsNullOrEmpty(state.Name))
            {
                state.Message += " " + state.Name;
            }

            // Tell Step Function to wait 5 seconds before calling 
            state.WaitInSeconds = 5;

            return state;
        }

        public State Salutations(State state, ILambdaContext context)
        {
            state.Message += ", Goodbye";

            if (!string.IsNullOrEmpty(state.Name))
            {
                state.Message += " " + state.Name;
            }

            return state;
        }
    }
}

Greetingメソッドでは、ステートを更新して次のステップでWaitする時間を動的に変更するために、StateのWaitInSecondsに5を設定している。

serverless.template

Cloud Formation Template っぽい記法で、Lambda や Role、StepFunction などのAWSの各リソースの作成方法を定義する。CloudFormationに不慣れだと読むのが辛いけれど、とりあえずLambdaを増やしたら要素をコピーして追加するぐらいの認識で良いかも。

構成としてはこんな感じになっている。
2018-12-12.4

GreetingTask や SalutationsTask の部分を展開するとこんな感じになっているので、新しいステップで実行したい関数が増えた場合は、このブロックを追加していけば良い。作成時点でのアセンブリ名や名前空間でHandler名が決定されるので、もし途中でアセンブリ名や名前空間、クラス名などを変えたらこっちも一緒に変えてあげたほうが後々わかりやすそう。

    "GreetingTask" : {
        "Type" : "AWS::Lambda::Function",
        "Properties" : {
            "Handler" : "AWSServerless1::AWSServerless1.StepFunctionTasks::Greeting",
            "Role"    : {"Fn::GetAtt" : [ "LambdaRole", "Arn"]},
            "Runtime" : "dotnetcore2.1",
            "MemorySize" : 256,
            "Timeout" : 30,
            "Code" : {
                "S3Bucket" : "",
                "S3Key" : ""
            }
        }
    },

    "SalutationsTask" : {
        "Type" : "AWS::Lambda::Function",
        "Properties" : {
            "Handler" : "AWSServerless1::AWSServerless1.StepFunctionTasks::Salutations",            
            "Role"    : {"Fn::GetAtt" : [ "LambdaRole", "Arn"]},
            "Runtime" : "dotnetcore2.1",
            "MemorySize" : 256,
            "Timeout" : 30,
            "Code" : {
                "S3Bucket" : "",
                "S3Key" : ""
            }
        }
    },

state-machine.json

ステートマシーンの定義、各ステートでどんなメソッドを実行するとか、Stateの状態を見て分岐させるとか、数秒ウエイトするみたいな状態とその実行方法、次のステートに移る方法をJSONで定義する。

{
  "Comment": "State Machine",
  "StartAt": "Greeting",
  "States": {
    "Greeting": {
      "Type": "Task",
      "Resource": "${GreetingTask.Arn}",
      "Next": "WaitToActivate"
    },
    "WaitToActivate": {
      "Type": "Wait",
      "SecondsPath": "$.WaitInSeconds",
      "Next": "Salutations"
    },
    "Salutations": {
      "Type": "Task",
      "Resource": "${SalutationsTask.Arn}",
      "End": true
    }
  }
}

ここで定義したものは、AWS上のコンソールからみると、こんな感じのステートマシンとして表示される。
2018-12-12.5

Greeting状態やSalutations状態では、TypeをTaskとしてserverless.templateで定義したLambdaを呼び出している。定義時点では各LambdaのArnは決定できていないので、${GreetingTask.Arn}や${SalutationsTask.Arn}のような変数になっている。

$.ステートに定義されたプロパティー名でステートの情報にアクセスできる。WaitInSecondsWaitToActivate状態では、ステートの状態を元にメソッドを数秒待ちたいので$.WaitInSecondsを参照してWaitしている。

この例では、単に上から下に流れるだけのステートしか定義していので、TaskタイプやWaitタイプしか使っていないけれど、StasteMacnine自体はAmazon States Language(https://states-language.net/spec.html)というDSLで定義されているので、Choiceタイプで条件分岐させたり、Parallelタイプで並列実行したりといったタスクを作ることもできる。

Choiceタイプでは、Lambdaから渡ってきたステート情報を元に分岐をかくことができるけれど、データ型に従った比較メソッドが用意されているのでステートの型と合うように分岐を書く必要がある。
https://dev.classmethod.jp/cloud/aws/aws-step-functions-states-choice/

ステートに日本語を使ったら、AWSにデプロイするときにステートが文字化けしてしまったので、英数字で定義するのが無難みたい。

とりあえずサンプルはココまで

カテゴリー:AWS タグ: ,

RiderのCodeVisionはVisual StudioのCodeLensっぽいもの

Rider 2018.3のEAPでCodeVisionというものが有効になるらしい。
Code Vision in Rider: Enriching the Editor with Contextual Information and Navigation(https://blog.jetbrains.com/dotnet/2018/12/10/code-vision-rider-enriching-editor-contextual-information-navigation/)

少し前の JetBrains “.NET” Night Tokyo 2018 @ Lifebear(https://lifebear.connpass.com/event/107559/)でJetBrainsのMaarten BalliauwさんのRiderでCodeLensっぽいものが表示されていて気になったんだけれど、EAPとして公開されたみたい。

Visual StudioのCodeLensに比べ、表示方法をカスタマイズできたり、取得できるメトリクスが多いみたいな違いがあるのかな。
コレって、Rider以外の他のIDEにも展開されるといいなー。

カテゴリー:メモ タグ: