Thursday, December 17, 2015

Building an ASP.NET 5 code-first MVC 6 app with EF7

Microsoft has embarked on a complete re-thinking of the ASP.NET MVC framework from version 5 to 6. Although most of the concepts, tools and approaches are similar, there is certainly lots that one needs to get familiar with if you want to work your way into MVC 6.
In this tutorial, I will build a simple ASP.NET MVC application using a Speaker model using Visual Studio 2015 and the Beta 7 version of the SDK.
At the time of writing, the latest version of the Visual Studio 2015 tooling for ASP.NET is version Beta 7. If you have not done so already, download the latest tooling for Visual Studio 2015 from https://www.microsoft.com/en-us/download/details.aspx?id=48738. Since I have a 64-bit computer, I chose the following two downloads:
image
If you have a 32-bit computer, you would choose DotNetVersionManager-x86.msi. Make sure you install DotNetVersionManager-x64.msi (or DotNetVersionManager-x86.msi) before WebToolsExtensionsVS14.msi. Note that the second download (WebToolsExtensionsVS14.msi) is a much bigger download and lakes much longer (around 30 minutes), so be patient.
It is very possible that some of the code in this post may need to change once the final version of ASP.NET MVC 6 is released. I shall attempt, as much as possible, to keep the code current.
Create a new ASP.NET 5 app in Visual Studio:
  • File >> New >> Project
  • Templates >> Visual C# >> Web >> ASP.NET Web Application
  • Give your application a suitable name. Name it MvcNext if you want the code below to match your environment.
image
  • Click on OK. On the next screen, under ASP.NET 5 Preview Templates, choose Web Application. This gives you a template with “Individual User Accounts” authentication. I also unchecked “Host in the cloud”.
image
  • After you click on OK, your app will get assembled. You will notice a new structure for both your solution and project. Highlights:
    • all configuration files are based on JSON rather than XML
    • The global.json file contains information about the solution, including the SDK version.
    • The project.json file holds information about the installed packages as well as other information about the project.
    • All static files that pertain to your web app are placed in the wwwroot folder. These include your CSS, JavaScript, and images.
    • The config.json file contains any configuration settings such as the database connection string.
  • When you run your application, you will see a different looking home page:
image
  • Like Node.js, ASP.NET 5 is modular and allows you to only use the required components for your web application.
  • Since Packet Manager Console will be used quite often, make sure the package manager is visible at the bottom of Visual Studio by selecting Tools >> NuGet Package Manager >> Package Manager Console:
image
  • Add the following Speaker class to the Models folder:
public class Speaker {
    public int SpeakerId { get; set; }
    [StringLength(40)]
    [Required]
    [Display(Name = "First Name")]
    public string FirstName { get; set; }
    [StringLength(40)]
    [Required]
    [Display(Name = "Last Name")]
    public string LastName { get; set; }
    [StringLength(15)]
    [Display(Name = "Mobile Phone")]
    public string MobilePhone { get; set; }
    [StringLength(50)]
    public string Email { get; set; }
    [StringLength(200)]
    [Display(Name = "Blog URL")]
    public string Blog { get; set; }
    [StringLength(15)]
    [Display(Name = "Twitter Handle")]
    public string Twitter { get; set; }
    [StringLength(40)]
    public string Specialization { get; set; }
    public string Bio { get; set; }
    [StringLength(200)]
    [Display(Name = "URL of Picture")]
    public string PhotoUrl { get; set; }
}


  • Resolve any required namespaces.
  • Also in the Models folder, create the following Entity Framework DbContext class named SpeakerContext:

public class SpeakerContext : DbContext {
    public DbSet<Speaker> Speakers { get; set; }
}


  • Add the following class named DummyData.cs - this class will help seed some initial data into the Speaker database entity:

public static class DummyData {
  public static void Initialize(SpeakerContext context) {
      if (!context.Speakers.Any()) {
          context.Speakers.Add(new Speaker { FirstName = "Richard", LastName = "Stone" });
          context.Speakers.Add(new Speaker { FirstName = "Anthony", LastName = "Lee" });
          context.Speakers.Add(new Speaker { FirstName = "Tommy", LastName = "Douglas" });
          context.Speakers.Add(new Speaker { FirstName = "Charles", LastName = "Brown" });
          context.Speakers.Add(new Speaker { FirstName = "Peter", LastName = "Mason" });

          context.SaveChanges();
      }
  }
}


  • Open the Startup.cs file and find this code in the ConfigureServices() method:.



AddDbContext<ApplicationDbContext>(options =>
                    options.UseSqlServer(Configuration["Data:DefaultConnection:ConnectionString"]));

