In this tutorial, you’ll create a simple web app using ASP.NET MVC and Entity Framework (EF). The app stores records in a SQL database and supports the basic CRUD operations (create, read, update, delete).
Note
This tutorial uses Visual Studio 2015. If you are completely new to ASP.NET MVC or Visual Studio, read 🔧 Building Your First MVC 6 Application first.
The sample app that you’ll build manages a list of authors and books. Here is a screen shot of the app:
The app uses Razor to generate static HTML. (An alternate approach is to update pages dynamically on the client, with a combination of AJAX calls, JSON data, and client-side JavaScript. This tutorial doesn’t cover that approach.)
In this article:
You can browse the source code for the sample app on GitHub.
Create the project
Start Visual Studio 2015. From the File menu, select New > Project.
Select the ASP.NET Web Application project template. It appears under Installed > Templates >Visual C# > Web. Name the project
ContosoBooks
and click OK.
In the New Project dialog, select Web Site under ASP.NET 5 Preview Templates.
Click Change Authentication and select No Authentication. You won’t need authentication for this sample. Click OK twice to complete the dialogs and create the project.
Open the Views/Shared/_Layout.cshtml file. Replace the following code:
<li><a asp-controller="Home" asp-action="Index">Home</a></li>
<li><a asp-controller="Home" asp-action="About">About</a></li>
<li><a asp-controller="Home" asp-action="Contact">Contact</a></li>
with this:
<li><a asp-controller="Book" asp-action="Index">Books</a></li>
<li><a asp-controller="Author" asp-action="Index">Authors</a></li>
This adds a link to the Books page, which we haven’t created yet. (That will come later in tutorial.)
Add Entity Framework
Open the project.json file. In the dependencies section, add the following line:
"dependencies": {
...
"EntityFramework.SqlServer": "7.0.0-beta5"
},
When you save project.json, Visual Studio automatically resolves the new package reference.
Create entity classes
The app will have two entities:
- Book
- Author
We’ll define a class for each. First, add a new folder to the project. In Solution Explorer, right-click the project. (The project appears under the “src” folder.) Select Add > New Folder. Name the folderModels.
Note
You can put model classes anywhere in your project. The Models folder is just a convention.
Right-click the Models folder and select Add > New Item. In the Add New Item dialog, select theClass template. In the Name edit box, type “Author.cs” and click OK. Replace the boilerplate code with:
using System.ComponentModel.DataAnnotations;
namespace ContosoBooks.Models
{
public class Author
{
public int AuthorID { get; set; }
[Display(Name = "Last Name")]
public string LastName { get; set; }
[Display(Name = "First Name")]
public string FirstMidName { get; set; }
}
}
Repeat these steps to add another class named
Book
with the following code:using System.ComponentModel.DataAnnotations;
namespace ContosoBooks.Models
{
public class Book
{
public int BookID { get; set; }
public string Title { get; set; }
public int Year { get; set; }
public decimal Price { get; set; }
public string Genre { get; set; }
public int AuthorID { get; set; }
// Navigation property
public Author Author { get; set; }
}
}
To keep the app simple, each book has a single author. The
Author
property provides a way to navigate the relationship from a book to an author. In EF, this type of property is called a navigation property. When EF creates the DB schema, EF automatically infers that AuthorID
should be a foreign key to the Authors table.Add a DbContext class
In EF 7, the primary class for interacting with data is
Microsoft.Data.Entity.DbContext
. Add a class in theModels folder named BookContext
that derives from DbContext
:using Microsoft.Data.Entity;
namespace ContosoBooks.Models
{
public class BookContext : DbContext
{
public DbSet<Author> Authors { get; set; }
public DbSet<Book> Books { get; set; }
}
}
The
DbSet
properties represent collections of entities. These will become tables in the SQL database.
Next, we’ll create some sample data. Add a class named
SampleData
in the Models folder with the following code:using Microsoft.Data.Entity;
using Microsoft.Framework.DependencyInjection;
using System;
using System.Linq;
namespace ContosoBooks.Models
{
public static class SampleData
{
public static void Initialize(IServiceProvider serviceProvider)
{
var context = serviceProvider.GetService<BookContext>();
if (context.Database.AsRelational().Exists())
{
if (!context.Books.Any())
{
var austen = context.Authors.Add(
new Author { LastName = "Austen", FirstMidName = "Jane" }).Entity;
var dickens = context.Authors.Add(
new Author { LastName = "Dickens", FirstMidName = "Charles" }).Entity;
var cervantes = context.Authors.Add(
new Author { LastName = "Cervantes", FirstMidName = "Miguel" }).Entity;
context.Books.AddRange(
new Book()
{
Title = "Pride and Prejudice",
Year = 1813,
Author = austen,
Price = 9.99M,
Genre = "Comedy of manners"
},
new Book()
{
Title = "Northanger Abbey",
Year = 1817,
Author = austen,
Price = 12.95M,
Genre = "Gothic parody"
},
new Book()
{
Title = "David Copperfield",
Year = 1850,
Author = dickens,
Price = 15,
Genre = "Bildungsroman"
},
new Book()
{
Title = "Don Quixote",
Year = 1617,
Author = cervantes,
Price = 8.95M,
Genre = "Picaresque"
}
);
context.SaveChanges();
}
}
}
}
}
You wouldn’t put this into production code, but it’s OK for a sample app.
Configure Entity Framework
Open config.json. Add the following highlighted lines:
{
"AppSettings": {
"SiteTitle": "Contoso Books"
},
"Data": {
"ConnectionString": "Server=(localdb)\\MSSQLLocalDB;Database=ContosoBooks;Trusted_Connection=True;MultipleActiveResultSets=true"
}
}
This defines a connection string to LocalDB, which is a lightweight version of SQL Server Express for development.
Open the Startup.cs file. In the
ConfigureServices
method, add:services.AddEntityFramework()
.AddSqlServer()
.AddDbContext<BookContext>(options =>
{
options.UseSqlServer(Configuration.Get("Data:ConnectionString"));
});
Add the following code at the end of the Configure method:
SampleData.Initialize(app.ApplicationServices);
Notice in ConfigureServices that we call
Configuration.Get
to get the database connection string. During development, this setting comes from the config.json file. When you deploy the app to a production environment, you set the connection string in an environment variable on the host. If the Configuration API finds an environment variable with the same key, it returns the environment variable instead of the value that is in config.json.
Here is the complete Startup.cs after these changes:
using ContosoBooks.Models;
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Diagnostics;
using Microsoft.AspNet.Hosting;
using Microsoft.Data.Entity;
using Microsoft.Framework.Configuration;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.Logging;
using Microsoft.Framework.Runtime;
namespace ContosoBooks
{
public class Startup
{
public Startup(IHostingEnvironment env, IApplicationEnvironment appEnv)
{
// Setup configuration sources.
var builder = new ConfigurationBuilder(appEnv.ApplicationBasePath)
.AddJsonFile("config.json")
.AddEnvironmentVariables();
Configuration = builder.Build();
}
public IConfiguration Configuration { get; set; }
// This method gets called by the runtime.
public void ConfigureServices(IServiceCollection services)
{
// Add MVC services to the services container.
services.AddMvc();
services.AddEntityFramework()
.AddSqlServer()
.AddDbContext<BookContext>(options =>
{
options.UseSqlServer(Configuration.Get("Data:ConnectionString"));
});
}
// Configure is called after ConfigureServices is called.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.MinimumLevel = LogLevel.Information;
loggerFactory.AddConsole();
// Configure the HTTP request pipeline.
// Add the following to the request pipeline only in development environment.
if (env.IsDevelopment())
{
app.UseBrowserLink();
app.UseErrorPage(ErrorPageOptions.ShowAll);
}
else
{
// Add Error handling middleware which catches all application specific errors and
// send the request to the following path or controller action.
app.UseErrorHandler("/Home/Error");
}
// Add static files to the request pipeline.
app.UseStaticFiles();
// Add MVC to the request pipeline.
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Book}/{action=Index}/{id?}");
// Uncomment the following line to add a route for porting Web API 2 controllers.
// routes.MapWebApiRoute("DefaultApi", "api/{controller}/{id?}");
});
SampleData.Initialize(app.ApplicationServices);
}
}
}
Use data migrations to create the database
Open project.json. - In the “commands” and “dependencies” sections, add an entry for
EntityFramework.Commands
.{
"webroot": "wwwroot",
"version": "1.0.0-*",
"dependencies": {
"Microsoft.AspNet.Diagnostics": "1.0.0-beta5",
"Microsoft.AspNet.Mvc": "6.0.0-beta5",
"Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-beta5",
"Microsoft.AspNet.Server.IIS": "1.0.0-beta5",
"Microsoft.AspNet.Server.WebListener": "1.0.0-beta5",
"Microsoft.AspNet.StaticFiles": "1.0.0-beta5",
"Microsoft.AspNet.Tooling.Razor": "1.0.0-beta5",
"Microsoft.Framework.Configuration.Json": "1.0.0-beta5",
"Microsoft.Framework.Logging": "1.0.0-beta5",
"Microsoft.Framework.Logging.Console": "1.0.0-beta5",
"Microsoft.VisualStudio.Web.BrowserLink.Loader": "14.0.0-beta5",
"EntityFramework.SqlServer": "7.0.0-beta5",
"EntityFramework.Commands": "7.0.0-beta5"
},
"commands": {
"web": "Microsoft.AspNet.Hosting --config hosting.ini",
"ef": "EntityFramework.Commands"
},
"frameworks": {
"dnx451": { },
"dnxcore50": { }
},
"exclude": [
"wwwroot",
"node_modules",
"bower_components"
],
"publishExclude": [
"node_modules",
"bower_components",
"**.xproj",
"**.user",
"**.vspscc"
],
"scripts": {
"prepublish": [ "npm install", "bower install", "gulp clean", "gulp min" ]
}
}
Build the app.
Open a command prompt in the project directory (ContosoBooks/src/ContosoBooks) and run the following commands:
dnvm use 1.0.0-beta5
dnx ef . migration add Initial
dnx ef . migration apply
The “
add Initial
” command adds code to the project that allows EF to update the database schema. The “apply
” command creates the actual database. After you run the run these commands, your project has a new folder named Migrations:- dnvm : The .NET Version Manager, a set of command line utilities that are used to update and configure .NET Runtime. The command
dnvm use 1.0.0-beta5
instructs the .NET Version Manager to add the 1.0.0-beta5 ASP.NET 5 runtime to thePATH
environment variable for the current shell. For ASP.NET 5 Beta 5, the following is displayed:
Adding C:\\Users\\<user>\\.dnx\\runtimes\\dnx-clr-win-x86.1.0.0-beta5\\bin to process PATH
- dnx ef migration add Initial : DNX is the .NET Execution Environment. The
ef migration apply
command runs pending migration code. For more information aboutdnvm
,dnu
, anddnx
, seeDNX Overview.
Add an index page
In this step, you’ll add code to display a list of books.
Right-click the Controllers folder. Select Add > New Item. Select the MVC Controller Class template. Name the class
BookController
.
Replace the boilerplate code with the following:
using ContosoBooks.Models;
using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.Data.Entity;
using Microsoft.Data.Entity.Storage;
using Microsoft.Framework.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace ContosoBooks.Controllers
{
public class BookController : Controller
{
[FromServices]
public BookContext BookContext { get; set; }
[FromServices]
public ILogger<BookController> Logger { get; set; }
public IActionResult Index()
{
var books = BookContext.Books.Include(b => b.Author);
return View(books);
}
}
}
Notice that we don’t set any value for
Logger
and BookContext
. The dependency injection (DI) subsystem automatically sets these properties at runtime. DI also handles the object lifetimes, so you don’t need to call Dispose
. For more information, see Dependency Injection.
In the Views folder, make a sub-folder named Book. You can do this by right-clicking the Views folder in Solution Explorer and clicking Add New Folder.
Right-click the Views/Book subfolder that you just created, and select Add > New Item. Select theMVC View Page template. Keep the default name, Index.cshtml.
Note
For views, the folder and file name are significant. The view defined in Views/Book/Index.cshtmlcorresponds to the action defined in the
BookController.Index
method.
Replace the boilerplate code with:
@model IEnumerable<ContosoBooks.Models.Book>
@{
ViewBag.Title = "Books";
}
<p>
<a asp-action="Create">Create New Book</a>
</p>
<table class="table">
<tr>
<th>
@Html.DisplayNameFor(model => model.Title)
</th>
<th>
@Html.DisplayNameFor(model => model.Author)
</th>
<th></th>
</tr>
@foreach (var item in Model)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.Author.LastName)
</td>
<td>
<a asp-action="Edit" asp-route-id="@item.BookID">Edit</a> |
<a asp-action="Details" asp-route-id="@item.BookID">Details</a> |
<a asp-action="Delete" asp-route-id="@item.BookID">Delete</a>
</td>
</tr>
}
</table>
Run the app and click the “Books” link in the top nav bar. You should see a list of books. The links for create, edit, details, and delete are not functioning yet. We’ll add those next.
Add a details page
Add the following method to the
BooksController
class:public async Task<ActionResult> Details(int id)
{
Book book = await BookContext.Books
.Include(b => b.Author)
.SingleOrDefaultAsync(b => b.BookID == id);
if (book == null)
{
Logger.LogInformation("Details: Item not found {0}", id);
return HttpNotFound();
}
return View(book);
}
This code looks up a book by ID. In the EF query:
- The
Include
method tells EF to fetch the relatedAuthor
entity. - The
SingleOrDefaultAsync
method returns a single entity, ornull
if one is not found.
If the EF query returns
null
, the controller method returns HttpNotFound
, which ASP.NET translates into a 404 response. Otherwise, the controller passes book to a view, which renders the details page. Let’s add the view now.
In the Views/Book folder, add a view named Details.cshtml with the following code:
@model ContosoBooks.Models.Book
@{
ViewBag.Title = "Details";
}
<h2>Details</h2>
<div>
<dl class="dl-horizontal">
<dt>@Html.DisplayNameFor(model => model.Title)</dt>
<dd>@Html.DisplayFor(model => model.Title)</dd>
<dt>@Html.DisplayNameFor(model => model.Author)</dt>
<dd>
@Html.DisplayFor(model => model.Author.FirstMidName)
@Html.DisplayFor(model => model.Author.LastName)
</dd>
<dt>@Html.DisplayNameFor(model => model.Year)</dt>
<dd>@Html.DisplayFor(model => model.Year)</dd>
<dt>@Html.DisplayNameFor(model => model.Genre)</dt>
<dd>@Html.DisplayFor(model => model.Genre)</dd>
<dt>@Html.DisplayNameFor(model => model.Price)</dt>
<dd>@Html.DisplayFor(model => model.Price)</dd>
</dl>
</div>
<p>
<a asp-action="Edit" asp-route-id="@Model.BookID">Edit</a> |
<a asp-action="Index">Back to List</a>
</p>
Add a create page
Add the following two methods to
BookController
:public ActionResult Create()
{
ViewBag.Items = GetAuthorsListItems();
return View();
}
private IEnumerable<SelectListItem> GetAuthorsListItems(int selected = -1)
{
var tmp = BookContext.Authors.ToList(); // Workaround for https://github.com/aspnet/EntityFramework/issues/2246
// Create authors list for <select> dropdown
return tmp
.OrderBy(author => author.LastName)
.Select(author => new SelectListItem
{
Text = String.Format("{0}, {1}", author.LastName, author.FirstMidName),
Value = author.AuthorID.ToString(),
Selected = author.AuthorID == selected
});
}
Add a view named Views/Book/Create.cshtml.
@model ContosoBooks.Models.Book
<div>
<form asp-controller="Book" asp-action="Create" method="post">
<div asp-validation-summary="ValidationSummary.ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Title"></label>
<input asp-for="Title" class="form-control" placeholder="Title"/>
<span asp-validation-for="Title" class="text-danger"></span>
</div>
<div class="form-group">
<select asp-for="AuthorID" asp-items="@ViewBag.Items"></select>
</div>
<div class="form-group">
<label asp-for="Year"></label>
<input asp-for="Year" class="form-control" placeholder="1900"/>
<span asp-validation-for="Year" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Price"></label>
<input asp-for="Price" class="form-control" placeholder="1.00"/>
<span asp-validation-for="Price" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Genre"></label>
<input asp-for="Genre" class="form-control" placeholder="Genre"/>
<span asp-validation-for="Genre" 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>
}
This view renders an HTML form. In the
form
element, the asp-action
tag helper specifies the controller action to invoke when the client submits the form. Notice that the form uses HTTP POST.<form asp-controller="Book" asp-action="Create" method="post">
Now let’s write the controller action to handle the form post. In the
BookController
class, add the following method.[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Create([Bind("Title", "Year", "Price", "Genre", "AuthorID")] Book book)
{
try
{
if (ModelState.IsValid)
{
BookContext.Books.Add(book);
await BookContext.SaveChangesAsync();
return RedirectToAction("Index");
}
}
catch (DataStoreException)
{
ModelState.AddModelError(string.Empty, "Unable to save changes.");
}
return View(book);
}
The
[HttpPost]
attribute tells MVC that this action applies to HTTP POST requests. The[ValidateAntiForgeryToken]
attribute is a security feature that guards against cross-site request forgery. For more information, see 🔧 Anti-Request Forgery.
Inside this method, we check the model state (
ModelState.IsValid
). If the client submitted a valid model, we add it to the database. Otherwise, we return the original view with validation errors shown:<span asp-validation-for="Title" class="text-danger"></span>
Add validation rules to the book model
To see how validation works, let’s add some validation rules to the
Book
model.- Open Book.cs.
- Add the
[Required]
attribute to theTitle
property. - Add the
[Range]
property to thePrice
property, as shown below.
public class Book
{
public int BookID { get; set; }
[Required]
public string Title { get; set; }
public int Year { get; set; }
[Range(1, 500)]
public decimal Price { get; set; }
public string Genre { get; set; }
public int AuthorID { get; set; }
// Navigation property
public Author Author { get; set; }
}
Run the app. Click Books > Create New Book. Leave Title blank, set Price to zero, and click Create.
Notice how the form automatically adds error messages next to the fields with invalid data. The errors are enforced both client-side (using JavaScript and jQuery) and server-side (using
ModelState
).
Client-side validation alerts the user before the form is submitted, which avoids a round-trip. However, server-side validation is still important, because it guards against malicious requests, and works even if the user has JavaScript disabled.
The data annotation attributes like
[Required]
and [Range]
only give you basic validation. To validate more complex business rules, you’ll need to write additional code that is specific to your domain.Add an edit page
Add the following methods to
BookController
:public async Task<ActionResult> Edit(int id)
{
Book book = await FindBookAsync(id);
if (book == null)
{
Logger.LogInformation("Edit: Item not found {0}", id);
return HttpNotFound();
}
ViewBag.Items = GetAuthorsListItems(book.AuthorID);
return View(book);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Update(int id, [Bind("Title", "Year", "Price", "Genre", "AuthorID")] Book book)
{
try
{
book.BookID = id;
BookContext.Books.Attach(book);
BookContext.Entry(book).State = EntityState.Modified;
await BookContext.SaveChangesAsync();
return RedirectToAction("Index");
}
catch (DataStoreException)
{
ModelState.AddModelError(string.Empty, "Unable to save changes.");
}
return View(book);
}
private Task<Book> FindBookAsync(int id)
{
return BookContext.Books.SingleOrDefaultAsync(book => book.BookID == id);
}
This code is very similar to adding a new entity, except for the code needed to update the database:
BookContext.Entry(book).State = EntityState.Modified;
await BookContext.SaveChangesAsync();
Add a view named Views/Book/Edit.cshtml view with the following code:
@model ContosoBooks.Models.Book
<div>
<form asp-controller="Book" asp-action="Update" method="post" asp-route-id="@Model.BookID">
<div asp-validation-summary="ValidationSummary.ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Title"></label>
<input asp-for="Title" class="form-control"/>
<span asp-validation-for="Title" class="text-danger"></span>
</div>
<div class="form-group">
<select asp-for="AuthorID" asp-items="@ViewBag.Items"></select>
</div>
<div class="form-group">
<label asp-for="Year"></label>
<input asp-for="Year" class="form-control" />
<span asp-validation-for="Year" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Price"></label>
<input asp-for="Price" class="form-control" />
<span asp-validation-for="Price" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Genre"></label>
<input asp-for="Genre" class="form-control" />
<span asp-validation-for="Genre" 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>
}
This view defines a form, very similar to the Create form.
Add a delete page
Add the following code to
BookController
.[HttpGet]
[ActionName("Delete")]
public async Task<ActionResult> ConfirmDelete(int id, bool? retry)
{
Book book = await FindBookAsync(id);
if (book == null)
{
Logger.LogInformation("Delete: Item not found {0}", id);
return HttpNotFound();
}
ViewBag.Retry = retry ?? false;
return View(book);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Delete(int id)
{
try
{
Book book = await FindBookAsync(id);
BookContext.Books.Remove(book);
await BookContext.SaveChangesAsync();
}
catch (DataStoreException)
{
return RedirectToAction("Delete", new { id = id, retry = true });
}
return RedirectToAction("Index");
}
Add a view named Views/Book/Delete.cshtml view with the following code:
@model ContosoBooks.Models.Book
@{
ViewBag.Title = "Confirm Delete";
}
<h3>Are you sure you want to delete this?</h3>
@if (ViewBag.Retry)
{
<p class="alert-danger">Error deleting. Retry?</p>
}
<div>
<dl class="dl-horizontal">
<dt>
@Html.DisplayNameFor(model => model.Title)
</dt>
<dd>
@Html.DisplayFor(model => model.Title)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Year)
</dt>
<dd>
@Html.DisplayFor(model => model.Year)
</dd>
</dl>
<div>
<form asp-controller="Book" asp-action="Delete" method="post">
<div class="form-group">
<input type="submit" class="btn btn-default" value="Delete" />
</div>
</form>
<p><a asp-controller="Author" asp-action="Index">Back to List</a></p>
</div>
</div>
The basic flow is:
- From the details page, the user clicks the “Delete” link.
- The app displays a confirmation page.
- The confirmation page is a form. Submitting the form (via HTTP POST) does the actual deletion.
You don’t want the “Delete” link itself to delete the item. Performing a delete operation in response to a GET request creates a security risk. For more information, see ASP.NET MVC Tip #46 — Don’t use Delete Links because they create Security Holes on Stephen Walther’s blog.
Wrapping up
The sample app has equivalent pages for authors. However, they don’t contain any new concepts, so I won’t show them in the tutorial. You can browse the source code on GitHub.
For information about deploying your app, see Publishing and Deployment.
Nice information about mvc framework.
ReplyDeleteConvert ASP to ASP.Net