『かぐや様は告らせたい(10)』を読んだ

かぐや様はお可愛いが、 ヒロインの中では早坂を結構気に入っているので、 早坂の出番が多くて楽しめた。

主従であり姉妹のようでもあるかぐやと早坂の関係は良いね。

柏木神が彼氏とイチャイチャするたびに涙目になっていたキャラが10巻でようやく登場。ここまで引っ張るとは。

四条眞妃。まさか、かぐやの遠縁だったなんてね。 同じ血筋なだけあって、かぐやに似ているところもあり、なかなか良いキャラだと思う。 生徒会メンバーとの絡みが今後増えそうで期待。

一方で柏木神はこんなめんどくさいキャラになっちゃったか。 いや、時折その片鱗を見せていた気もするが。

眞妃を加えた三角関係になるんだろうか。二人とも家の力を使うことに抵抗なさそうなので、両家の抗争が勃発? このマンガでそんな展開はまず無いが、 それはそれで見てみたい気もする。

かぐやのケータイが壊れてスマホに変える話で、落ち込むかぐやに対していつも通り接することにした生徒会メンバーには、おいおいそれでいいのかと一瞬思ったけど、それで充分だったという話のもっていき方は上手い。

読んでいて心がじんわり温かくなる話だった。

かしいかえん

休日に家族で『かしいかえん』に行った。 子供を連れての遊園地は初めて。 かしいかえんに来たのは小学生のとき以来だから20年ぶりくらいだろうか。

昔は『かしいかえん』ではなく『香椎花園』だった気がする。当時から花と緑は多かったんだろうけど、これほどだったっけ、と思うくらい緑が多かった。ちょっと前にリニューアルしたから、そのおかげかも。

特に一面の芝生。レジャーシートや小さいテントを広げてる家族が結構いた。晴れだったし、自分もレジャーシート持って来ればよかったな。それとも、そろそろテントを買うべきか。

シルバニアガーデンの雰囲気づくりなんだろうか。実際、シルバニアガーデンのエリアは遊園地らしからぬのどかな感じで良かった。子供もふかふかな芝生の上を走りまわって楽しそうだった。

アトラクションは年期の入ったものが結構あった。新しいものは身体を動かす系で、まだうちの子供には早かったみたい。アトラクションのほとんどは保護者同伴必須だったし。もう少し大きくなってからまた来よう。

www.kashiikaen.com

Entity Framework Core で値オブジェクトを実装する

Entity Framework Core 2.0 の頃から既に値オブジェクトを実装する資料は存在した。

docs.microsoft.com

ただ、自分がやりたかったのは「エンティティの ID を値オブジェクトにすること」だったので、 ちょっと違う。 Entity Framework Core 2.1 から追加された「値の変換」機能を使えば、 やりたかったことが実現できるかもと思ったので試してみた。