You will add code to identify the connection string that will be used for our SpeakerContext. After you add the following code just after the above code, make sure you move the ; (semicolon) to its new location:

.AddDbContext<SpeakerContext>(options =>
    options.UseSqlServer(Configuration["Data:DefaultConnection:ConnectionString"]));


  • Recompile your application.

What is DNVM?


DNVM is a version manager command line tool. DNVM allows you to configure your .NET runtime. Use DNVM to specify which version of the .NET Execution Environment you need at the process, user, or machine level.

To list available DNX runtimes:

dnvm list
To download and install the latest stable version of the regular .NET framework:
dnvm install latest

To install the latest 64bit CoreCLR:

dnvm install latest -r coreclr -arch x64

Switch to a Different Runtime for the Current Process

dnvm use 1.0.0-beta6 -r coreclr -arch x64

Upgrade runtime 32-bit runtime:

dnvm upgrade -arch x86 -r clr

If you want to remove older versions of the runtime, go to c:\Users\{your profile}\.dnx\runtimes

image

Simply delete the runtime versions that you do not need.

What is this DNX?


The .NET Execution Environment (DNX) is a software development kit (SDK) and runtime environment that has everything you need to build and run .NET applications for Windows, Mac and Linux. It provides a host process, CLR hosting logic and managed entry point discovery. DNX was built for running cross-platform ASP.NET Web applications, but it can run other types of .NET applications, too, such as cross-platform console apps.

What is DNU?


DNU is a command-line tool which provides a variety of utility to install and manage library packages in our application, and/or to package and publish our own application. Under the hood, DNU uses Nuget for package management and deployment.

Creating the EF7 Code 1’sr Migrations


1) Get the latest version of Entity Framework 7. Type the following command into the Package Manager Console:
Install-Package EntityFramework.SqlServer -Version 7.0.0-beta7 -Pre

We are now ready to add our initial migration. Open a Command Prompt inside of the project folder. I found the quickest way to do this is as follows:


  • Right-click on the project folder and choose “Open Folder In File Explorer” as shown below:

image


  • In File Explorer, select File >> Open Command Prompt. This opens a command prompt in the correct project folder.

To ensure that the correct version of the runtime (Beta 7) is being used in the command line window, enter the following:

dnvm use 1.0.0-beta7
Next we will add a migration specifying the context that we want. Bear in mind that there are two contexts (SpeakerContext & ApplicatioDbContext). Therefore, it is necessary to be explicit about which context we want to use.
dnx ef migrations add MyFirstMigration --context SpeakerContext
We have created a class with some dummy data. Let’s use it. In the Startup.cs file, add the following code to bottom of Configure() method.

using (var serviceScope = app.ApplicationServices
   .GetRequiredService<IServiceScopeFactory>()
   .CreateScope()) {
   
   var context = serviceScope.ServiceProvider.GetService<SpeakerContext>();

   DummyData.Initialize(context);
}
To apply the new migration to the database, run the following:

dnx ef database update --context SpeakerContext
At this stage, your database will have been created but not seeded. Unlike previous version of MVC, the database is not created in the App_Data directory. Instead, its is created inside the database server default data directory. You can view your database by using the SQL Server Object Explorer:
image
Data will be seeded once you run the application.

Creating the Controller


The current tooling for ASP.NET 5 in Visual Studio 2015 does not provide tooling for creating controllers based on a model class (I.E. Scaffolding). This could change once ASP.NET 5 is formally released. Meantime, we will create a controller class manually.


  • Right-click on the Controllers folder and a new Class
  • Name the class SpeakersController
  • Replace the class definition with the following code:
public class SpeakersController : Controller {
  private SpeakerContext _context { get; set; }

  [FromServices]
  public ILogger<SpeakersController> Logger { get; set; }

  public SpeakersController(SpeakerContext context) {
      _context = context;
  }

  public IActionResult Index() {
      return View(_context.Speakers.ToList());
  }

  public ActionResult Create() {
      ViewBag.Items = GetSpeakersListItems();
      return View();
  }

  [HttpPost]
  [ValidateAntiForgeryToken]
  public async Task<ActionResult> Create(Speaker speaker) {
      if (ModelState.IsValid) {
          _context.Speakers.Add(speaker);
          await _context.SaveChangesAsync();
          return RedirectToAction("Index");
      }
      return View(speaker);
  }

  public ActionResult Details(int id) {
      Speaker speaker = _context.Speakers
          .Where(b => b.SpeakerId == id)
          .FirstOrDefault();
      if (speaker == null) {
          Logger.LogInformation("Details: Item not found {0}", id);
          return HttpNotFound();
      }
      return View(speaker);
  }

