With Our .NET Core MVC Transactions CRUD Application, You can Create, Read, Update, or Delete transactional data with ease, providing a well-structured and intuitive experience for handling financial records.
Our ASP.NET Core MVC Transactions CRUD Application is a testament to the capabilities of the ASP.NET Core MVC framework. You're getting a robust foundation for building CRUD applications with great user interface.
Whether you're a newcomer to ASP.NET Core MVC or an experienced developer, this example application lets you focus on what matters most. Explore the code, customize it to your specific requirements, or build upon its streamlined structure.
AspNetCoreMvcFull/AspNetCoreMvcFullContext.db
.Program.cs
file.
NOTE: In AspNetCoreMvcFull.csproj
file all required tools and dependencies are added for Microsoft , Entity Core framework & their tools and SQLite database must be included for enabling dotnet to use them
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.SQLite" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="8.0.0" />
<PackageReference Include="System.Data.SQLite" Version="1.0.118" />
<None Update="AspnetCoreMvcFullContext.db">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
NOTE: Run the given CLI commands to install .NET packages and it would inject the packages in .csproj
file
dotnet add package Microsoft.EntityFrameworkCore
dotnet tool install --global dotnet-ef
dotnet tool install --global dotnet-aspnet-codegenerator
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.SQLite
dotnet add package Microsoft.EntityFrameworkCore.Tools
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet add package System.Data.SQLite
NOTE: SQLite connection with transaction context and SeedData Initialization Code is added in Program.cs
which enables a connection between Controller, Model, Context and database.
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using AspnetCoreMvcFull.Data;
using AspnetCoreMvcFull.Models;
var builder = WebApplication.CreateBuilder(args);
// Connect to the database
builder.Services.AddDbContext<AspnetCoreMvcFullContext>(options =>
options.UseSqlite(builder.Configuration.GetConnectionString("AspnetCoreMvcFullContext") ?? throw new InvalidOperationException("Connection string 'AspnetCoreMvcFullContext' not found.")));
builder.Services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
// Add services to the container.
builder.Services.AddControllersWithViews();
var app = builder.Build();
// Create a service scope to get an AspnetCoreMvcFullContext instance using DI and seed the database.
using (var scope = app.Services.CreateScope())
{
var services = scope.ServiceProvider;
SeedData.Initialize(services);
}
// Configure the HTTP request pipeline.
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();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Dashboards}/{action=Index}/{id?}"); // <-- Update in AspnetCoreMvcStarter
app.Run();
Models/Transactions.cs
to define the data structure and Controllers/TransactionsController.cs
to perform CRUD operations, handle form submissions, and interact with the database
through AspnetCoreMvcFullContext.cs
and finally provides UI from Index.cshtml
, Add.cshtml
& Update.cshtml
in Views/Transactions/
directory.
To access Transactions CRUD app, click the Transactions(CRUD)
link in the left sidebar or add /Transactions
to the URL.
AspNetCoreMvcFull -> AspnetCoreMvcFullContext.db
.
The first thing you will see is a list of existing transactions. We have used Datatable for our transactions table.
The data is seeded in database by Program.cs
and that data is shown by TransactionsController.cs
Controllers's public async Task
which gives transaction data and all other counts in widgets as well.
TransactionsToast.cs
in Models
is used to define structure for the toast messages generated for any updates in transactions table (added, updated or deleted).
Here you can see how we implemented datatable with .NET Core MVC CRUD App.
@* Transactions Table *@
<div class="card">
<div class="card-datatable table-responsive">
<table id="transactionsTable" class="table">
<thead class="border-top">
<tr class="text-nowrap">
<th></th>
<th>@Html.DisplayNameFor(model => model.Id)</th>
<th>
@Html.DisplayNameFor(model => model.Customer)
</th>
<th>
@Html.DisplayNameFor(model => model.TransactionDate)
</th>
<th>
@Html.DisplayNameFor(model => model.DueDate)
</th>
<th>
@Html.DisplayNameFor(model => model.Total)
</th>
<th>
@Html.DisplayNameFor(model => model.Status)
</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@if (Model?.Any() == true){
@foreach (var item in Model) {
string statusClass = "info"; // Default value if item.Status is null or doesn't match any case
if (!string.IsNullOrWhiteSpace(item.Status)) {
string lowerCaseStatus = item.Status.ToLower();
if (lowerCaseStatus == "paid") {
statusClass = "label-success";
} else if (lowerCaseStatus == "due") {
statusClass = "label-warning";
} else if (lowerCaseStatus == "canceled") {
statusClass = "label-danger";
}
}
<tr>
<td></td>
<td>
@Html.DisplayFor(modelItem => item.Id)
</td>
<td class="text-heading fw-medium">
@Html.DisplayFor(modelItem => item.Customer)
</td>
<td>
@item.TransactionDate.ToString("dd MMMM, yy")
</td>
<td>
@item.DueDate.ToString("dd MMMM, yy")
</td>
<td>
$ @item.Total
</td>
<td>
<div class="badge bg-@statusClass text-capitalize">
@Html.DisplayFor(modelItem => item.Status)
</div>
</td>
<td class="text-nowrap">
<a asp-action="Update" asp-route-id="@item.Id" class="btn btn-sm btn-icon shadow-none"><i class='ti ti-edit ti-md'></i></a>
<a href="/Transactions/Delete/@item.Id" class="btn btn-sm btn-icon shadow-none delete-transaction" data-transaction-username="@Html.DisplayFor(modelItem => item.Customer)"><i class="ti ti-trash ti-md"></i></a>
</td>
</tr>
}
}
</tbody>
</table>
</div>
</div>
We have used datatables
only to handle UI , export options
, pagination
, search
, and page length
whereas all data seeding , updating or deleting is done by TransactionsController.cs
Controller
Note: We have initialized datatables with column options to achieve better UI for Transactions app
The function public async Task
class lists the data in datatable from Sqlite database and therefore the data is shown
// GET: Transactions
public async Task Index()
{
// Calculate total transactions
int totalTransactions = await _context.Transactions.CountAsync();
var transactions = await _context.Transactions.ToListAsync();
var totalPaidTransactions = transactions
.Where(t => t.Status?.ToLower() == "paid")
.Sum(t => t.Total);
var totalDueTransactions = transactions
.Where(t => t.Status?.ToLower() == "due")
.Sum(t => t.Total);
var totalCanceledTransactions = transactions
.Where(t => t.Status?.ToLower() == "canceled")
.Sum(t => t.Total);
// Pass these counts to the view or perform further operations
ViewData["TotalTransactions"] = totalTransactions;
ViewData["TotalPaidTransactions"] = totalPaidTransactions;
ViewData["TotalDueTransactions"] = totalDueTransactions;
ViewData["TotalCanceledTransactions"] = totalCanceledTransactions;
return View(await _context.Transactions.ToListAsync());
}
You can add new ones by clicking the Add Transaction
button (above the table on the right). On the Add Transaction page, you will find a form that allows you to fill out the customer's name, transaction date, due Date, total and transaction status.
We have provided an update/edit icon in table, which leads to Update Transactions page, where existing transaction details can be updated.
@model AspnetCoreMvcFull.Models.Transactions
@{
ViewData["Title"] = "Add Transaction";
}
@section VendorStyles {
...
}
@section VendorScripts {
...
}
@section PageScripts {
...
}
<div class="card">
<div class="card-body">
<form asp-action="Add" id="addTransactionForm" method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
...
<div class="mb-4">
<button type="submit" value="Create" class="btn btn-primary me-sm-3 me-1">Create</button>
<a href="Index" value="Back" class="btn btn-secondary">Back</a>
</div>
</form>
</div>
</div>
On clicking , It would open New Transaction form which on submitting would call
public async Task
class that would add new transaction in the context, which is then updated to database.
On clicking , It would open Update Transaction page which would get selected transaction from context with help of its
id
and then display current data in the Update Transaction Form. On submit, Update form would call public async Task
class which would make updates to transaction data if any and send it to context, which saves it to database.
@model AspnetCoreMvcFull.Models.Transactions
@{
ViewData["Title"] = "Update Transaction";
}
@section VendorStyles {
...
}
@section VendorScripts {
...
}
@section PageScripts {
...
}
<!-- Update Transactions Form -->
<div class="card">
<div class="card-body">
<form asp-action="Update" id="UpdateTransactionForm" method="post">
...
<div class="mb-4">
<button type="submit" value="Save" class="btn btn-primary me-sm-3 me-1">Save</button>
<a href="/Transactions/Delete/@Model.Id" class="btn btn-danger me-sm-3 me-1 delete-transaction" data-transaction-username="@Model.Customer" >Delete</a>
<a asp-controller="Transactions" asp-action="Index" value="Back" class="btn btn-secondary">Back</a>
</div>
</form>
</div>
</div>
Add.cshtml
and Update.cshtml
for Add forms & Update forms respectively. Whereas for Delete operations, Sweet Alerts are used from transactions-delete.js
file included in Views/Transactions/Index.cshtml
file.
// POST: Transactions/Add
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Add([Bind("Id,Customer,TransactionDate,DueDate,Total,Status")] Transactions transactions)
{
if (ModelState.IsValid)
{
_context.Add(transactions);
await _context.SaveChangesAsync();
// Add success toast message for adding
SetSuccessToast("Added successfully", "bg-success");
TempData["DisplayToast"] = true;
return RedirectToAction(nameof(Index));
}
// Replace default error messages
ReplaceErrorMessage("TransactionDate", "Enter a valid Transaction Date");
ReplaceErrorMessage("DueDate", "Enter a valid Due Date");
ReplaceErrorMessage("Total", "Enter Total as a currency value");
return View(transactions);
}
// POST: Transactions/Update
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Update(int id, [Bind("Id,Customer,TransactionDate,DueDate,Total,Status")] Transactions transactions)
{
if (id != transactions.Id)
{
return NotFound();
}
// Set default dates to Today
if (transactions.TransactionDate == DateTime.MinValue)
{
transactions.TransactionDate = DateTime.Today;
ModelState.Remove("TransactionDate"); // Clear existing errors
}
// Set default dates to Tomorrow
if (transactions.DueDate == DateTime.MinValue)
{
transactions.DueDate = DateTime.Today.AddDays(1);
ModelState.Remove("DueDate"); // Clear existing errors
}
// Revalidate dates
if (transactions.DueDate <= transactions.TransactionDate)
{
ModelState.AddModelError("DueDate", "Due Date must be later than Transaction Date.");
}
if (ModelState.IsValid)
{
try
{
_context.Update(transactions);
await _context.SaveChangesAsync();
// Add success toast message for updating
SetSuccessToast("Updated successfully", "bg-info");
TempData["DisplayToast"] = true;
}
catch (DbUpdateConcurrencyException)
{
if (!TransactionsExists(transactions.Id))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction(nameof(Index));
}
// Replace default error messages
ReplaceErrorMessage("TransactionDate", "Enter a valid Transaction Date");
ReplaceErrorMessage("DueDate", "Enter a valid Due Date");
ReplaceErrorMessage("Total", "Enter Total as a currency value");
return View(transactions);
}
transactions-add-update.js
for Add/Update form-validations, transactions-list.js
for data-tables & toasts and transactions-delete.js
for Delete Sweet Alerts. These files are included in Add.cshtml
, Update.cshtml
& Index.cshtml
files respectively.
We are validating add/update forms using the Asp Validation(server-side) as well as formValidation javascript plugin. After successfully validating the form, add or update form is submitted and their respective functions are called!
Server-side validation in ASP.NET Core is implemented using the Model/Transactions.cs
file, working seamlessly with Controllers/TransactionsController.cs
& the default validation classes provided by ASP.NET Core.
using System;
using System.ComponentModel.DataAnnotations;
using System.Reflection;
namespace AspnetCoreMvcFull.Models
{
public class Transactions
{
public int Id { get; set; }
[StringLength(60, MinimumLength = 2, ErrorMessage = "Customer name must be between 2 and 60 characters")]
[Required(ErrorMessage = "Customer name is required")]
public string? Customer { get; set; }
[Display(Name = "Transaction Date")]
[DataType(DataType.Date)]
[Required(ErrorMessage = "Transaction Date is required")]
public DateTime TransactionDate { get; set; }
[Display(Name = "Due Date")]
[DateGreaterThan("TransactionDate", ErrorMessage = "Due Date must be later than Transaction Date")]
[DataType(DataType.Date)]
[Required(ErrorMessage = "Due Date is required")]
public DateTime DueDate { get; set; }
[DataType(DataType.Currency, ErrorMessage = "Total must be a currency value")]
[Required(ErrorMessage = "Total is required")]
public decimal? Total { get; set; }
[Required(ErrorMessage = "Status must be paid, due or canceled")]
public String? Status { get; set; }
}
}
// For validation of DueDate > TransactionDate
public class DateGreaterThanAttribute : ValidationAttribute
{
private readonly string _comparisonProperty;
public DateGreaterThanAttribute(string comparisonProperty)
{
_comparisonProperty = comparisonProperty;
}
protected override ValidationResult IsValid(object? value, ValidationContext validationContext)
{
ErrorMessage = ErrorMessageString;
var currentValue = (DateTime?)value;
var property = validationContext.ObjectType.GetProperty(_comparisonProperty);
if (property == null)
throw new ArgumentException("Property with this name not found");
var comparisonValue = (DateTime?)property.GetValue(validationContext.ObjectInstance);
if (currentValue <= comparisonValue)
return new ValidationResult(ErrorMessage);
return ValidationResult.Success!;
}
}
Here, attributes asp-validation-summary
& asp-validation-for
compare datatype & validations with the validations provided in transactions.cs
model for given field & validate it!
<form asp-action="Add" id="addTransactionForm" method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="mb-4">
<label asp-for="Customer" class="form-label"></label>
<input asp-for="Customer" class="form-control" placeholder="Customer Name"/>
<span asp-validation-for="Customer" class="text-danger"></span>
</div>
<div class="mb-4">
<button type="submit" value="Create" class="btn btn-primary me-sm-3 me-1">Create</button>
<a href="Index" value="Back" class="btn btn-secondary">Back</a>
</div>
</form>
JS for using form validation in Transactions CRUD .NET Core MVC is given below which is similar for Add/Update.
const addTransactionForm = document.getElementById('addTransactionForm');
if (addTransactionForm) {
// Add New Transaction Form Validation
fv = FormValidation.formValidation(addTransactionForm, {
fields: {
...
},
plugins: {
trigger: new FormValidation.plugins.Trigger(),
bootstrap5: new FormValidation.plugins.Bootstrap5({
eleValidClass: '',
rowSelector: '.mb-4'
}),
submitButton: new FormValidation.plugins.SubmitButton(),
defaultSubmit: new FormValidation.plugins.DefaultSubmit(),
autoFocus: new FormValidation.plugins.AutoFocus()
}
});
}
// Update transaction form validation
const UpdateTransactionForm = document.getElementById('UpdateTransactionForm');
if (UpdateTransactionForm) {
fv = FormValidation.formValidation(UpdateTransactionForm, {
fields: {
...
},
plugins: {
trigger: new FormValidation.plugins.Trigger(),
bootstrap5: new FormValidation.plugins.Bootstrap5({
eleValidClass: '',
rowSelector: '.mb-4'
}),
submitButton: new FormValidation.plugins.SubmitButton(),
defaultSubmit: new FormValidation.plugins.DefaultSubmit(),
autoFocus: new FormValidation.plugins.AutoFocus()
}
});
}
To delete the transaction, we have provided a delete icon.
We have used sweetalert2
to get the delete confirmation.
<a href="/Transactions/Delete/@item.Id" class="btn btn-sm btn-icon shadow-none delete-transaction" data-transaction-username="@Html.DisplayFor(modelItem => item.Customer)"><i class="ti ti-trash"></i></a>
const deleteButtons = document.querySelectorAll('.delete-transaction');
deleteButtons.forEach(deleteButton => {
deleteButton.addEventListener('click', function (e) {
e.preventDefault();
const userName = this.getAttribute('data-transaction-username');
Swal.fire({
title: 'Delete Transaction?',
html: `Are you sure you want to delete transaction of ?
${userName}
`,
...
}).then(result => {
if (result.isConfirmed) {
window.location.href = this.getAttribute('href'); //redirect to href
} else {
Swal.fire({
...
});
}
});
});
});
When the icon is clicked for deletion, a confirmation modal powered by SweetAlert is displayed.
Upon confirming the deletion, the
public async Task
class is invoked. This class retrieves the transaction based on its ID,
deletes the transaction, and then persists the changes to the database context.
// GET: Transactions/Delete
public async Task Delete(int? id)
{
if (id == null)
{
return NotFound();
}
var transactions = await _context.Transactions
.FirstOrDefaultAsync(m => m.Id == id);
if (transactions == null)
{
return NotFound();
}
else
{
_context.Transactions.Remove(transactions);
await _context.SaveChangesAsync();
// Add success toast message for deleting
SetSuccessToast("Deleted successfully", "bg-danger");
TempData["DisplayToast"] = true;
}
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
dotnet ef migrations add InitialCreate
dotnet ef database update
The migrations command generates code to create the initial database schema. The schema is based on the model specified in DbContext. The InitialCreate argument is used to name the migrations. Any name can be used, but by convention a name is selected that describes the migration.
The update command runs the Up method in migrations that have not been applied. In this case, update runs the Up method in the Migrations/
file, which creates the database.
Download SQLite database at SQlite.
Download Db browser for SQLite to view & manage database at SQlite Browser.
Here, ConnectionStrings => Data Source => Database Name
is set to Database name which is used in Program.cs
to establish connection between database and table allowing CRUD operations.
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"AspnetCoreMvcFullContext": "Data Source=AspnetCoreMvcFullContext.db"
}
}
Models/SeedData.cs
to seed data in database. You can also use Models/SeedData.cs
to seed custom data in your database.
Just make sure to change the SeedData.cs
file according to the database schema.
var builder = WebApplication.CreateBuilder(args);
// Connecting to database
builder.Services.AddDbContext(options =>
options.UseSqlite(builder.Configuration.GetConnectionString("AspnetCoreMvcFullContext") ?? throw new InvalidOperationException("Connection string 'AspnetCoreMvcFullContext' not found.")));
builder.Services.AddSingleton();
// Add services to the container.
builder.Services.AddControllersWithViews();
var app = builder.Build();
// To Seed Data
using (var scope = app.Services.CreateScope())
{
var services = scope.ServiceProvider;
SeedData.Initialize(services);
}
To set up CRUD in starter kit (AspNetCoreMvcStarter), consider the following steps:
/AspNetCoreMvcFull
and paste in /AspNetCoreMvcStarter
:
/Controllers/TransactionsController.cs
file & paste in /AspNetCoreMvcStarter/Controllers
directory.Program.cs
in /AspnetCoreMvcStarter and then replace it with Program.cs
file from AspNetCoreMvcFull./AspNetCoreMvcFull
as well as in replaced Program.cs
file./AspNetCoreMvcFull/Views/Shared/Sections/
& in them find Menu links for Transactions (CRUD)./AspNetCoreMvcFull/Views/Shared/Sections/
and paste them in /AspNetCoreMvcStarter/Views/Shared/Sections/
in _VerticalMenu.cshtml & _HorizontalMenu.cshtml files.dotnet ef migrations add InitialCreate
dotnet ef database update
/Migrations
in which its corresponding context files will be created whereas AspnetCoreMvcStarterContext.db
file will be created in /AspnetCoreMvcStarter
.
Now run your starter kit with dotnet run
or dotnet watch
and you will have CRUD in your starter kit!