.NET Core MVC CRUD

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.


Introduction

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.


CRUD Requirements

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();

Key Features

  • Create: Add new transactions effortlessly, without navigating between multiple screens
  • Read: Access and view transactions with ease, all in one organized and intuitive interface
  • Update: Modify transactions quickly and conveniently
  • Delete: Safely remove transactions from the database with a single click

To access Transactions CRUD app, click the Transactions(CRUD) link in the left sidebar or add /Transactions to the URL.


Show Transactions

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 Index() 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 pb-0">
    <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 = "success";
              } else if (lowerCaseStatus == "due") {
                statusClass = "warning";
              } else if (lowerCaseStatus == "canceled") {
                statusClass = "danger";
              }
            }

            <tr>
              <td></td>
              <td>
                @Html.DisplayFor(modelItem => item.Id)
              </td>
              <td>
                @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 rounded-pill">
                  @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='bx bx-edit'></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="bx bx-trash"></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 Index() 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());
}

Add/Update transactions

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 {
...
}

<h4 class="py-3 mb-4">
  <span class="text-muted fw-light">Transactions /</span> Add Transaction
</h4>

<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-3">
        <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 Add 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 Update 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 {
...
}

<h4 class="py-3 mb-4">
  <span class="text-muted fw-light">Transactions /</span> Update Transaction
</h4>

<!-- Update Transactions Form -->
<div class="card">
  <div class="card-body">
    <form asp-action="Update" id="UpdateTransactionForm" method="post">
      ...
      <div class="mb-3">
        <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>
// 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);
}

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-3">
    <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-3">
    <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-3'
      }),
      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-3'
      }),
      submitButton: new FormValidation.plugins.SubmitButton(),

      defaultSubmit: new FormValidation.plugins.DefaultSubmit(),
      autoFocus: new FormValidation.plugins.AutoFocus()
    }
  });
}

Delete Transactions

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="bx bx-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 Delete(int? id) 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));
}

SQLite

Migration and Database setup

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/_InitialCreate.cs file, which creates the database.

Requirements

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"
  }
}
        

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);
}
        

FAQs

How to implement transactions CRUD in starter kit?

To set up CRUD in starter kit (AspNetCoreMvcStarter), consider the following steps:

  • Copy the given directories from /AspNetCoreMvcFull and paste in /AspNetCoreMvcStarter :
    1. /Data
    2. /Models
    3. /Views/Transactions
  • Copy /Controllers/TransactionsController.cs file & paste in /AspNetCoreMvcStarter/Controllers directory.
  • locate Program.cs in /AspnetCoreMvcStarter and then replace it with Program.cs file from AspNetCoreMvcFull.
  • Now, search and replace "Full" to "Starter" in all these files/directories added from /AspNetCoreMvcFull as well as in replaced Program.cs file.
  • locate _VerticalMenu.cshtml & _HorizontalMenu.cshtml files in /AspNetCoreMvcFull/Views/Shared/Sections/ & in them find Menu links for Transactions (CRUD).
  • Copy the menu links for Transactions (CRUD) from /AspNetCoreMvcFull/Views/Shared/Sections/ and paste them in /AspNetCoreMvcStarter/Views/Shared/Sections/ in _VerticalMenu.cshtml & _HorizontalMenu.cshtml files.
  • For seeding , initiation(creation) and migration of SQLite database run below given commands:
    dotnet ef migrations add InitialCreate
    dotnet ef database update
    
  • Running Migration command will create /Migrations in which its corresponding context files will be created whereas AspnetCoreMvcStarterContext.db file will be created in /AspnetCoreMvcStarter.
  • AspnetCoreMvcStarterContext.db-shm & AspnetCoreMvcStarterContext.db-wal are sometimes generated along with database file as its SQLite database enhancement files which help in caching or storing data and enable smooth experience.
  • Download Db browser for SQLite at SQlite Browser to view & manage database ! Locate your database file from /AspnetCoreMvcStarter and you can operate the database from browser itself!

Now run your starter kit with dotnet run or dotnet watch and you will have CRUD in your starter kit!

© 2017- ThemeSelection, Hand-crafted & Made with ❤️