.Net MVC App With EF Core
Last modified: 12 February 2025How to build a basic app with full CRUD implementation using MVC.
Setup
OS: macOS
IDE: Rider
Framework: net 8.0
Database IDE: DataGrip
Database: SQL Server using Docker
ORM: Entity Framework Core
Create The Project
Launch Rider and select new solution.
Select 'Put solution and project in the same directory' (If this is a standalone project).
Enter a solution and project name. Select 'Web App (Model-View-Controller' as the template. Press Create.
You can now run the project which will be a very basic website with a header with links to a home and privacy pages, plus a footer.
DOTNET Watch
DOTNET Watch is A tool that enables a developer to build and run an application automatically whenever changes are made to the source code.
This greatly improves the developer experience, to enable it in Rider go to edit configurations, then click the plus symbol to add a new configuration. Select dotnet-watch from the list and give the configuration a name.
In order to have it launch Chrome instead of the default browser, select the checkbox next to suppress launching browser, then in the before launch section, click add and select launch web browser. Select Chrome as the browser then add http://localhost:5127
as the URL.
Understanding the Project Files
The project files are organized in the following structure:
csproj: To view, click on project name in solution explorer and press F4. This file defines the overall structure of the project, lists dependencies and can contain build instructions and build configurations.
Properties: This folder usually stores settings that define project specifics, for example, assembly information.
launchsettings.json: (Contained within the properties folder) This is a configuration file that is used by the .NET Core runtime for setting up the hosting environment during the development phase.
wwwroot: Contains static files such as CSS, JavaScript, and images.
Controllers: Contains the controllers that handle the incoming requests and define the actions to be performed.
Models: Contains the data models used by the application.
Views: Contains the HTML templates that define the user interface.
appsettings.json: Contains configuration settings for the application.
Program.cs: The main project file ...
Understanding the Default Project
Controllers and Views
Here is a basic explanation of the controllers and views in the default project.
Controllers handle the logic and flow of the application, while views are responsible for displaying the data to the user. In the default project, controllers can be found in the "Controllers" folder and views can be found in the "Views" folder.
In the controllers folder, you will find a file called HomeController.cs. This home controller dictates what the home view will do. The name 'Home' that appears before the word controller in the title needs to have a folder with the same name within the views' folder. That folder should contain files that match the names of the actions listed in the home controller (Index and Privacy for the default app).
IActionResult methods within the home controller:
public IActionResult Index()
{
return View();
}
When the above method Index() is called from the home controller, the view named index will be returned from within the home folder in views. It knows where to look using the name of the controller and the method. It knows to look in the home folder as that is the name of the controller, and it knows to return the index view as that is the name of the IActionResult method.
_Layout.cshtml View
_Layout.cshtml view is a shared layout file that contains the common elements of all views in the project. It typically includes the HTML structure, navigation menu, and other elements that are common across multiple pages.
<div class="container">
<main role="main" class="pb-3">
@RenderBody()
</main>
</div>
Between the header and footer code within this view is the above div containing a section called main. This contains a function called RenderBody. It is this line that loads the rest of the views within the application.
How does the layout file get loaded?
The _ViewStart.cshtml file is a special file automatically executed before any view is rendered. It is used to set the layout file for all views in the project. By default, the _ViewStart.cshtml file in the Shared folder sets the layout file to _Layout.cshtml. However, you can customize this file to set a different layout file for specific views or areas of your project.
The default view loaded by RenderBody() is set within the main Program.cs file:
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
So we can see above that the index action within the home controller is called to load the default view. When the user clicks a link etc. to request a new page, the contents of the RenderBody() function are replaced with the requested view.
At the end of the body section in the layout file is this line:
@await RenderSectionAsync("Scripts", required: false)
This line is used to render any scripts that are specific to a particular view. It is optional and can be omitted if there are no scripts to render.
So what this means is, that you can add the following to the bottom of a view:
@section Scripts {
}
And here you can add any JavaScript code required by that view.
ViewImports and ValidationScriptsPartial
ViewImports.cshtml is located in the Views folder. This file contains using statements for the views, models and tag helpers in the project. This file allows you to use any view and model within your project without having to declare it with a using statement at the top of the view file. Tag helpers are the .net commands used inline such as asp-controller and asp-action. This file is what gives you access to them in your views.
ValidationScriptsPartial.cshtml is a partial view that includes the necessary JavaScript files for client-side validation. It is typically included in the layout file and is used to enable client-side validation for forms in the project.
appsettings.json
The appsettings.json file is a configuration file that stores various settings for the application. It is typically used to store connection strings, API keys, and other sensitive information. Multiple appsettings files can be created so that separate connection strings etc can be stored for use in different environments such as dev and live.
program.cs
The program.cs file is the entry point of the application. It contains the Main method which is the starting point of the application. This method sets up the web host and configures the application's services and middleware.
var builder = WebApplication.CreateBuilder(args);
This line initializes a new instance of WebApplicationBuilder using the provided args. The CreateBuilder method sets up the application's configuration sources, logging, and other essential services needed to build the web application.
builder.Services.AddControllersWithViews();
Here, services are being added to the Dependency Injection (DI) container. AddControllersWithViews is a method call that registers services required for controllers and views in an ASP.NET Core MVC application. This enables the Model-View-Controller (MVC) pattern, with controllers that can render views.
var app = builder.Build();
After configuring services, the builder.Build() method is called, which compiles everything into a WebApplication instance. This app instance will be used to configure and run the application.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
This line checks if the current environment is not a development environment. When in a non-development environment, this line adds a middleware to the application's request pipeline that will handle exceptions. Instead of showing detailed error information (which you might want to avoid in production for security reasons), this middleware will direct the user to a generic error page. The "/Home/Error" argument specifies the path where the error-handling controller action is located.
HSTS stands for HTTP Strict Transport Security. This is a web security policy mechanism that helps to protect websites against protocol downgrade attacks and cookie hijacking. The app.UseHsts() method enforces this policy by adding a Strict-Transport-Security header to responses. The default duration for this policy is 30 days.
app.UseHttpsRedirection();
This middleware redirects HTTP requests to HTTPS. This is an important security feature ensuring that communications between the client and server are encrypted.
app.UseStaticFiles();
This middleware serves static files such as HTML, CSS, JavaScript, and images. It looks for these files in the default wwwroot folder or any other specified directory.
app.UseRouting();
Adds routing capabilities to the middleware pipeline. This middleware looks at the incoming request and maps it to an endpoint (such as a controller action) based on matching route patterns.
app.UseAuthorization();
This middleware ensures that the user is authorized to access secured resources based on the app's defined authorization policies. It must be placed after app.UseRouting but before any endpoint mapping (app.UseEndpoints or similar).
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
This line defines the default route for the application. This means requests like /Home/Index or /Home/Index/1 will be mapped to the Index action of the Home controller.
app.Run();
This line starts the application and begins listening for incoming HTTP requests.
Creating the Database
These are the steps required to set up and connect to the database, and create the required table etc
Create the Model for the Database
First, we need a model to represent the data that will be held in our database.
For the diary app we are going to create we need a database that has the following columns:
Id
Title
Content
Date
Right-click on the Models folder in the solution explorer, select add, then select class. Enter the name DiaryEntry.
namespace lambdaluke_mvc_example.Models;
public class DiaryEntry
{
}
Add the properties we need for the database:
namespace lambdaluke_mvc_example.Models;
public class DiaryEntry
{
public int Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public DateTime Created { get; set; }
}
We will use EntityFramework to create our database using this model. The name given to the table will be the name of the model, DiaryEntry. And the columns will be named after our four properties.
[Key]
public int Identifier { get; set; }
But if the name Id, or DiaryEntryId is used, then this is not required.
To prevent the user from creating entries in the database with null values, add the required attribute to the properties. And to prevent any possible null reference exceptions, initialize the values so they can never be empty:
public int Id { get; set; }
[Required]s
public string Title { get; set; } = string.Empty;
[Required]
public string Content { get; set; } = string.Empty;
[Required]
public DateTime Created { get; set; } = DateTime.Now;
Add a Connection String
Next, we need to add a connection string to link our application to the database.
Update the appsettings.json file with a connection string:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"DefaultConnection": "Server=localhost; Database=DiaryTest; Trusted_Connection=false; TrustServerCertificate=True; User Id=sa; Password=MyPassword;"
}
}
For the database field, you should enter whatever you would like the created database to be called. For user id and password, you should use the appropriate details for your SQL Server instance.
Install the Required Packages
Next, we need to install EntityFrameworkCore in our application.
Go to the Nuget Package manager, and search for and add the following packages to the project:
Microsoft.EntityFrameworkCore
Microsoft.EntityFrameworkCore.SqlServer
Microsoft.EntityFrameworkCore.Tools
Create a Database Context Class
next, we need to create a database context class called ApplicationDbContext
, which is a part of Entity Framework Core. This context class is used to interact with the database.
Create a new folder within the project called Data. Then add a new class file called ApplicationDbContext:
namespace lambdaluke_mvc_example.Data;
public class ApplicationDbContext
{
}
Add a constructor and inherit from DbContext
, which is a class from EF Core that manages database connections and is used to query or save data to the database. Pass in a parameter called options
of type DbContextOptions<ApplicationDbContext>
. DbContextOptions
encapsulates all the configuration related to DbContext
, such as database connection strings, database provider (SQL Server, SQLite, etc.), and other database-specific settings. Pass this to the base class using : base(options)
. This ensures that the DbContext is properly configured with those options.
using Microsoft.EntityFrameworkCore;
namespace lambdaluke_mvc_example.Data;
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
{
}
}
Add a DbSet property of type DiaryEntries (the model we created for our table)
using lambdaluke_mvc_example.Models;
using Microsoft.EntityFrameworkCore;
namespace lambdaluke_mvc_example.Data;
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
{
}
public DbSet<DiaryEntry> DiaryEntries { get; set; }
}
DbSet is a property declaration within the ApplicationDbContext class, which is part of an Entity Framework Core context. This property maps the DiaryEntry model class to a table in the database.
Add a Service
Next, we need to register our database context class as a service by adding the following line to the Program.cs file:
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
This line registers our class as a service, then configures it to use SQL Server as the database provider, then retrieves our connection string.
This registers ApplicationDbContext as a service in the application's dependency injection container. This setup enables the application to inject ApplicationDbContext wherever needed and ensures it is properly configured to interact with the specified SQL Server database.
Run SQL Server
First, ensure that SQL Server is up and running in a docker container.
Open DataGrip (either app or plugin) select connect to database, then select add data source manually.
Set data source to Microsoft SQL Server
Enter port number as 1433 Enter the server name as localhost
Enter the user name as appropriate
Enter the password as appropriate
Click test connection
If test was successful click connect to database
Add a Migration
A migration is an instruction to how we want to modify our database.
Right click on the project name in the solution explorer, select Entity Framework Core then select add migration.
The add migration window will open, with a default name for the migration of 'Initial'. You can either keep this name or change it to something like the name for our table.
Press ok.
Once the process has finished a Migrations folder will be added to the project. This folder contains the class files with the instruction to build our table.
Now, to run the migration to actually create the database and table, right-click on the solution name, select Entity Framework Core, then select update database, then press ok on the window that opens.
To view our table in the database open DataGrip, right-click on the connection, select tools then select manage shown schemas. Select the name of the database you created (it will have been given the name you set in your connection string). Then you can expand the options within the database until you see tables, and the name of our new table (DiaryEntries).
Double-clicking on the table name will show the table columns.
Seeding Data
Within the ApplicationDbContext class, add a new method called OnModelCreating (just typing the name of this method and hitting tab will auto create it).
using lambdaluke_mvc_example.Models;
using Microsoft.EntityFrameworkCore;
namespace lambdaluke_mvc_example.Data;
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
{
}
public DbSet<DiaryEntry> DiaryEntries { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
}
}
Within this method, add the following line:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<DiaryEntry>().HasData();
}
This tells Entity Framework Core that we are configuring the DiaryEntry table in our database (represented by our DiaryEntry class).
HasData is a method that seeds initial data into the table, so the next step os to provide this method with some data:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<DiaryEntry>().HasData(
new DiaryEntry
{
Id = 1,
Title = "Went Hiking",
Content = "Went hiking with Joe!",
Created = DateTime.Now
},
new DiaryEntry
{
Id = 2,
Title = "Went Shopping",
Content = "Went shopping with Joe!",
Created = DateTime.Now
},
new DiaryEntry
{
Id = 3,
Title = "Went Diving",
Content = "Went diving with Joe!",
Created = DateTime.Now
}
);
}
Now that we have our data ready to be seeded into our table, we need to create a new migration (A new migration is required any time we wish to make a change to our database using Entity Framework).
Follow the steps to create the migration as before, right-click on the solution, select Entity Framework and create migration.
GIve the migration an appropriate name such as AddedSeedingDataDiaryEntry, this will add a new migration file to the migrations' folder. Then, as before, run update database. You should now see the data in your database table.
- Note:
If you reveice an error when trying to run update database stating that the moldel has changed, add this to the AppicationDBContext calss to ignore the error -
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.ConfigureWarnings(warnings => warnings.Ignore(RelationalEventId.PendingModelChangesWarning)); }
CRUD - Create and Read
Here we will create the controller and views required to implement the create and read elements of our CRUD application.
Add a Controller
Add a new class file to the controllers folder called DiaryEntriesController.
Inherit from the Controller base class, then add an IActionResult called index that returns a view.
using Microsoft.AspNetCore.Mvc;
namespace lambdaluke_mvc_example.Controllers;
public class DiaryEntriesController : Controller
{
public IActionResult Index()
{
return View();
}
}
Creating Views
Now we need to create the view to go with our controller.
Add a new folder called DiaryEntries (to match the name of the controller) inside the Views folder.
Inside the DiaryEntries folder, add a new razor file called Index.cshtml (to match the name of our action method).
You can now test that the controller and view are working. Enter some text such as hello world into the view. Run the application and type /diaryentries/index our new page should now be visible.
We can now add a link to our new page in the header navigation. Open the layout view and look for the existing links (Home and Privacy).
Copy one of these links and paste iit below them. Change the value of the asp-controller attribute to our controller name (DiaryEntries), and change the value of the asp-action attribute to our view name (Index).
Give the <a>
tag a name of My Diary.
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="DiaryEntries" asp-action="Index">My Diary</a>
</li>
Create the View for the Table
Add the following to the DiaryEntries Index page:
@model List<DiaryEntry>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Title</th>
<th>Content</th>
<th>Date</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@foreach (var entry in Model)
{
<tr>
<td>@entry.Title</td>
<td>@entry.Content</td>
<td>@entry.Created</td>
<td>Edit and Delete</td>
</tr>
}
</tbody>
</table>
</div>
The first line declares that the view expects a model of type List<DiaryEntry>
. This list will be used to generate the table rows dynamically.
The classes used define a responsive table that will adapt to different screen sizes. - table-responsive
is a Bootstrap class that makes the table scrollable when needed. - table
, table-striped
, and table-hover
are Bootstrap classes used for styling the table.
The @foreach
loop iterates over each DiaryEntry
object in the Model
(which is a list of DiaryEntry
). - For each entry
in the list, a new table row (<tr>
) is created.
Viewing this page will currently display an error as the controller does not know how to access the database
Add a GET Request to the Controller
Add a private readonly field of type ApplicationDbContext
with an appropriate name such as _db or _dbContext
using lambdaluke_mvc_example.Data;
using Microsoft.AspNetCore.Mvc;
namespace lambdaluke_mvc_example.Controllers;
public class DiaryEntriesController : Controller
{
private readonly ApplicationDbContext _db;
public IActionResult Index()
{
return View();
}
}
This sets up a way for our controller to interact with the database.
Next, add a constructor and pass in a parameter of type ApplicationDbContext
, then assign this parameter to the field we just created
using lambdaluke_mvc_example.Data;
using Microsoft.AspNetCore.Mvc;
namespace lambdaluke_mvc_example.Controllers;
public class DiaryEntriesController : Controller
{
private readonly ApplicationDbContext _db;
public DiaryEntriesController(ApplicationDbContext db)
{
_db = db;
}
public IActionResult Index()
{
return View();
}
}
Now, within the action method, add a variable of type List<DiaryEntry>
and set it to equal the diary entries in the database. Then, pass this variable into the view
using lambdaluke_mvc_example.Data;
using lambdaluke_mvc_example.Models;
using Microsoft.AspNetCore.Mvc;
namespace lambdaluke_mvc_example.Controllers;
public class DiaryEntriesController : Controller
{
private readonly ApplicationDbContext _db;
public DiaryEntriesController(ApplicationDbContext db)
{
_db = db;
}
public IActionResult Index()
{
List<DiaryEntry> objDiaryEntryList = _db.DiaryEntries.ToList();
return View(objDiaryEntryList);
}
}
The results should now be visible in the view.
Add some Styling
We will now make some amendments to the layout and our new page to improve the styling. We will use Bootstrap classes to achieve this as bootstrap is automatically included in .NET projects.
Within the layout view change the class within the nav component from bg-white to bg-primary.
Add the class bg-light to the first anchor tag, then change the class bg-dark to bg-light to the anchor tags within the list items.
Next, we will add an icon to our header link using a bootstrap icon
Go to the bootstrap icons website and search for an appropriate image such as notebook. Then add wither the icon font code (this requires either installing bootstrap icons via npm, or adding the link text to the header in the layout)
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-icons/1.10.0/font/bootstrap-icons.min.css">
Or you can use copy HTML on the website to pasete in the SVG code for the image.
Add this before the text My Diary
in the anchor tag in the navbar.
Now lets add some styling to our diary page. Add the following to the top of the index page after the model declaration
<div class="container">
<div class="row pt-4">
<div class="col-6">
<a class="btn btn-primary">
<i class="bi bi-journal-plus"></i> Create new Diary Entry
</a>
</div>
<div class="col-6 text-end">
<h2>Diary Entries</h2>
</div>
</div>
</div>
Add the Create Controller Method and View
Add a Create action method to the controller that returns a view
public IActionResult Create()
{
return View();
}
Next, create the view that this method will return. Add a new file to the DiaryEntries view folder called Create.cshtml. Add some dummy text to the page for testing purposes, we should now be able to view the page by going to the URL /DiaryEntries/create.
Next, we will use tag helpers to call our new action.
<a class="btn btn-primary" asp-controller="DiaryEntries" asp-action="Create">
<i class="bi bi-journal-plus"></i> Create new Diary Entry
</a>
The asp-controller tag helper states which controller should be used, and the asp-action tag-helper states which IActionResult method is called.
Now we will complete the view, replace the Create.cshtml with the following:
@model DiaryEntry
<div class="container mt-4">
<div class="row">
<div class="col-md-8 offset-md-2">
<div class="card">
<div class="card-header bg-primary text-white">
<h2>Create Diary Entry</h2>
</div>
<div class="card-body">
<form asp-action="" method="post">
<div class="form-group mb-3">
<label asp-for="Title" class="form-label">Title</label>
<input asp-for="Title" class="form-control" placeholder="Enter a title"/>
</div>
<div class="form-group mb-3">
<label asp-for="Content" class="form-label">Content</label>
<input asp-for="Content" aria-owns="5" class="form-control" placeholder="Enter what you did that day"/>
</div>
<div class="form-group mb-3">
<label asp-for="Created" class="form-label">Date</label>
<input asp-for="Created" class="form-control" type="date"/>
</div>
<div class="d-flex justify-content-between">
<button type="submit" class="btn btn-primary">Create</button>
<a asp-controller="DiaryEntries" asp-action="Index" class="btn btn-secondary">Cancel</a>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
Create an Entry and Store it in the Database
In the DiaryEntryController we currently have the following IActionResult that returns are view:
public IActionResult Create()
{
return View();
}
But now we need a new action to create a database entry. So duplicate the existing action we just mentioned, then add an HttpPost attribute. As we want to create an entry pass in a DiaryEntry object:
[HttpPost]
public IActionResult Create(DiaryEntry objDiaryEntry)
{
return View();
}
To link this controller action to our form in the view, update the asp-action on the form tag to equal Create:
<form asp-action="Create" method="post">
We already included access to our database within the controller via dependency injection, so now we can access the database with _db (the name we gave to our ApplicationDbContext property), access the DiaryEntries table, then call the add method and pass in our object:
public IActionResult Create(DiaryEntry objDiaryEntry)
{
_db.DiaryEntries.Add(objDiaryEntry);
return View();
}
Then save the changes with
_db.SaveChanges();
Next we can specify what view we would like the user to be redirected to after the entry is saved using the RedirectToAction method:
[HttpPost]
public IActionResult Create(DiaryEntry objDiaryEntry)
{
_db.DiaryEntries.Add(objDiaryEntry);
_db.SaveChanges();
return RedirectToAction("Index");
}
If you want to redirect to a view that is not within the same controller you can achieve this by passing in the name of the controller as a 2nd parameter.
Now If you fill out the form we should be redirected back to the main diary entries page and see our new entry being retrieved from the database.
Client and Server Side Validation
Adding Client Side Validation
Client side validation is validation that happens in the browser.
If we go to the create diary entry page in our application and attempt to submit a form we will see several error messages. This is because in our DiaryEntry model we have set the fields to be non-nullable by using the required attribute
public class DiaryEntry
{
public int Id { get; set; }
[Required]
public string Title { get; set; } = string.Empty;
[Required]
public string Content { get; set; } = string.Empty;
[Required]
public DateTime Created { get; set; } = DateTime.Now;
}
So we should implement validation to prevent this error screen.
We can do this very easily in by just adding the following to the bottom of our Create.cshtml view:
@section Scripts{
<partial name="_ValidationScriptsPartial" />
}
And that's it! Now if we go back and try to submit the form instead of seeing the ugly error page, the first empty field will be highlighted as soon as we press create.
Adding Client Side Validation Summary
We should now inform the user why there form is currently not valid. One way to do that is with a validation summary.
Again in .net this is very easy to implement by adding a new div above our form with a tag helper called asp-validation-summary
<div asp-validation-summary="All" class="text-danger"></div>
Now when we try to submit our form a list of the requirements not currently met will be displayed (e.g. the title field is required)
Adding Client Side Individual Validations
Rather than the summary that displays all the errors together we can use a different tag helper called asp-validation-for to show just one.
This can be used to display an error directly below the relevant input.
<div class="form-group mb-3">
<label asp-for="Title" class="form-label">Title</label>
<input asp-for="Title" class="form-control" placeholder="Enter a title"/>
<span asp-validation-for="Title" class="text-danger"></span>
</div>
In the example above adding the tag helper within a span beneath the input and stating that it is for the Title will show the validation for that input directly below it.
Amending the Validation Message
If you wish to change the validation message that is displayed this can be done within the model.
Add parenthesis after the word required in the property attribute, pass in the property ErrorMessage
and set it to a string with your chosen message
[Required(ErrorMessage = "Please enter a title!")]
public string Title { get; set; } = string.Empty;
Other validations can be added here as well. For example, we can set a string length requirement using the StringLength
attribute
[Required(ErrorMessage = "Please enter a title!")]
[StringLength(100, MinimumLength = 3, ErrorMessage = "Title must be between 3 and 100 characters.")]
public string Title { get; set; } = string.Empty;
Adding Server Side Validation
The client side validation covers most of the validation needs we require. But, we also need to make sure that no one can bypass our client and enter erroneous data and corrupt out database.
We can do this within our controller by using the ModelState
property which is part of the controller base class
[HttpPost]
public IActionResult Create(DiaryEntry objDiaryEntry)
{
if (objDiaryEntry.Title.Length < 3)
{
ModelState.AddModelError("Title", "Title is too short");
}
if (ModelState.IsValid)
{
_db.DiaryEntries.Add(objDiaryEntry);
_db.SaveChanges();
return RedirectToAction("Index");
}
return View(objDiaryEntry);
}
In the if statement here we are checking if the title property of the submitted object is too short. If it is, the AddModelError
method is called on ModelState
and we pass in the name of the field that we want to attach the error too, and then the error wording.
The next if statement checks if the model is valid and our previous code to update the database is moved here.
Outside of this a new return statement is added to return the existing view along with the supplied object. This will reload the form the user is currently on, with there entered data and display the error we have entered.
Below is a refactored version with the validation moved to its own function:
[HttpPost]
public IActionResult Create(DiaryEntry diaryEntry)
{
ValidateTitleLength(diaryEntry);
if (ModelState.IsValid)
{
_db.DiaryEntries.Add(diaryEntry);
_db.SaveChanges();
return RedirectToAction("Index");
}
return View(diaryEntry);
}
private void ValidateTitleLength(DiaryEntry diaryEntry)
{
if (diaryEntry.Title.Length < 3)
{
ModelState.AddModelError("Title", "Title is too short");
}
}
CRUD - Update and Delete
Now we will add the final CRUD operations, which are Update and Delete, to complete the basic functionality of our application.
Add the Update and Delete Buttons
In the index page for the diary entries view, in the last table data row within out table body, remove the temporary text we added of update and delete.
Replace this with 2 anchor tags with tag helpers of asp-controller and asp-action.
add the value of DiaryEntires
for the controller and leave the action blank for now.
Enter the text Edit and Delete for the anchor tags and add some bootstrap icons as we have before.
Finally add bootstrap classes to make them look more like buttons.
<td>
<a class="btn btn-primary btn-sm" asp-controller="DiaryEntries" asp-action="">
<i class="bi bi-pencil"></i> Edit
</a>
<a class="btn btn-danger btn-sm" asp-controller="DiaryEntries" asp-action="">
<i class="bi bi-trash3"></i> Delete
</a>
</td>
'Update' View and Controller Action
The edit view will be almost identical to our create view, so create a new file called edit.cshtml and then copy over the contents from the create view.
The only details that need to be changed are the header should say Edit View Page instead of create, the form asp-action should be Edit, and the button should say Edit instead of create.
@model DiaryEntry
<div class="container mt-4">
<div class="row">
<div class="col-md-8 offset-md-2">
<div class="card">
<div class="card-header bg-primary text-white">
<h2>Edit Diary Entry</h2>
</div>
<div class="card-body">
<form asp-action="Edit" method="post">
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group mb-3">
<label asp-for="Title" class="form-label">Title</label>
<input asp-for="Title" class="form-control" placeholder="Enter a title"/>
<span asp-validation-for="Title" class="text-danger"></span>
</div>
<div class="form-group mb-3">
<label asp-for="Content" class="form-label">Content</label>
<input asp-for="Content" aria-owns="5" class="form-control" placeholder="Enter what you did that day"/>
<span asp-validation-for="Content" class="text-danger"></span>
</div>
<div class="form-group mb-3">
<label asp-for="Created" class="form-label">Date</label>
<input asp-for="Created" class="form-control" type="date"/>
<span asp-validation-for="Created" class="text-danger"></span>
</div>
<div class="d-flex justify-content-between">
<button type="submit" class="btn btn-primary">Edit</button>
<a asp-controller="DiaryEntries" asp-action="Index" class="btn btn-secondary">Cancel</a>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@section Scripts{
<partial name="_ValidationScriptsPartial" />
}
Next we need to update the button on our index page.
Open the diary entries index page and update the edit button asp-action value to Edit.
We will also need to let the controller know what database entry we are editing. To do this, add another tag helper asp-route-id
and pass in @entry.Id
as the value (entry references the current object in the foreach loop).
<a class="btn btn-primary btn-sm" asp-controller="DiaryEntries" asp-action="Edit" asp-route-id="@entry.Id">
<i class="bi bi-pencil"></i> Edit
</a>
Now we need to add our controller methods.
In the diary entries controller, add a new action method to return the view, this should receive the id we are passing in as a parameter.
Within the method we should create a new object of type DiaryEntry
, and we should make it nullable. The object should be set to our DiaryEntries table within our database, then the find method called via dot notation and the id passed in as a parameter. Then we can return the View with the object as a parameter.
public IActionResult Edit(int? id)
{
DiaryEntry? objDiaryEntry = _db.DiaryEntries.Find(id);
return View(objDiaryEntry);
}
We made the object nullable in case the id is null. We can improve upon this further by adding an if statment before this that checks if the id is null or zero, and if it is return the 404 not found error page.
We can add another if statement that does a similar function for the result of our diary entries object.
public IActionResult Edit(int? id)
{
if (id == null || id == 0)
{
return NotFound();
}
DiaryEntry? objDiaryEntry = _db.DiaryEntries.Find(id);
if (objDiaryEntry == null)
{
return NotFound();
}
return View(objDiaryEntry);
}
'Update' Post Request
Copy the 'create' post request we already have and rename it to Edit, then Ccange the command called on the database object from Add to Update
[HttpPost]
public IActionResult Edit(DiaryEntry objDiaryEntry)
{
if (objDiaryEntry.Title.Length < 3)
{
ModelState.AddModelError("Title", "Title is too short");
}
if (ModelState.IsValid)
{
_db.DiaryEntries.Update(objDiaryEntry);
_db.SaveChanges();
return RedirectToAction("Index");
}
return View(objDiaryEntry);
}
And that should be all that is required. You can now test this by selecting the edit button on onw of your diary entries, making an alteration and pressing the edit button. You should now be returned to the main diary page, with the updated information showing for that entry.
'Delete' View and Controller Action
First lets create the Delete route action in our controller. To do this we can just copy the action we made for Edit, and rename it to Delete. No other changes should be required.
public IActionResult Delete(int? id)
{
if (id == null || id == 0)
{
return NotFound();
}
DiaryEntry? objDiaryEntry = _db.DiaryEntries.Find(id);
if (objDiaryEntry == null)
{
return NotFound();
}
return View(objDiaryEntry);
}
Next lets create the Delete view.
Add a new file within DiaryEntries called Delete.cshtml. The view for this will be almost identical to our edit page, so copy and paste the code from there into the new file.
Then just change the title to see Edit instead of Delete, so the same for the asp-action tag helper, ane the button. Also change the css style for the button from btn-primary to btn-danger to give it a red colour.
@model DiaryEntry
<div class="container mt-4">
<div class="row">
<div class="col-md-8 offset-md-2">
<div class="card">
<div class="card-header bg-primary text-white">
<h2>Delete Diary Entry</h2>
</div>
<div class="card-body">
<form asp-action="Delete" method="post">
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group mb-3">
<label asp-for="Title" class="form-label">Title</label>
<input asp-for="Title" class="form-control" placeholder="Enter a title"/>
<span asp-validation-for="Title" class="text-danger"></span>
</div>
<div class="form-group mb-3">
<label asp-for="Content" class="form-label">Content</label>
<input asp-for="Content" aria-owns="5" class="form-control" placeholder="Enter what you did that day"/>
<span asp-validation-for="Content" class="text-danger"></span>
</div>
<div class="form-group mb-3">
<label asp-for="Created" class="form-label">Date</label>
<input asp-for="Created" class="form-control" type="date"/>
<span asp-validation-for="Created" class="text-danger"></span>
</div>
<div class="d-flex justify-content-between">
<button type="submit" class="btn btn-danger">Delete</button>
<a asp-controller="DiaryEntries" asp-action="Index" class="btn btn-secondary">Cancel</a>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@section Scripts{
<partial name="_ValidationScriptsPartial" />
}
Finally, we just need to update the navigation to the page by amending the button in the index page.
All we need to do here is add Delete as the value for the asp-action and to add an asp-route-id helper tag with a value of the entry id (the same as we have for the edit button)
<a class="btn btn-danger btn-sm" asp-controller="DiaryEntries" asp-action="Delete" asp-route-id="@entry.Id">
<i class="bi bi-trash3"></i> Delete
</a>
'Delete' Post Request
Now all that is left is to add the controller action to handle the post request for deleting an entry.
Again we can start by copying one of the post actions we already have for Edit or Create and naming the new method Delete. Then a few changes are required, we can remove any checking of the data as all we are doing is deleting. So all that is required is to call the database object with the Remove method, save the changes then redirect to the index page
[HttpPost]
public IActionResult Delete(DiaryEntry objDiaryEntry)
{
_db.DiaryEntries.Remove(objDiaryEntry);
_db.SaveChanges();
return RedirectToAction("Index");
}
And that's it! Our MVC application with a SQL database and a full CRUD functionality is now complete.