vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

ASP.NET Core のプロジェクトに EntityFrameworkCore で Model を追加してみた話

はじめに

ようやく ASP.NET Core MVC の Model の話です。

Empty で ASP.NET Core のプロジェクトを作り、 Controller クラスだけを作った状態で、下記チュートリアルを参考に Model を追加してみることにします。

スキャフォールド

今回は ASP.NET Core のプロジェクトを Empty のテンプレートで生成し、そこに追加することにしたいと思います。

チュートリアルにのっとり、 Models > Movie というクラスを作ってみます。

Movies.cs

using System;

namespace WebApplication1.Models {
    public class Movie {
        public int ID { get; set; }
        public string Title { get; set; }
        public DateTime ReleaseDate { get; set; }
        public string Genre { get; set; }
        public decimal Price { get; set; }
    }
}

DbContext クラスを追加します。

DB の設定などを行うためのクラスですが、ここではほぼ空の状態で作成しておきます。

using Microsoft.EntityFrameworkCore;

namespace EfCoreSample.Models
{
    public class EfCoreSampleContext : DbContext
    {
        public EfCoreSampleContext (DbContextOptions options)
            : base(options)
        {
        }
        public DbSet Movie { get; set; }        
    }
}

このチュートリアルでは、既存の DB を使うのではなくコードから生成します。

その名前は appsettings.json で指定しています。

appsettings.json

{
~省略~
  "ConnectionStrings": {
    "MovieContext": "Data Source=MvcMovie.db",
  }
}

必要なパッケージを NuGet でインストールします(どれもバージョンは 2.2.2 にしました)。

Startup クラスで先ほど作成した DBContext を追加します。

Startup.cs

using EfCoreSample.Models;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace EfCoreSample
{
    public class Startup
    {

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

        public void ConfigureServices(IServiceCollection services)
        {
            services.Configure< CookiePolicyOptions>(options =>
            {
                options.CheckConsentNeeded = context => true;
                options.MinimumSameSitePolicy = SameSiteMode.None;
            });

            services.AddDbContext< EfCoreSampleContext>(options =>
                options.UseSqlite(Configuration.GetConnectionString("MovieContext")));

            services.AddMvc();
        }
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            app.UseMvc();
        }
    }
}

そしてスキャフォールド、…といきたいところですが、 Rider では GUI ではスキャフォールドできず、 EntityFramework のコマンドを実行する必要があるようです。

dotnet aspnet-codegenerator controller -name MoviesController -m Movie -dc EfCoreSampleContext --relativeFo
lderPath Controllers --useDefaultLayout --referenceScriptLibraries

エラー

スキャフォールドに成功すると、 View、Controllerにファイルが追加されます。

が、 Views/Movies/Create.cshtml と Views/Movies/Edit.cshtml でエラーが。。。

Create.cshtml

@model EfCoreSample.Models.Movie

@{
    ViewData["Title"] = "Create";
}

< h1>Create< /h1>

< h4>Movie< /h4>
< hr />
< div class="row">
    < div class="col-md-4">
        < form asp-action="Create">
            < div asp-validation-summary="ModelOnly" class="text-danger">< /div>
            < div class="form-group">
                < label asp-for="Title" class="control-label">< /label>
                < input asp-for="Title" class="form-control" />
                < span asp-validation-for="Title" class="text-danger">< /span>
            < /div>
            < div class="form-group">
                < label asp-for="ReleaseDate" class="control-label">< /label>
                < input asp-for="ReleaseDate" class="form-control" />
                < span asp-validation-for="ReleaseDate" class="text-danger">< /span>
            < /div>
            < div class="form-group">
                < label asp-for="Genre" class="control-label">< /label>
                < input asp-for="Genre" class="form-control" />
                < span asp-validation-for="Genre" class="text-danger">< /span>
            < /div>
            < div class="form-group">
                < label asp-for="Price" class="control-label">< /label>
                < input asp-for="Price" class="form-control" />
                < span asp-validation-for="Price" class="text-danger">< /span>
            < /div>
            < div class="form-group">
                < input type="submit" value="Create" class="btn btn-primary" />
            < /div>
        < /form>
    < /div>
< /div>

< div>
    < a asp-action="Index">Back to List
< /div>

@{ // ここでエラー }
@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

「Cannot resolve section 'Scripts'」と出ます。

また、エラーの個所をコメントアウトして実行しても、ボタンが動作しません orz

どうしてこうなった/(^o^)\

エラーを何とかする

とりあえずエラーを直します。

やるべきことは、「@section Scripts{}」の削除です。

・・・まじかよ。。。

Create.cshtml

~省略~
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}