  private IEnumerable<SelectListItem> GetSpeakersListItems(int selected = -1) {
      var tmp = _context.Speakers.ToList();

      // Create authors list for <select> dropdown
      return tmp
          .OrderBy(s => s.LastName)
          .Select(s => new SelectListItem
          {
              Text = String.Format("{0}, {1}", s.FirstName, s.LastName),
              Value = s.SpeakerId.ToString(),
              Selected = s.SpeakerId == selected
          });
  }

  public async Task<ActionResult> Edit(int id) {
      Speaker speaker = await FindSpeakerAsync(id);
      if (speaker == null) {
          Logger.LogInformation("Edit: Item not found {0}", id);
          return HttpNotFound();
      }

      ViewBag.Items = GetSpeakersListItems(speaker.SpeakerId);
      return View(speaker);
  }

  [HttpPost]
  [ValidateAntiForgeryToken]
  public async Task<ActionResult> Edit(int id, Speaker speaker) {
      try {
          speaker.SpeakerId = id;
          _context.Speakers.Attach(speaker);
          _context.Entry(speaker).State = EntityState.Modified;
          await _context.SaveChangesAsync();
          return RedirectToAction("Index");
      } catch (Exception) {
          ModelState.AddModelError(string.Empty, "Unable to save changes.");
      }
      return View(speaker);
  }

  private Task<Speaker> FindSpeakerAsync(int id) {
      return _context.Speakers.SingleOrDefaultAsync(s => s.SpeakerId == id);
  }

  [HttpGet]
  [ActionName("Delete")]
  public async Task<ActionResult> ConfirmDelete(int id, bool? retry) {
      Speaker speaker = await FindSpeakerAsync(id);
      if (speaker == null) {
          Logger.LogInformation("Delete: Item not found {0}", id);
          return HttpNotFound();
      }
      ViewBag.Retry = retry ?? false;
      return View(speaker);
  }

  [HttpPost]
  [ValidateAntiForgeryToken]
  public async Task<ActionResult> Delete(int id) {
      try {
          Speaker speaker = await FindSpeakerAsync(id);
          _context.Speakers.Remove(speaker);
          await _context.SaveChangesAsync();
      } catch (Exception ex) {
          return RedirectToAction("Delete", new { id = id, retry = true });
      }
      return RedirectToAction("Index");
  }
}

Adding The Views


We will start by creating the Index.cshtml view for our Index() action method.


  • Right-click on the Views folder and select Add >> New Folder
  • Enter Speakers as the name of the folder
  • Right-click on the Speakers folder and select Add >> New Item…
  • From the left menu select Installed >> Server-Side
  • Select the MVC View Page item template
  • Enter Index.cshtml as the name and click OK
  • Replace the contents of the Index.cshtml file with the following code:

Index.cshtml

@model IEnumerable<MvcNext.Models.Speaker>

@{
    ViewBag.Title = "Speakers";
}
<p><a asp-action="Create">Create New Speaker</a></p>

<table class="table">
    <tr>
        <th>@Html.DisplayNameFor(model => model.FirstName)</th>
        <th>@Html.DisplayNameFor(model => model.LastName)</th>
        <th></th>
    </tr>
    @foreach (var item in Model) {
        <tr>
            <td>@Html.DisplayFor(modelItem => item.FirstName)</td>
            <td>@Html.DisplayFor(modelItem => item.LastName)</td>
            <td>
                <a asp-action="Edit" asp-route-id="@item.SpeakerId">Edit</a> |
                <a asp-action="Details" asp-route-id="@item.SpeakerId">Details</a> |
                <a asp-action="Delete" asp-route-id="@item.SpeakerId">Delete</a>
            </td>
        </tr>
    }
</table>

Create.cshtml

@model MvcNext.Models.Speaker

<div>
    <form asp-controller="Speaker" asp-action="Create" method="post">
        <div asp-validation-summary="ValidationSummary.ModelOnly" class="text-danger"></div>
        <div class="form-group">
            <label asp-for="FirstName"></label>
            <input asp-for="FirstName" class="form-control" placeholder="First Name" />
            <span asp-validation-for="FirstName" class="text-danger"></span>
        </div>
        <div class="form-group">
            <label asp-for="LastName"></label>
            <input asp-for="LastName" class="form-control" placeholder="Last Name" />
            <span asp-validation-for="LastName" class="text-danger"></span>
        </div>
        <div class="form-group">
            <label asp-for="MobilePhone"></label>
            <input asp-for="MobilePhone" class="form-control" placeholder="Mobile Phone Number" />
            <span asp-validation-for="MobilePhone" class="text-danger"></span>
        </div>
        <div class="form-group">
            <label asp-for="Email"></label>
            <input asp-for="Email" class="form-control" placeholder="Email" />
            <span asp-validation-for="Email" class="text-danger"></span>
        </div>
        <input type="submit" class="btn btn-default" value="Create" />
    </form>
