Moving from one ecosystem to another can be challenging, especially when unfamiliar with the technology stack and available tools. So I wrote this post for anyone coming to ASP.NET Core from another ecosystem or .NET developers who might be dabbling with web development for the first time. Knowing where all the bits and bobs are in a new environment can be overwhelming, and I’m here to help.
As you read through the post, we’ll create a new ASP.NET Core project, integrate a data access library called Entity Framework Core 7, and discuss the steps you need to take toward a straightforward solution. Like all solutions, I developed this guidance from my first-hand experience.
Please do so if you’d like to experiment with your choices at any point in this tutorial. This guide is a jumping-off point so you can succeed. This blog post assumes you have .NET installed and some kind of editor or tooling. I recommend JetBrains Rider but this tutorial should work with Visual Studio and Visual Studio Code.
Let’s go ahead and get started.
Creating a New ASP.NET Core Web Project
Before we get started, you’ll need to create a new ASP.NET Core solution. Creating a new .NET solution can typically be done in one of two ways: Command-line tooling or through your favorite IDE (I prefer JetBrains Rider).
For this guide, I’ll show you how to create a new Razor Pages web project using the command line. First, in a new terminal, type the following command.
dotnet new webapp -o WebApplication
The command will create a web project ready for development. Note: This template is available in most IDEs and will create an additional solution file with your project in a nested folder when selected. A solution is a typical structure for most .NET applications, as you may have more than one project per solution.
You’re ready to start adding some of our necessary dependencies from here.
Tools and NuGet Dependencies
We’ll start by installing a tool manifest in our solution directory. The tool manifest keeps track of all command-line tooling installed for the current solution, and many of EF Core’s management feature set exists in the dotnet-ef
tool.
Type the following command from the root folder of your solution.
dotnet new tool-manifest
The command will create a .config
folder with a dotnet-tool.json
file. It’s not necessary to look at it. Just know it’s a manifest of all the tools installed for your local development needs.
Next, you’ll need to install the EF Core 7 tooling with the following command.
dotnet tool install dotnet-ef
Now, let’s move on to installing our EF Core 7 dependencies.
You’ll need to install two specific packages in your web application project.
The first package is your EF Core database provider package, and you can choose from Microsoft SQL Server, PostgreSQL, MySQL, or other providers. In this case, I decided to use SQLite because it’s file-based and gets the point across.
The second is EF Core’s Design package, which allows you to generate database migrations. These migrations help keep your database schema compatible with your C# models.
Here’s my .csproj
file.
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.2" />
</ItemGroup>
</Project>
NuGet will install all other EF Core packages transitively from these two packages. Our web application is now ready for some code.
Designing a Database with EF Core and C#
EF Core supports multiple design philosophies, but the one I’m fond of is Code-First. Code first lets you design C# models, which EF Core will translate into a database schema. Create a folder at the root of your web application named “Models” and create a new file called “Database.cs”. Add the following content to the file. This code contains our first table and our DbContext
implementation.
Adjust the namespace according to your project’s namespace if you’d like.
using Microsoft.EntityFrameworkCore;
namespace WebApplication4.Models;
public class Database : DbContext
{
public Database(DbContextOptions<Database> options)
: base(options)
{}
public DbSet<Person> People => Set<Person>();
}
public class Person
{
public int Id { get; set; }
public string Name { get; set; } = "";
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}
This code will create a new “People” table with the columns of Id
, Name
, and CreatedAt
. You’ll also notice that our Database
class takes a parameter of DbContextOptions<Database>
; this is how you configure our database settings. The following section will show you how to set up values like connection strings and database providers.
Services Collection and Configuring Our DbContext
You’ll need to register the type with ASP.NET Core’s services collection to take advantage of your newly created’ Database’ class. The registration process allows you to set up everything just the way you want, with options like database provider, logging, interceptors, and more. For this guide, let’s keep it simple. Add the following lines of code in your Program.cs
file.
builder.Services.AddDbContext<Database>(options => {
var config = builder.Configuration;
var connectionString = config.GetConnectionString("database");
options.UseSqlite(connectionString);
});
You’ll notice that you’re using the configuration to get a connection string. So what does the configuration file look like? In your appSettings.Development.json
file, you’ll have the following.
{
"DetailedErrors": true,
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"ConnectionStrings": {
"database": "Data Source=database.db"
}
}
Here you can add your connection strings and read them in code. I recommend storing connection strings in environment variables or cloud-specific settings providers when running your application in production settings. Do not store sensitive information in source control under any circumstance.
The next step is to create our database instance using database migrations and EF Core tooling.
EF Core Migrations and Databases
We have most of the working parts of our application set in place, but we still haven’t touched a database. That changes now. You’ll be creating your first migration from the command line. From the root directory of your solution, type the following command:
dotnet ef migrations add "Initial" --project WebApplication4
Be sure you change the --project
argument to match your project’s name. You can also exclude this argument by executing the command from the web application’s root directory instead of the solution directory.
When executed correctly, you should see a new “Migrations” folder in your web application project and a timestamped migration named “Initial”. Opening the file will show the C# to Database schema translation that EF Core performed on your behalf.
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace WebApplication4.Migrations
{
/// <inheritdoc />
public partial class Initial : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "People",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Name = table.Column<string>(type: "TEXT", nullable: false),
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_People", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "People");
}
}
}
Let’s apply this to a database. Running the following command will use the connection string in your app settings. Be sure it is a local developer instance of a database.
dotnet ef database update --project WebApplication4
If you’re using SQLite, you should now see a database.db
or file with the same name as is found in your connection string. If you’re using another database provider, you can now see the EF Core-created tables in your database instance.
For production purposes, I recommend you run migrations outside the running application. A lot can go wrong with database migration, and you don’t want a lousy migration to stop your application from running. That said, you can run migrations from your application’s startup for local development purposes. As an optional step, you can add the following migration code to your “Program.cs” file, below the call to var app = builder.Build();
.
if (app.Environment.IsDevelopment())
{
// migrate database, only during development
using var scope = app.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<Database>();
await db.Database.MigrateAsync();
}
Let’s start using our DbContext
and database instance.
Using a DbContext in a Razor Page
Our goal in this section is to use our Database
class to read and write to our People
table. If you’re following along, you should have an Index.cshtml
along with an Index.cshtml.cs
file under a Pages
directory. We’ll be modifying the Index.cshtml.cs
type to pass in a Database
parameter to the constructor. So let’s start with the most straightforward modification of constructor injection.
using Microsoft.AspNetCore.Mvc.RazorPages;
using WebApplication4.Models;
namespace WebApplication4.Pages;
public class IndexModel : PageModel
{
private readonly ILogger<IndexModel> _logger;
private readonly Database _database;
public Person? Person { get; set; }
public IndexModel(ILogger<IndexModel> logger, Database database)
{
_logger = logger;
_database = database;
}
public void OnGet()
{
Person = _database
.People
.OrderByDescending(p => p.CreatedAt)
.FirstOrDefault();
}
}
The above code takes in a Database
instance and sets it to a private member. Then, on the page’s response to a GET
HTTP request, the OnGet
method will query for the latest person in the People
table. You can set the result to the Person
property. You’ll use this later when building the view.
Let’s also take a look at parameter injection. You’ll need to add an OnPost
method and a new bindable property. The new method will also take a Database
instance as an argument, but you’ll need a FromServices
attribute to tell ASP.NET Core to resolve this value from the services collection. Let’s look at the updated file.
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using WebApplication4.Models;
namespace WebApplication4.Pages;
public class IndexModel : PageModel
{
private readonly ILogger<IndexModel> _logger;
private readonly Database _database;
public Person? Person { get; set; }
public IndexModel(ILogger<IndexModel> logger, Database database)
{
_logger = logger;
_database = database;
}
public void OnGet()
{
Person = _database
.People
.OrderByDescending(p => p.CreatedAt)
.FirstOrDefault();
}
[BindProperty, Required, DisplayName("Person's Name")]
public string? Name { get; set; }
public async Task<IActionResult> OnPost([FromServices] Database db)
{
if (ModelState.IsValid)
{
Person person = new() { Name = Name! };
db.People.Add(person);
await db.SaveChangesAsync();
return RedirectToPage();
}
// show validation messages
return Page();
}
}
Before continuing to the Razor view, let’s discuss the OnPost
implementation. It’s a critical practice when developing applications to adhere to a GET-POST-Redirect pattern. The pattern keeps your users from accidentally refreshing the page and submitting multiple requests for the same action. The method uses a return value of IActionResult
so you can return numerous interface implementations, including RedirectToPage
and Page
.
In addition to handling a form post, we need properties to bind to our ASP.NET Core Razor Pages form. In this case, we only have one property of Name
. In Razor Pages, the page model is where you add all your form fields, but you can also certainly move them into individual models. If you’re using ASP.NET Core MVC, you might apply a “View Model” approach, which will also work here.
Let’s write some Razor!
Creating an ASP.NET Core Razor Form
The culmination of our work has led you to the Razor view, where you’ll be adding a form to add new Person
instances to your database. First, let’s look at the view and break down its parts.
@page
@model IndexModel
@{
ViewData["Title"] = "Home page";
}
@if (Model.Person is {} person)
{
<div class="alert alert-primary" role="alert">
Hello <span class="alert-link">@person.Name</span>!
Nice to meet you.
</div>
}
<form asp-page="Index" method="post">
<div asp-validation-summary="All"></div>
<div class="input-group">
<div class="form-group mb-2">
<label asp-for="Name"></label>
<input class="form-control" asp-for="Name">
<span asp-validation-for="Name" class="invalid-feedback"></span>
</div>
</div>
<button class="btn btn-primary" type="submit">Save Person</button>
</form>
The form
element uses TagHelpers to use the IndexModel
properties to build our form. We are also telling the form to POST
the form to our page handler of OnPost
using the asp-page
attribute. Finally, there’s a submit button.
Above our form, we’ve also added usage of our Person
property. If it is not null, we’ll show an alert from the last entry in the database.
Running your application now, you’ll be able to save and read data in an ASP.NET Core application using Entity Framework Core. Congratulations!
Conclusion
Getting a working ASP.NET Core application using EF Core can seem tedious, but in most cases, the “plumbing” work is done once upfront and never touched again.
You’ll spend most of your time adding a C# class to your DbContext
, adding new database migrations, and then plumbing that entity to a Razor page. That’s the positive side of this approach; adding new functionality is boring with few surprises. When building and maintaining applications, boring is a good thing.