これで問題なく動作します。

また、そもそも上記が不要なら削除してしまっても OK です。

ボタンなどが動くようにする

ボタンなどが動作しない原因は、 TagHelper が無いためです。

ということで、先頭に addTagHelper を追加します。

Create.cshtml

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
~省略~

TagHelper については他の画面も同様であるため、同じように追加します。

Visual Studio でスキャフォールドしたときも同じような状態になるのでしょうか。

それともみんな使ってなry

ルーティング

あ、あとここまでの設定だと生成された MovieController のルーティングが効かず、真っ白な画面が表示されてしまうため、この辺で試したように 設定しておきます。

MovieController.cs

using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using EfCoreSample.Models;

namespace EfCoreSample.Controllers
{
    public class MoviesController : Controller
    {
        private readonly EfCoreSampleContext _context;

        public MoviesController(EfCoreSampleContext context)
        {
            _context = context;
        }

        [Route("/")]
        [Route("/Movies")]
        // GET: Movies
        public async Task Index()
        {
            return View(await _context.Movie.ToListAsync());
        }
        [Route("/Movies/Details/{*id}")]
        public async Task Details(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var movie = await _context.Movie
                .FirstOrDefaultAsync(m => m.MovieId == id);
            if (movie == null)
            {
                return NotFound();
            }

            return View(movie);
        }
        [Route("/Movies/Create")]
        public IActionResult Create()
        {
            return View("/Views/Movies/Create.cshtml");
        }
        [HttpPost]
        [Route("/Movies/Create")]
        [ValidateAntiForgeryToken]
        public async Task Create([Bind("MovieId,Title,ReleaseDate,Genre,Price")] Movie movie)
        {
            if (ModelState.IsValid)
            {
                _context.Add(movie);
                await _context.SaveChangesAsync();
                return RedirectToAction(nameof(Index));
            }
            return View(movie);
        }
        [Route("/Movies/Edit/{*id}")]
        public async Task Edit(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var movie = await _context.Movie.FindAsync(id);
            if (movie == null)
            {
                return NotFound();
            }
            return View(movie);
        }
        [HttpPost]
        [Route("/Movies/Edit/{*id}")]
        [ValidateAntiForgeryToken]
        public async Task Edit(int id, [Bind("MovieId,Title,ReleaseDate,Genre,Price")] Movie movie)
        {
            if (id != movie.MovieId)
            {
                return NotFound();
            }

            if (ModelState.IsValid)
            {
                try
                {
                    _context.Update(movie);
                    await _context.SaveChangesAsync();
                }
                catch (DbUpdateConcurrencyException)
                {
                    if (!MovieExists(movie.MovieId))
                    {
                        return NotFound();
                    }
                    else
                    {
                        throw;
                    }
                }
                return RedirectToAction(nameof(Index));
            }
            return View(movie);
        }
        [Route("/Movies/Delete/{*id}")]
        public async Task Delete(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var movie = await _context.Movie
                .FirstOrDefaultAsync(m => m.MovieId == id);
            if (movie == null)
            {
                return NotFound();
            }

            return View(movie);
        }
        [HttpPost, ActionName("Delete")]
        [Route("/Movies/Delete/{*id}")]
        [ValidateAntiForgeryToken]
        public async Task DeleteConfirmed(int id)
        {
            var movie = await _context.Movie.FindAsync(id);
            _context.Movie.Remove(movie);
            await _context.SaveChangesAsync();
            return RedirectToAction(nameof(Index));
        }
        private bool MovieExists(int id)
        {
            return _context.Movie.Any(e => e.MovieId == id);
        }
    }
}

DB のマイグレーション

CSS などが無いため非常に簡素ではあるものの、 CRUD ができそうなページが出来上がりました。

が、そのまま実行すると DB は生成されますが、テーブルが見つからないとエラーになります。

DB にテーブルを追加するには、マイグレーションを実行します。

dotnet ef migrations add AddMovie
dotnet ef database update

CUIGUI より最初とっつきにくい印象があるものの覚えた後は楽なので(個人の感想です)、どちらで慣れるのが良いのかは決めづらいどころですが。

これでとりあえず CRUD ができるようになりました。

次回はもう少し生成されたものの中身を見てみたいと思います。

参照