C# Corner
ASP.NET Core: Learning the Ropes, Part 2
Welcome to Part 2 of ASP.NET Core: Learning the Ropes. I'll show how to use Entity Framework (EF) Core in the application that was developed in Part 1. I'll go over how to implement the basic CRUD operations using EF Core. To get started, download the code (clicking on link initiates download) from Part 1. Then install the Microsoft.EntityFrameworkCore.SqlServer NuGet package as seen in Figure 1.
Next install the Microsoft.EntityFrameworkCore.Tools NuGet package, which is needed to support migrations easily (Figure 2)
Now that EF Core is installed it's time to add the DbContext class that we will use to perform CRUD operations on the Contact model. Add a new class to the Models folder named AppDbContext. Next add a using statement for Entity Framework Core to the class:
using Microsoft.EntityFrameworkCore;
Next make the class inherit from DbContext:
public class AppDbContext : DbContext
Then add a constructor that takes a DbContextOptions<AppDbContext> and passes it to the base constructor:
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
{
}
Lastly add a DbSset<Contact> property named Contacts:
public DbSet<Contact> Contacts { get; set; }
Your AppDbContext class should now look like this:
using Microsoft.EntityFrameworkCore;
namespace VSMASPCoreApp.Models
{
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
{
}
public DbSet<Contact> Contacts { get; set; }
}
}
Now that we have our custom DbContext class we will need a connection to a database. Open up the appsettings.json file and add the following connection string setting:
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\MSSQLLocalDB;Database=VSMASPCoreApp;Trusted_Connection=True;MultipleActiveResultSets=true"
}
Your appsettings.json file should now look like this:
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\MSSQLLocalDB;Database=VSMASPCoreApp;Trusted_Connection=True;MultipleActiveResultSets=true"
}
}
Now we need to update the Startup class to configure EF Core to use our AppDbContext class with the "DefaultConnection" connection string. Open up the Start.cs file and add using statements for the Models and Microsoft.EntityFrameworkCore namespaces:
using VSMASPCoreApp.Models;
using Microsoft.EntityFrameworkCore;
Next update the ConfigureServices method to add our custom DbContext class using the set "DefaultConnection" connection string and use SQL Server:
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
Your Startup class should now match Listing 1.
Listing 1: Updated Startup.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using VSMASPCoreApp.Models;
using VSMASPCoreApp.Repositories;
namespace VSMASPCoreApp
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddTransient<IContactRepository, MockContactRepository>();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}
Now it's time to update the contact repository interface to support the full CRUD operations. The current interface only defines getting all contacts and getting a contact by its ID. We are going to add methods to add, update and delete a contact. Open up the IContactRepository interface file. Next we add a method to add a contact to the database:
void AddContact(Contact contact);
Then add a method to update a given contact:
void UpdateContact(Contact contact);
Lastly add a method to delete a contact by its ID:
void DeleteContact(int id);
Your IContactRepository interface should now look like this:
using System.Collections.Generic;
using VSMASPCoreApp.Models;
namespace VSMASPCoreApp.Repositories
{
public interface IContactRepository
{
Contact GetContactById(int id);
IEnumerable<Contact> GetContacts();
void AddContact(Contact contact);
void UpdateContact(Contact contact);
void DeleteContact(int id);
}
}
Next we need to update the MockContactRepository to implement the new members. As we aren't going to be using the new methods on the MockContactRepository you can let Visual Studio stub them out for you as seen in Listing 2.
Listing 2: MockContactRepository.cs
using System.Collections.Generic;
using System.Linq;
using VSMASPCoreApp.Models;
namespace VSMASPCoreApp.Repositories
{
public class MockContactRepository : IContactRepository
{
private List<Contact> _contacts;
public MockContactRepository()
{
_contacts = new List<Contact>
{
new Contact
{
ContactId = 1,
FirstName = "Phillip",
LastName = "Fry",
EmailAddress = "[email protected]"
},
new Contact
{
ContactId = 2,
FirstName = "Hubert",
LastName = "Farnsworth",
EmailAddress = "[email protected]"
},
new Contact
{
ContactId = 3,
FirstName = "Amy",
LastName = "Wong",
EmailAddress = "[email protected]"
},
new Contact
{
ContactId = 4,
FirstName = "Turanga",
LastName = "Leela",
EmailAddress = "[email protected]"
},
new Contact
{
ContactId = 5,
FirstName = "Bender",
LastName = "Rodriguez",
EmailAddress = "[email protected]"
}
};
}
public void AddContact(Contact contact)
{
throw new System.NotImplementedException();
}
public void DeleteContact(int id)
{
throw new System.NotImplementedException();
}
public Contact GetContactById(int id)
{
return _contacts.SingleOrDefault(x => x.ContactId == id);
}
public IEnumerable<Contact> GetContacts()
{
return _contacts.OrderBy(x => x.LastName).ThenBy(x => x.FirstName);
}
public void UpdateContact(Contact contact)
{
throw new System.NotImplementedException();
}
}
}
Now it's time to implement the real contact repository that will use EF Core to hit a real database. Add a new class file named ContactRepository to the Repositories folder. First add a using statement for the Models namespace as this is where the AppDbContact class resides:
using VSMASPCoreApp.Models;
Then make the class inherit the IContactRepository interface:
public class ContactRepository : IContactRepository
Next add a private read-only member variable of type AppDbContext and initialize it in the constructor:
private readonly AppDbContext _appDbContext;
public ContactRepository(AppDbContext appDbContext)
{
_appDbContext = appDbContext;
}
Next I implement the AddContact method that adds and saves a contact using the AppDbContext:
public void AddContact(Contact contact)
{
_appDbContext.Contacts.Add(contact);
_appDbContext.SaveChanges();
}
Then I implement the DeleteContact method that finds a contact by its ID, removes the record, and saves the change:
public void DeleteContact(int id)
{
var contact = _appDbContext.Contacts.SingleOrDefault(x => x.ContactId == id);
if(contact != null)
{
_appDbContext.Contacts.Remove(contact);
_appDbContext.SaveChanges();
}
}
Next I implement the GetContactById method that finds a contact by its ID:
public Contact GetContactById(int id)
{
return _appDbContext.Contacts.SingleOrDefault(x => x.ContactId == id);
}
Then I implement the GetContacts method which returns all of the contacts sorted by last name and then first name:
public IEnumerable<Contact> GetContacts()
{
return _appDbContext.Contacts.OrderBy(x => x.LastName).ThenBy(x => x.FirstName);
}
Lastly I implement the UpdateContact method which gets a contact record and updates the fields on it:
public void UpdateContact(Contact contact)
{
var existing = _appDbContext.Contacts.SingleOrDefault(x => x.ContactId == contact.ContactId);
if (existing != null)
{
existing.FirstName = contact.FirstName;
existing.LastName = contact.LastName;
existing.EmailAddress = contact.EmailAddress;
_appDbContext.SaveChanges();
}
}
Your ContactRepository class should now look like Listing 3.
Listing 3: ContactRepository.cs
using System.Collections.Generic;
using System.Linq;
using VSMASPCoreApp.Models;
namespace VSMASPCoreApp.Repositories
{
public class ContactRepository : IContactRepository
{
private readonly AppDbContext _appDbContext;
public ContactRepository(AppDbContext appDbContext)
{
_appDbContext = appDbContext;
}
public void AddContact(Contact contact)
{
_appDbContext.Contacts.Add(contact);
_appDbContext.SaveChanges();
}
public void DeleteContact(int id)
{
var contact = _appDbContext.Contacts.SingleOrDefault(x => x.ContactId == id);
if(contact != null)
{
_appDbContext.Contacts.Remove(contact);
_appDbContext.SaveChanges();
}
}
public Contact GetContactById(int id)
{
return _appDbContext.Contacts.SingleOrDefault(x => x.ContactId == id);
}
public IEnumerable<Contact> GetContacts()
{
return _appDbContext.Contacts.OrderBy(x => x.LastName).ThenBy(x => x.FirstName);
}
public void UpdateContact(Contact contact)
{
var existing = _appDbContext.Contacts.SingleOrDefault(x => x.ContactId == contact.ContactId);
if (existing != null)
{
existing.FirstName = contact.FirstName;
existing.LastName = contact.LastName;
existing.EmailAddress = contact.EmailAddress;
_appDbContext.SaveChanges();
}
}
}
}
Now it's time to tell the application to use the real contact repository. Open up the Startup class and change the ConfigureServices method to use the real contact repository class:
services.AddTransient<IContactRepository, ContactRepository>();
Your updated Startup class file should now look like Listing 4.
Listing 4: Updated Startup.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using VSMASPCoreApp.Models;
using VSMASPCoreApp.Repositories;
namespace VSMASPCoreApp
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddTransient<IContactRepository, ContactRepository>();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}
Next we need to add an initial EF Core migration to create the database from our model. Open up the Package Manager Console as seen in Figure 3.
In the Package Manager Console run the command "Add-Migration Initial."
You should now see a Migrations folder added to your project with a class file named Initial.cs. Now that the migration has been created it is time to create the database. Run the "Update-Database" command in the Package Manager Console. Now you should be able to run the application to verify that the database connection is indeed working. Once the application loads click on the Contact menu item and you should see an empty table with header rows on it as seen in Figure 4.
This isn't very user friendly if the user hasn't added any contacts yet in the system. Let's update the Index.cshtml Contact view to output a nice message that says "No contacts found, add a contact first." Open the Views\Contact\Index.cshtml and add in the markup from Listing 5.
Listing 5: Contact Index.cshtml
@model IEnumerable<Contact>
@if (Model.Any())
{
<table class="table-striped table-responsive table-bordered">
<thead>
<tr>
<th>First</th>
<th>Last</th>
<th>Email</th>
</tr>
</thead>
<tbody>
@foreach (var contact in Model)
{
<tr>
<td>@contact.FirstName</td>
<td>@contact.LastName</td>
<td>@contact.EmailAddress</td>
</tr>
}
</tbody>
</table>
}
else
{
<div class="alert alert-info" role="alert">
No contacts found, add a contact first.
</div>
}
You should now see the "No contacts found, add a contact first." message when you go to the Contact page as seen in Figure 5.
In this article I showed how to setup Entity Framework Core in an ASP.NET MVC Core application. You've seen how to implement the basic CRUD operations using EF Core. Stay tuned for the third and final article of this series where I'll show how to implement the view and controller actions needed to leverage our new contact repository.