using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace ValueObjectSample
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                MainAsync(args).GetAwaiter().GetResult();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
            Console.WriteLine("Press enter key");
            Console.ReadLine();
        }

        static async Task MainAsync(string[] args)
        {
            using (var context = new ApplicationDbContext())
            {
                await context.Database.EnsureDeletedAsync();
                await context.Database.EnsureCreatedAsync();
            }

            using (var context = new ApplicationDbContext())
            {
                var author = new Author()
                {
                    Name = "荒川 弘"
                };
                context.Authors.Add(author);

                context.Books.Add(new Book()
                {
                    Title = "銀の匙 Silver Spoon",
                    AuthorId = author.Id,
                });
                context.Books.Add(new Book()
                {
                    Title = "アルスラーン戦記",
                    AuthorId = author.Id,
                });

                await context.SaveChangesAsync();
            }

            using (var context = new ApplicationDbContext())
            {
                var books = await context.Books
                    .Include(b => b.Author)
                    .ToListAsync();
                foreach (var book in books)
                {
                    Console.WriteLine($"Id : {book.Id}");
                    Console.WriteLine($"Title : {book.Title}");
                    Console.WriteLine($"AuthorName : {book.Author.Name}");
                    Console.WriteLine();
                }
            }

            using (var context = new ApplicationDbContext())
            {
                var authors = await context.Authors
                    .Include(a => a.Books)
                    .ToListAsync();
                foreach (var author in authors)
                {
                    Console.WriteLine($"Id : {author.Id}");
                    Console.WriteLine($"Name : {author.Name}");
                    for (int i = 0; i < author.Books.Count; i++)
                    {
                        Console.WriteLine($"BookTitle{i} : {author.Books[i].Title}");
                    }
                    Console.WriteLine();
                }
            }
        }
    }

    public class ApplicationDbContext : DbContext
    {
        public DbSet<Author> Authors => Set<Author>();

        public DbSet<Book> Books => Set<Book>();

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(
                "Server=(local);Database=ValueObjectSample;Integrated Security=SSPI;");
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Author>(b =>
            {
                b.Property(e => e.Id)
                    .IsRequired()
                    .HasConversion( // データベースには文字列で保存する
                        id => id.Value,
                        value => AuthorId.Parse(value));
                b.Property(e => e.Name)
                    .IsRequired();

                b.HasKey(e => e.Id);
                b.HasMany(e => e.Books);
            });

            modelBuilder.Entity<Book>(b =>
            {
                b.Property(e => e.Id)
                    .IsRequired()
                    .HasConversion( // データベースには文字列で保存する
                        id => id.Value,
                        value => BookId.Parse(value));
                b.Property(e => e.Title)
                    .IsRequired();
                b.Property(e => e.AuthorId)
                    .IsRequired()
                    .HasConversion( // データベースには文字列で保存する
                        id => id.Value,
                        value => AuthorId.Parse(value));

                b.HasKey(e => e.Id);
                b.HasOne(e => e.Author);
            });
        }
    }

    public class Author
    {
        public AuthorId Id { get; set; } = AuthorId.NewAuthorId();

        public string Name { get; set; }

        public IList<Book> Books { get; set; }
    }

    public class Book
    {
        public BookId Id { get; set; } = BookId.NewBookId();

        public string Title { get; set; }

        public AuthorId AuthorId { get; set; }

        public Author Author { get; set; }
    }

    // Author の ID を ValueObject にする
    public class AuthorId
    {
        AuthorId(string value)
        {
            Value = value;
        }

        public string Value { get; }

        public static AuthorId NewAuthorId() =>
            new AuthorId(Guid.NewGuid().ToString());

        public static AuthorId Parse(string value) =>
            new AuthorId(value);

        public override int GetHashCode() =>
            Value.GetHashCode();

        public override bool Equals(object obj)
        {
            if (obj == null || GetType() != obj.GetType())
                return false;
            if (object.ReferenceEquals(this, obj))
                return true;
            if (obj is AuthorId other)
                return Value.Equals(other.Value);
            return false;
        }

        public override string ToString() =>
            $"AuthorId({Value})";
    }

    // Book の ID を ValueObject にする
    public class BookId
    {
        BookId(string value)
        {
            Value = value;
        }

        public string Value { get; }

        public static BookId NewBookId() =>
            new BookId(Guid.NewGuid().ToString());

        public static BookId Parse(string value) =>
            new BookId(value);

        public override int GetHashCode() =>
            Value.GetHashCode();

        public override bool Equals(object obj)
        {
            if (obj == null || GetType() != obj.GetType())
                return false;
            if (object.ReferenceEquals(this, obj))
                return true;
            if (obj is BookId other)
                return Value.Equals(other.Value);
            return false;
        }

        public override string ToString() =>
            $"BookId({Value})";
    }
}

実行結果がこちら。 ID をデータベースには文字列として保存しつつ、 エンティティで扱うときは値オブジェクト化できている。 リレーションシップも OK。

f:id:griefworker:20180606145237p:plain

ID は上手くいくと思っていたけど、 リレーションシップの方は上手くいくかどうか半信半疑だったので一安心。

JwtBearer Authentication の裏で ASP.NET Core Identity を使う

認証に ASP.NET Core Identity を使うけど、アプリ向けに JwtBearer の認証もサポートしたい。JwtBearer でトークンを発行するときは、ユーザーを認証するコードを書くことになるわけなので、そこで Identity の SignInManager や UserManager を使えばいいかもしれない。早速試してみた。

using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;

namespace IdentityJwtSample
{
    public class Program
    {
        public static void Main(string[] args)
        {
            BuildWebHost(args).Run();
        }