</div>

@section Scripts {
    <script src="~/lib/jquery-validation/jquery.validate.js"></script>
    <script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>
}

Delete.cshtml

@model MvcNext.Models.Speaker

@{
    ViewBag.Title = "Confirm Delete";
}

<h3>Are you sure you want to delete this?</h3>

@if (ViewBag.Retry) {
    <p class="alert alert-danger">Error deleting. Retry?</p>
}

<div>
    <dl class="dl-horizontal">
        <dt>@Html.DisplayNameFor(model => model.FirstName)</dt>
        <dd>@Html.DisplayFor(model => model.FirstName)</dd>

        <dt>@Html.DisplayNameFor(model => model.LastName)</dt>
        <dd>@Html.DisplayFor(model => model.LastName)</dd>
    </dl>

    <div>
        <form asp-controller="Speaker" asp-action="Delete" method="post">
            <div class="form-group">
                <input type="submit" class="btn btn-default" value="Delete" />
            </div>
        </form>

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

Details.cshtml

@model MvcNext.Models.Speaker

@{
    ViewBag.Title = "Details";
}

<h2>Details</h2>
<div>
    <dl class="dl-horizontal">
        <dt>@Html.DisplayNameFor(model => model.FirstName)</dt>
        <dd>@Html.DisplayFor(model => model.FirstName)</dd>

        <dt>@Html.DisplayNameFor(model => model.MobilePhone)</dt>
        <dd>@Html.DisplayFor(model => model.MobilePhone)</dd>

        <dt>@Html.DisplayNameFor(model => model.Email)</dt>
        <dd>@Html.DisplayFor(model => model.Email)</dd>

        <dt>@Html.DisplayNameFor(model => model.Blog)</dt>
        <dd>@Html.DisplayFor(model => model.Blog)</dd>

        <dt>@Html.DisplayNameFor(model => model.Twitter)</dt>
        <dd>@Html.DisplayFor(model => model.Twitter)</dd>

        <dt>@Html.DisplayNameFor(model => model.Specialization)</dt>
        <dd>@Html.DisplayFor(model => model.Specialization)</dd>

        <dt>@Html.DisplayNameFor(model => model.Bio)</dt>
        <dd>@Html.DisplayFor(model => model.Bio)</dd>

        <dt>@Html.DisplayNameFor(model => model.PhotoUrl)</dt>
        <dd>@Html.DisplayFor(model => model.PhotoUrl)</dd>
    </dl>
</div>
<p>
    <a asp-action="Edit" asp-route-id="@Model.SpeakerId">Edit</a> |
    <a asp-action="Index">Back to List</a>
</p> 

Edit.cshtml

@model MvcNext.Models.Speaker

<div>
    <form asp-controller="Speaker" asp-action="Update" method="post" asp-route-id="@Model.SpeakerId">
        <div asp-validation-summary="ValidationSummary.ModelOnly" class="text-danger"></div>
        <div class="form-group">
            <select asp-for="SpeakerId" asp-items="@ViewBag.Items"></select>
        </div>
        <div class="form-group">
            <label asp-for="FirstName"></label>
            <input asp-for="FirstName" class="form-control" />
            <span asp-validation-for="FirstName" class="text-danger"></span>
        </div>
        <div class="form-group">
            <label asp-for="LastName"></label>
            <input asp-for="LastName" class="form-control" />
            <span asp-validation-for="LastName" class="text-danger"></span>
        </div>
        <div class="form-group">
            <label asp-for="MobilePhone"></label>
            <input asp-for="MobilePhone" class="form-control" />
            <span asp-validation-for="MobilePhone" class="text-danger"></span>
        </div>
        <div class="form-group">
            <label asp-for="Email"></label>
            <input asp-for="Email" class="form-control" />
            <span asp-validation-for="Email" class="text-danger"></span>
        </div>
        <input type="submit" class="btn btn-default" value="Save" />
    </form>
</div>

@section Scripts {
    <script src="~/lib/jquery-validation/jquery.validate.js"></script>
    <script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>
}

Adding a Speakers link to the main page


Add the following link to the navigation in the _Layout.cshtml file located in the Views >> Shared folder:
<li><a asp-controller="Speakers" asp-action="Index">Speakers</a></li>

Let’s try it out


You can now run the application.

image

image