        public static IWebHost BuildWebHost(string[] args)
        {
            // 構成にはメモリ内プロバイダーを使う
            var dict = new Dictionary<string, string>
            {
                ["ConnectionStrings:DefaultConnection"] = "Server=(localdb)\\mssqllocaldb;Database=aspnet-IdentityJwtSample-550CDEDB-AB81-4711-8A92-0988E2505FD1;Trusted_Connection=True;MultipleActiveResultSets=true",
                ["JwtKey"] = "9AC93E2B-8C4F-46C3-B9E5-F7E7CB37A73A",
                ["JwtIssuer"] = "http://yourdomain.com",
            };
            var builder = new ConfigurationBuilder();
            builder.AddInMemoryCollection(dict);

            return WebHost.CreateDefaultBuilder(args)
                .UseStartup<Startup>()
                .UseConfiguration(builder.Build())
                .Build();
        }
    }

    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

            services.AddIdentity<ApplicationUser, IdentityRole>()
                .AddEntityFrameworkStores<ApplicationDbContext>()
                .AddDefaultTokenProviders();

            services.AddAuthentication(options =>
            {
                // JWT Bearer をデフォルトにする
                options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            }).AddJwtBearer(options =>
            {
                options.SaveToken = true;
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidIssuer = Configuration["JwtIssuer"],
                    ValidAudience = Configuration["JwtIssuer"],
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JwtKey"])),
                };
            });

            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ApplicationDbContext dbContext)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseAuthentication();

            app.UseMvc();

            // データベースが無ければ作る
            dbContext.Database.EnsureCreated();
        }
    }

    public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
            : base(options)
        {
        }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);
        }
    }

    // アプリケーション用ユーザー
    public class ApplicationUser : IdentityUser
    {
    }

    // ログインに使うパラメーターを格納
    public class LoginViewModel
    {
        [Required]
        [EmailAddress]
        public string Email { get; set; }

        [Required]
        [DataType(DataType.Password)]
        public string Password { get; set; }
    }

    // ユーザー登録に使うパラメーターを格納
    public class RegisterViewModel
    {
        [Required]
        [EmailAddress]
        [Display(Name = "Email")]
        public string Email { get; set; }

        [Required]
        [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
        [DataType(DataType.Password)]
        [Display(Name = "Password")]
        public string Password { get; set; }

        [DataType(DataType.Password)]
        [Display(Name = "Confirm password")]
        [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
        public string ConfirmPassword { get; set; }
    }

    // 発行したアクセストークンを格納
    public class TokenViewModel
    {
        public string Token { get; set; }
    }

    // API で取得できるユーザー情報を格納
    public class UserViewModel
    {
        public string Id { get; set; }

        public string UserName { get; set; }

        public string Email { get; set; }
    }

    [Route("api/user")]
    public class AccountController : Controller
    {
        private readonly UserManager<ApplicationUser> _userManager;
        private readonly SignInManager<ApplicationUser> _signInManager;
        private readonly IConfiguration _configuration;

        public AccountController(
            UserManager<ApplicationUser> userManager,
            SignInManager<ApplicationUser> signInManager,
            IConfiguration configuration)
        {
            _userManager = userManager;
            _signInManager = signInManager;
            _configuration = configuration;
        }

        // ユーザーを新規登録してアクセストークンを取得
        [HttpPost("register")]
        public async Task<IActionResult> Register([FromBody]RegisterViewModel model)
        {
            if (ModelState.IsValid)
            {
                var user = new ApplicationUser()
                {
                    UserName = model.Email,
                    Email = model.Email
                };
                var result = await _userManager.CreateAsync(user, model.Password);
                if (result.Succeeded)
                {
                    await _signInManager.SignInAsync(user, isPersistent: false);

                    var token = await GenerateJwtTokenAsync(user);
                    return Ok(token);
                }
                AddErrors(result);
            }

            return BadRequest(ModelState);
        }

        // メールアドレスとパスワードでログインしてアクセストークンを取得
        [HttpPost("login")]
        public async Task<IActionResult> Login([FromBody]LoginViewModel model)
        {
            if (ModelState.IsValid)
            {
                var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, false, lockoutOnFailure: false);
                if (result.Succeeded)
                {
                    var user = await _userManager.FindByEmailAsync(model.Email);
                    var token = await GenerateJwtTokenAsync(user);
                    return Ok(token);
                }
            }

            return BadRequest(ModelState);
        }

        // アクセストークンを生成
        async Task<TokenViewModel> GenerateJwtTokenAsync(ApplicationUser user)
        {
            var principal = await _signInManager.CreateUserPrincipalAsync(user);

            var claims = new List<Claim>(principal.Claims);
            claims.Add(new Claim(JwtRegisteredClaimNames.Sub, user.UserName));
            claims.Add(new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()));

            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JwtKey"]));
            var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
            var expires = DateTime.Now.AddDays(7);

            var token = new JwtSecurityToken(
                _configuration["JwtIssuer"],
                _configuration["JwtIssuer"],
                claims,
                expires: expires,
                signingCredentials: credentials
            );

            var jwt = new JwtSecurityTokenHandler().WriteToken(token);
            return new TokenViewModel()
            {
                Token = jwt
            };
        }

        // ログインユーザーの情報を取得
        [HttpGet("current")]
        [Authorize]
        public async Task<IActionResult> Current()
        {
            var user = await _userManager.GetUserAsync(User);
            var viewModel = new UserViewModel()
            {
                Id = user.Id,
                UserName = user.UserName,
                Email = user.Email,
            };
            return Ok(viewModel);
        }

        private void AddErrors(IdentityResult result)
        {
            foreach (var error in result.Errors)
            {
                ModelState.AddModelError(string.Empty, error.Description);
            }
        }
    }
}

curl で動作確認してみる。 まずはユーザー登録。

f:id:griefworker:20180604133613p:plain

ユーザーの登録に成功し、アクセストークンが返ってきた。

次はログイン。

f:id:griefworker:20180604133632p:plain

ログインに成功すればアクセストークンが返ってくる。

そのアクセストークンを使って、認証必須の API を呼び出せるかテスト。

f:id:griefworker:20180604133655p:plain

ログインユーザーの情報が返ってきた。テスト成功。

『アオアシ(13)』を読んだ

プレミアリーグ柏大商業戦で葦人がベンチ入りできたと思ったら、アクシデントによりまさかの途中出場とは。 まだプレミアリーグのレベルにないので、相手から集中的に狙われてしまうわけだが。

このまま無惨に散るのかと思いきや、 ユースに入ったばかりの頃と比べて成長していたので、同じ轍は踏まなかった。 実力不足を認め、できないならどうするか、考えることをやめない葦人の姿勢には学びがある。

あと、敵対していた阿久津と協力して(?)守備するところは見どころ。 監督の手前仕方なくって感じで、 認められたわけではないけど。 阿久津の指示の意図を理解して動いたところと、最後のカウンターにはシビレた。

福岡アンパンマンこどもミュージアムinモール

f:id:griefworker:20180601214052p:plain

子供の誕生日に有休とって『福岡アンパンマンこどもミュージアムinモール』に行ってきた。平日だからか、予想よりもかなり人が少なくて、ステージはすべて最前列で見ることができた。凄く混雑した写真を見ていてビビってたけど、あれは休日だからだろうな。

子供はステージの他には砂場がたいそう気に入ったみたいで、滞在時間の大半を砂場で費やした。手にほとんど粒がつかないほどサラサラで、夢中になるのも分かる。大人でも少し童心に返ってしまった。 ただ、砂場とステージ以外はそれほど遊べるものがなくて、他のミュージアム&モールも同じくらいの内容なんだろうか。期待値が高すぎたのかもしれない。

www.fukuoka-anpanman.jp

タカサキハンバーグ

飯倉から七隈方面に向かう福大通沿いに、気になる店構えのハンバーグ屋があった。名前は『タカサキハンバーグ』。 食べログや Retty によると、かなりの人気店らしい。行列覚悟で行ってみたら案の定、一時間待ちだった。

席に案内されたころには 14 時を過ぎていた。もう空腹の絶頂。今ならハンバーグ 2 個ぐらい余裕で食べれそうだったので、挑戦してみた。

サラダはドレッシングが美味。評判が良いのか、ドレッシングの販売もしているようだった。

スープと味噌汁のどちらか選べたので、味噌汁を選択。ご飯は1回だけおかわりできる。

そしてお待ちかねのハンバーグ。 1個が結構大きい。運ばれてきたときは、2個完食できるか不安になった。 箸を入れると閉じ込められていた肉汁が溢れ出てくる。ふっくらとしていてジューシーだった。 ソースはジンジャーソース。 オニオンとはまた違った辛味と爽やかな後味は新鮮で、お店イチオシなのも納得。

空腹 MAX だったのもあって、無事に完食できた。ご飯のおかわりまでしてしまった。一時間待った甲斐はあったと思う。自分の中のハンバーグランキングトップ5には入るな。