Blog Management Application using ASP.NET Core MVC

Blog Management Application using ASP.NET Core MVC and EF Core

Let us develop a Real-time Blog Management Application using ASP.NET Core MVC and Entity Framework Core (EF Core). The Application will provide a robust and user-friendly platform for creating, managing, and interacting with blog content. It serves as a comprehensive solution for bloggers, content creators, and administrators to efficiently handle blog posts, categories, authors, and user comments. The following will be our Blog Management Index Page, which provides the options to manage the blog-related CRUD Operations:

Real-time Blog Management Application using ASP.NET Core MVC and Entity Framework Core (EF Core)

Key Features of Our Blog Management Application
CRUD Operations:
  • Blog Posts: Create, Read, Update, and Delete blog posts with rich content, images, and SEO metadata.
  • Categories: Manage blog categories to organize content effectively.
  • Comments: Enable user engagement through comments on blog posts.
User Interface:
  • Responsive Design: The application uses Bootstrap to ensure it is accessible and visually appealing across various devices and screen sizes.
  • Rich Text Editing: Integrates CKEditor for enhanced content creation and formatting capabilities.
  • Dynamic Navigation: Implements view components to display categories and other dynamic content across the application consistently.
Search and Filtering:
  • Title Search: Allows users to search for blog posts by title.
  • Category Filtering: Enables filtering of blog posts based on selected categories.
  • Pagination: Implements pagination to efficiently manage the display of large numbers of blog posts.
File Management:
  • Image Uploads: Facilitates the uploading and managing of featured images for blog posts, ensuring media content is handled securely and efficiently.
SEO Optimization:
  • Meta Tags: This feature allows authors to input SEO-related metadata such as meta title, meta description, and meta keywords to enhance search engine visibility.
  • Slug Generation: Generates unique, URL-friendly slugs for blog posts, improving link sharing and SEO.
Data Validation and Integrity:
  • Custom Validation Attributes: Implements custom validators (e.g., CommentTextValidator) to enforce business rules and maintain data quality.
  • Entity Relationships: Define clear relationships between entities (Authors, Categories, Blog Posts, Comments) to ensure data consistency.
Implementing the Blog Management Application using ASP.NET Core MVC:

Now, let us proceed and implement the above Blog Management Application using ASP.NET Core MVC and EF Core step by step, following the Industry Coding standard and proper exception handling and Validation for Data Integrity and Security.

Setting Up the Project

Create a new ASP.NET Core MVC project and name it BlogManagementApp. Once you create the Project, please add the Microsoft EntityFrameworkCore.SqlServer and Microsoft.EntityFrameworkCore.Tools Packages either using the Package Manager solution or by executing the following commands in the Package Manager Console.

  • Install-Package Microsoft.EntityFrameworkCore.SqlServer
  • Install-Package Microsoft.EntityFrameworkCore.Tools
Creating Models:

The Models represent the core data structures of our application. They define the properties and relationships of the data entities that our application will manage, such as authors, categories, blog posts, and comments. First, create a folder named Models in the project root directory if you have not yet created it. Inside the Models folder, we will create the following modes.

Author Model

Create a class file named Author.cs within the Models folder, then copy and paste the following code. The Author Model represents the authors who create blog posts within the application. We will use this model to store and manage information about authors. Enables associating blog posts with their respective authors, facilitating author-specific functionalities like listing all posts by an author.

using System.ComponentModel.DataAnnotations;

namespace BlogManagementApp.Models
{
    public class Author
    {
        public int Id { get; set; }

        [Required, StringLength(100)]
        public string Name { get; set; }

        [EmailAddress, Required]
        public string Email { get; set; }

        // Navigation Property
        public ICollection<BlogPost> BlogPosts { get; set; }
    }
}
Category Model

Create a class file named Category.cs within the Models folder, and copy and paste the following code. The Category Model defines the categories under which blog posts are classified. This model organizes blog posts into meaningful groups, enabling users to filter and browse posts by category.

using System.ComponentModel.DataAnnotations;

namespace BlogManagementApp.Models
{
    public class Category
    {
        public int Id { get; set; }

        [Required]
        [StringLength(100)]
        public string Name { get; set; }

        // Navigation property
        public ICollection<BlogPost> BlogPosts { get; set; }
    }
}
BlogPost Model

Create a class file named BlogPost.cs within the Models folder, and copy and paste the following code. This model represents individual blog posts created by authors. It serves as the primary entity for creating, displaying, editing, and managing blog content. It facilitates relationships with authors, categories, and comments, enabling comprehensive blog management features.

using System.ComponentModel.DataAnnotations;

namespace BlogManagementApp.Models
{
    public class BlogPost
    {
        public int Id { get; set; }

        [Required, StringLength(200)]
        public string Title { get; set; }

        [Required]
        public string Body { get; set; }

        public string? FeaturedImage { get; set; }

        [StringLength(150)]
        public string? MetaTitle { get; set; }

        [StringLength(300)]
        public string? MetaDescription { get; set; }

        [StringLength(250)]
        public string? MetaKeywords { get; set; }

        [StringLength(200)]
        public string? Slug { get; set; }
        public int Views { get; set; } = 0; // Initialize to 0

        public DateTime PublishedOn { get; set; } = DateTime.UtcNow;
        public DateTime? ModifiedOn { get; set; } = DateTime.UtcNow;
        
        [Required]
        public int? AuthorId { get; set; }  // Foreign Key
        public Author? Author { get; set; } // Navigation Property
       
        [Required]
        public int? CategoryId { get; set; } // Foreign Key
        public Category? Category { get; set; } // Navigation Property

        public ICollection<Comment>? Comments { get; set; }
    }
}
Comment Model

Create a class file named Comment.cs within the Models folder and then copy and paste the following code. It represents user comments on blog posts. This model allows users to engage with blog posts by adding comments. Enhances interactivity and community engagement within the blog.

using BlogManagementApp.ValidationAttributes;
using System.ComponentModel.DataAnnotations;

namespace BlogManagementApp.Models
{
    public class Comment
    {
        public int Id { get; set; }

        [Required, StringLength(100)]
        public string Name { get; set; }

        [Required, EmailAddress]
        public string Email { get; set; }

        [Required, StringLength(1000)]
        [CommentTextValidator] //We will create this Custom Validator
        public string Text { get; set; }

        public DateTime PostedOn { get; set; }

        // Foreign Key
        public int BlogPostId { get; set; }

        // Navigation Property
        public BlogPost? BlogPost { get; set; }
    }
}

Note: We will create the Custom Data validation attribute in the next step.

Creating Validation Attributes:

The Custom validation attributes ensure that the data entered by users meets specific criteria beyond standard validation rules. They enforce business logic and maintain data integrity. Next, create a folder named ValidationAttributes in the project root directory. Inside the ValidationAttributes folder, we will create all our Custom Data Annotation Attributes.

Custom CommentTextValidator Attribute:

So, create a class file named CommentTextValidator.cs within the ValidationAttributes folder and copy and paste the following code. The following CommentTextValidator will validate the content of comments to prevent inappropriate or prohibited language. This Custom validation attribute will ensure that comments adhere to community guidelines by filtering out offensive or inappropriate language. It enhances the quality of user-provided comments and maintains a respectful environment.

using System.ComponentModel.DataAnnotations;

namespace BlogManagementApp.ValidationAttributes
{
    public class CommentTextValidator : ValidationAttribute
    {
        private readonly HashSet<string> _blacklist;

        public CommentTextValidator()
        {
            // Initialize blacklist and whitelist
            // In a real application, consider loading these from a database or configuration
            _blacklist = new HashSet<string> { "badword1", "badword2", "badword3" };
        }

        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            var text = value as string;
            if (string.IsNullOrEmpty(text))
            {
                return ValidationResult.Success;
            }

            var words = text.ToLower().Split(' ', StringSplitOptions.RemoveEmptyEntries);
            foreach (var word in words)
            {
                if (_blacklist.Contains(word))
                {
                    return new ValidationResult($"The comment contains a prohibited word: {word}");
                }
            }

            return ValidationResult.Success;
        }
    }
}
Key Components:
  • _blacklist: A set of prohibited words that are not allowed in comments.
  • IsValid Method: Overrides the base validation method to check the comment text against the blacklist and whitelist.
Creating DbContext:

DbContext is the primary class for interacting with the database using Entity Framework Core. It manages the database connections, model configurations, and data operations. So, create a folder named Data at the project root directory. Inside the Data folder, we will create our DbContext.

BlogManagementDBContext

So, create a class file named BlogManagementDBContext.cs within the Data folder and copy and paste the following code. This class configures the database context for the blog management application, defining the database sets and relationships between models. It facilitates all database interactions, including CRUD (Create, Read, Update, Delete) operations for authors, categories, blog posts, and comments. Manages relationships and enforces data integrity through configurations and constraints.

using BlogManagementApp.Models;
using Microsoft.EntityFrameworkCore;

namespace BlogManagementApp.Data
{
    public class BlogManagementDBContext : DbContext
    {
        public BlogManagementDBContext(DbContextOptions<BlogManagementDBContext> options)
            : base(options)
        {
        }

        // Configure model relationships and constraints
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            // Ensure Slug is unique
            modelBuilder.Entity<BlogPost>()
                .HasIndex(b => b.Slug)
                .IsUnique();

            // Cascade delete comments when a blog post is deleted
            modelBuilder.Entity<Comment>()
                .HasOne(c => c.BlogPost)
                .WithMany(b => b.Comments)
                .HasForeignKey(c => c.BlogPostId)
                .OnDelete(DeleteBehavior.Cascade);

            // Seed Authors
            modelBuilder.Entity<Category>().HasData(
                new Category { Id = 1, Name = "C#" },
                new Category { Id = 2, Name = "ASP.NET Core" },
                new Category { Id = 3, Name = "SQL Server" },
                new Category { Id = 4, Name = "Java" }
                //You can add more categories as needed by extending the HasData method
            );

            // Seed Authors
            modelBuilder.Entity<Author>().HasData(
                new Author { Id = 1, Name = "Pranaya Rout", Email = "Pranaya.Rout@example.com" },
                new Author { Id = 2, Name = "Rakesh Kumar", Email = "Rakesh.Kumar@example.com" },
                new Author { Id = 3, Name = "Hina Sharma", Email = "Hina.Sharma@example.com" }
                //You can add more authors as needed by extending the HasData method
            );
        }

        public DbSet<Author> Authors { get; set; }
        public DbSet<BlogPost> BlogPosts { get; set; }
        public DbSet<Comment> Comments { get; set; }
        public DbSet<Category> Categories { get; set; }
    }
}
Key Components:
DbSets:
  • Authors, BlogPosts, Comments, Categories: Represent the tables in the database corresponding to each model.
OnModelCreating Method:
  • Configures model relationships and constraints, such as ensuring the uniqueness of the Slug in BlogPost.
  • Sets up cascade delete behavior for comments when a blog post is deleted.
  • Seeds initial data for Authors and Categories to populate the database with predefined entries.
Storing Connection String and Page Size in AppSettings.json file:

This appsettings.json file stores configuration settings for the application, such as logging levels, connection strings, and pagination settings. This file centralizes configuration settings, allowing easy management and modification without altering the codebase. So, modify the appsettings.json file as follows.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "BlogManagementDBConnection": "Server=LAPTOP-6P5NK25R\\SQLSERVER2022DEV;Database=BlogManagementDB;Trusted_Connection=True;TrustServerCertificate=True;"
  },
  "Pagination": {
    "PageSize": 3
  }
}

Key Sections:
  • Logging: Configures logging levels for the application, specifying different default and Microsoft-related logs levels.
  • AllowedHosts: Defines which hosts are allowed to access the application.
  • ConnectionStrings: Contains the connection string for BlogManagementDB, specifying the server, database name, and security settings.
  • Pagination: Defines the PageSize, determining how many blog posts are displayed per page.
Configuring DbContext in Program Class:

The Program Class sets up and configures the application’s services, middleware, and request pipeline. It initializes the application and ensures all necessary services are registered and configured correctly. In the below code, we register the database context with the dependency injection container using the connection string from appsettings.json. Please modify the Program class as follows.

using BlogManagementApp.Data;
using Microsoft.EntityFrameworkCore;

namespace BlogManagementApp
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            // Add services to the container.
            builder.Services.AddControllersWithViews();

            // Register BlogManagementDBContext
            builder.Services.AddDbContext<BlogManagementDBContext>(options =>
                options.UseSqlServer(builder.Configuration.GetConnectionString("BlogManagementDBConnection")));

            var app = builder.Build();

            // 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=BlogPosts}/{action=Index}/{id?}");

            app.Run();
        }
    }
}

It acts as the application’s entry point, setting up all necessary services and middleware. It also ensures that the application is correctly configured to handle incoming requests, serve content, and interact with the database.

Creating and Applying Migration:

So, open the Package Manager Console and Execute the Add-Migration and Update-Database commands as follows to generate the Migration file and then apply the Migration file to create the BlogManagementDB database and required tables based on our Models and BlogManagementDBContext class:

Blog Management Application using ASP.NET Core MVC and EF Core

Once you execute the above commands and verify the database, you should see the BlogManagementDB database with the required tables, as shown in the image below.

Blog Management Application using ASP.NET Core MVC and Entity Framework Core

Now, if you verify the Authors and Categories tables, then you will see the initial seed data as shown in the below image:

Blog Management Application Development using ASP.NET Core MVC and Entity Framework Core

Creating View Models:

ViewModels are specialized classes designed to encapsulate and transfer data between the controllers and the views. They often combine multiple models or include additional properties needed for rendering views. So, next, create a folder named ViewModels at the project root directory. Inside the ViewModels folder, we will create our view models.

BlogPostDetailsViewModel View Model

So, create a class file named BlogPostDetailsViewModel.cs within the ViewModels folder and copy and paste the following code. The BlogPostDetailsViewModel aggregates data needed to display the details of a single blog post along with a comment form. This model facilitates the display of a blog post’s details and provides the necessary structure for users to submit comments. Enhances the separation of concerns by isolating the data required for the details view.

using BlogManagementApp.Models;

namespace BlogManagementApp.ViewModels
{
    public class BlogPostDetailsViewModel
    {
        public BlogPost BlogPost { get; set; }
        public Comment Comment { get; set; }
    }
}
BlogPostsIndexViewModel View Model

So, create a class file named BlogPostsIndexViewModel.cs within the ViewModels folder and copy and paste the following code. The BlogPostsIndexViewModel encapsulates data required to display the blog post lists, including pagination and search/filtering options. This model provides the necessary data for listing blog posts with pagination and search capabilities. It enhances user experience by allowing efficient navigation and filtering of blog content.

using BlogManagementApp.Models;

namespace BlogManagementApp.ViewModels
{
    public class BlogPostsIndexViewModel
    {
        public List<BlogPost>? Posts { get; set; }

        // Pagination Properties
        public int? CurrentPage { get; set; }
        public int? TotalPages { get; set; }

        // Search Filter Properties
        public string? SearchTitle { get; set; }
        public int? SearchCategoryId { get; set; }
    }
}
CategoryPostsViewModel View Model

So, create a class file named CategoryPostsViewModel.cs within the ViewModels folder and copy and paste the following code. The CategoryPostsViewModel contains data needed to display blog posts filtered by a specific category and pagination details. It facilitates the display of blog posts filtered by category, enabling users to browse content within specific topics. Enhances content organization and user navigation within the application.

using BlogManagementApp.Models;

namespace BlogManagementApp.ViewModels
{
    public class CategoryPostsViewModel
    {
        public List<BlogPost>? Posts { get; set; }
        public int? CurrentPage { get; set; }
        public int? TotalPages { get; set; }
        public string? CategoryName { get; set; }
        public int? CategoryId { get; set; }
    }
}
Creating View Components.

View Components are reusable components that encapsulate both the rendering logic and the data retrieval for specific parts of a view. They promote modularity and reusability within the application’s UI. First, create a folder named ViewComponents in the project root directory. Inside the ViewComponents folder, we will create all the server-side files required for view components.

Server-Side Class File:

So, create a class file named CategoriesViewComponent.cs within the ViewComponents folder and then copy and paste the following code. This class retrieves and displays the list of blog categories typically used in the site’s navigation menu or sidebar. It provides a consistent and reusable way to display categories across different parts of the application and enhances the navigational structure by allowing users to easily access posts by category.

using BlogManagementApp.Data;
using BlogManagementApp.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace BlogManagementApp.ViewComponents
{
    public class CategoriesViewComponent : ViewComponent
    {
        private readonly BlogManagementDBContext _context;

        public CategoriesViewComponent(BlogManagementDBContext context)
        {
            _context = context;
        }

        public async Task<IViewComponentResult> InvokeAsync()
        {
            List<Category> categories = await _context.Categories.OrderBy(c => c.Name).ToListAsync();

            //View Location shuld be : Views/Shared/Components/Categories/Default.cshtml
            return View(categories); 
        }
    }
}
Key Components:
  • Constructor: Injects the BlogManagementDBContext to access category data from the database.
  • InvokeAsync Method: Asynchronously fetches all categories ordered by name and passes them to the corresponding view.
Client-Side View File:

Next, create a view file named Default.cshtml within the Views/Shared/Components/Categories folder, and copy and paste the following code. This View renders the HTML for displaying the list of categories fetched by the CategoriesViewComponent. That means it defines the visual representation of the categories, ensuring they are displayed correctly within the application’s layout. Integrates seamlessly with the server-side CategoriesViewComponent to provide dynamic and interactive navigation elements.

@* Views/Shared/Components/Categories/Default.cshtml *@
@model IEnumerable<Category>

@foreach (var category in Model)
{
    <li class="nav-item">
        <a class="nav-link @(ViewContext.RouteData.Values["action"].ToString() == "PostsByCategory" && ViewContext.RouteData.Values["id"].ToString() == category.Id.ToString() ? "active" : "")" href="@Url.Action("PostsByCategory", "BlogPosts", new { id = category.Id })">
            @category.Name
        </a>
    </li>
}
Creating Controllers:

Controllers handle incoming HTTP requests, process user input, interact with models and services, and return responses, typically in the form of views or data. They are the central point for implementing the application’s business logic. If you haven’t already, create a folder named Controllers in the project root directory. Inside the Controllers folder, we will create all our Controllers.

Blog Posts Controller

So, create an Empty MVC controller named BlogPostsController within the Controllers folder and copy and paste the following code. The BlogPostsController manages all operations related to blog posts, including listing, viewing details, creating, editing, and deleting posts. It is the central hub for all blog post-related functionalities, coordinating data retrieval, processing, and view rendering.

using BlogManagementApp.Data;
using BlogManagementApp.Models;
using BlogManagementApp.ViewModels;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
namespace BlogManagementApp.Controllers
{
public class BlogPostsController : Controller
{
private readonly BlogManagementDBContext _context;
private readonly IWebHostEnvironment _webHostEnvironment;
private readonly IConfiguration _configuration;
public BlogPostsController(BlogManagementDBContext context, IWebHostEnvironment webHostEnvironment, IConfiguration configuration)
{
_context = context;
_webHostEnvironment = webHostEnvironment;
_configuration = configuration;
}
public async Task<IActionResult> Index(string? searchTitle, int? searchCategoryId, int? pageNumber)
{
try
{
// 1. Fetch PageSize from appsettings.json, default to 10 if not set
int pageSize = _configuration.GetValue<int?>("Pagination:PageSize") ?? 10;
// 2. Fetch Categories for the Dropdown
var categories = await _context.Categories.OrderBy(c => c.Name).ToListAsync();
ViewBag.Categories = new SelectList(categories, "Id", "Name");
// 3. Initialize query
var postsQuery = _context.BlogPosts
.Include(b => b.Author) //Eager Loading
.Include(b => b.Category) //Eager Loading
.AsQueryable(); // The query is built but not executed yet
// 4. Apply Title filter if provided
if (!string.IsNullOrEmpty(searchTitle))
{
postsQuery = postsQuery.Where(b => b.Title.Contains(searchTitle));
}
// 5. Apply Category filter if provided
if (searchCategoryId.HasValue && searchCategoryId.Value != 0)
{
postsQuery = postsQuery.Where(b => b.CategoryId == searchCategoryId.Value);
}
// 6. Order by PublishedOn descending (recent first)
postsQuery = postsQuery.OrderByDescending(b => b.PublishedOn);
// 7. Fetch total count for pagination
int totalPosts = await postsQuery.CountAsync(); // Executes the query to get the total count
// 8. Calculate total pages
int totalPages = (int)Math.Ceiling(totalPosts / (double)pageSize);
totalPages = totalPages < 1 ? 1 : totalPages; // Ensure at least 1 page
// 9. Ensure pageNumber is within valid range
pageNumber = pageNumber.HasValue && pageNumber.Value > 0 ? pageNumber.Value : 1;
pageNumber = pageNumber > totalPages ? totalPages : pageNumber;
// 10. Fetch posts for the current page
var posts = await postsQuery
.Skip((pageNumber.Value - 1) * pageSize)
.Take(pageSize)
.ToListAsync(); //Execute the query to retrive only the records which required in the current page
// 11. Prepare ViewModel for Pagination
var viewModel = new BlogPostsIndexViewModel
{
Posts = posts,
CurrentPage = pageNumber.Value,
TotalPages = totalPages,
SearchTitle = searchTitle,
SearchCategoryId = searchCategoryId ?? 0,
};
return View(viewModel);
}
catch (Exception ex)
{
ViewBag.ErrorMessage = "Unable to load blog posts. Please try again later.";
return View("Error");
}
}
// GET: Blog Post by Slug
[HttpGet]
[Route("/blog/{slug}")]
public async Task<IActionResult> Details(string slug)
{
if (string.IsNullOrEmpty(slug))
{
ViewBag.ErrorMessage = "Slug not provided.";
return View("Error");
}
try
{
var blogPost = await _context.BlogPosts
.Include(b => b.Author)
.Include(b => b.Category)
.Include(b => b.Comments)
.FirstOrDefaultAsync(m => m.Slug == slug);
if (blogPost == null)
{
ViewBag.ErrorMessage = "Blog post not found.";
return View("Error");
}
// Increment Views
blogPost.Views = blogPost.Views + 1;
await _context.SaveChangesAsync();
// Set SEO meta tags
ViewBag.MetaDescription = blogPost.MetaDescription;
ViewBag.MetaKeywords = blogPost.MetaKeywords;
ViewBag.Title = blogPost.MetaTitle ?? blogPost.Title;
var viewModel = new BlogPostDetailsViewModel
{
BlogPost = blogPost,
Comment = new Comment()
};
return View(viewModel);
}
catch (Exception ex)
{
ViewBag.ErrorMessage = "An error occurred while loading the blog post details.";
return View("Error");
}
}
// GET: Categories/{id}/Posts
[HttpGet("/categories/{id}/posts")]
public async Task<IActionResult> PostsByCategory(int id, int? pageNumber)
{
var category = await _context.Categories.FindAsync(id);
if (category == null)
{
ViewBag.ErrorMessage = "Invalid Category";
return View("Error");
}
int pageSize = _configuration.GetValue<int?>("Pagination:PageSize") ?? 10;
var postsQuery = _context.BlogPosts
.Where(b => b.CategoryId == id)
.Include(b => b.Author)
.Include(b => b.Category)
.OrderByDescending(b => b.PublishedOn)
.AsQueryable();
int totalPosts = await postsQuery.CountAsync();
int totalPages = (int)Math.Ceiling(totalPosts / (double)pageSize);
totalPages = totalPages < 1 ? 1 : totalPages;
pageNumber = pageNumber.HasValue && pageNumber.Value > 0 ? pageNumber.Value : 1;
pageNumber = pageNumber > totalPages ? totalPages : pageNumber;
var posts = await postsQuery
.Skip((pageNumber.Value - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
var viewModel = new CategoryPostsViewModel
{
Posts = posts,
CurrentPage = pageNumber.Value,
TotalPages = totalPages,
CategoryName = category.Name,
CategoryId = category.Id
};
return View("CategoryPosts", viewModel);
}
// GET: BlogPosts/Create
public async Task<IActionResult> Create()
{
try
{
// Fetch authors and Categories for dropdown
ViewBag.Authors = await _context.Authors.ToListAsync();
ViewBag.Categories = await _context.Categories.OrderBy(c => c.Name).ToListAsync();
return View();
}
catch (Exception ex)
{
ViewBag.ErrorMessage = "Unable to load the create blog post form. Please try again later.";
return View("Error");
}
}
// POST: BlogPosts/Create
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(BlogPost blogPost, IFormFile? FeaturedImage)
{
if (ModelState.IsValid)
{
try
{
// Handle image upload (using a separate method to avoid duplication)
blogPost.FeaturedImage = await UploadFeaturedImageAsync(FeaturedImage) ?? blogPost.FeaturedImage;
// Generate or validate the slug
blogPost.Slug = string.IsNullOrEmpty(blogPost.Slug)
? await GenerateSlugAsync(blogPost.Title)
: blogPost.Slug;
// Ensure the slug is unique
if (await _context.BlogPosts.AnyAsync(b => b.Slug == blogPost.Slug))
{
ModelState.AddModelError("Slug", "The slug must be unique.");
}
else
{
_context.Add(blogPost);
await _context.SaveChangesAsync();
TempData["SuccessMessage"] = "Blog post added successfully.";
return RedirectToAction(nameof(Index));
}
}
catch (Exception ex)
{
ViewBag.ErrorMessage = "An error occurred while creating the blog post.";
return View("Error");
}
}
// If validation fails, reload authors and Categories for dropdown
ViewBag.Categories = await _context.Categories.OrderBy(c => c.Name).ToListAsync();
ViewBag.Authors = await _context.Authors.ToListAsync();
return View(blogPost);
}
// GET: BlogPosts/Edit/5
public async Task<IActionResult> Edit(int? id)
{
if (id == null)
{
ViewBag.ErrorMessage = "Blog post ID is missing.";
return View("Error");
}
try
{
var blogPost = await _context.BlogPosts.FindAsync(id);
if (blogPost == null)
{
ViewBag.ErrorMessage = "Blog post not found.";
return View("Error");
}
ViewBag.Categories = await _context.Categories.OrderBy(c => c.Name).ToListAsync();
ViewBag.Authors = await _context.Authors.ToListAsync();
return View(blogPost);
}
catch (Exception ex)
{
ViewBag.ErrorMessage = "An error occurred while loading the edit blog post form.";
return View("Error");
}
}
// POST: BlogPosts/Edit/5
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, BlogPost blogPost, IFormFile? FeaturedImage)
{
if (id != blogPost.Id)
{
return NotFound("Blog post ID mismatch.");
}
if (ModelState.IsValid)
{
try
{
var existingPost = await _context.BlogPosts.AsNoTracking().FirstOrDefaultAsync(b => b.Id == id);
if (existingPost == null)
{
return NotFound("Blog post not found.");
}
if (FeaturedImage != null && FeaturedImage.Length > 0)
{
blogPost.FeaturedImage = await UploadFeaturedImageAsync(FeaturedImage);
}
else
{
//If you to remove the Featured Image, then don't set this
blogPost.FeaturedImage = existingPost.FeaturedImage;
}
// Generate or validate the slug
blogPost.Slug = string.IsNullOrEmpty(blogPost.Slug)
? await GenerateSlugAsync(blogPost.Title)
: blogPost.Slug;
if (await _context.BlogPosts.AnyAsync(b => b.Slug == blogPost.Slug && b.Id != blogPost.Id))
{
ModelState.AddModelError("Slug", "The slug must be unique.");
}
else
{
_context.Update(blogPost);
await _context.SaveChangesAsync();
// Set success message
TempData["SuccessMessage"] = "Blog post updated successfully.";
return RedirectToAction(nameof(Index));
}
}
catch (Exception ex)
{
ViewBag.ErrorMessage = "An error occurred while updating the blog post.";
return View("Error");
}
}
ViewBag.Categories = await _context.Categories.OrderBy(c => c.Name).ToListAsync();
ViewBag.Authors = await _context.Authors.ToListAsync();
return View(blogPost);
}
// GET: BlogPosts/Delete/5
public async Task<IActionResult> Delete(int? id)
{
if (id == null)
{
ViewBag.ErrorMessage = "Blog post ID is missing.";
return View("Error");
}
try
{
var blogPost = await _context.BlogPosts
.Include(b => b.Author)
.FirstOrDefaultAsync(m => m.Id == id);
if (blogPost == null)
{
ViewBag.ErrorMessage = "Blog post not found.";
return View("Error");
}
return View(blogPost);
}
catch (Exception ex)
{
ViewBag.ErrorMessage = "An error occurred while loading the blog post for deletion.";
return View("Error");
}
}
// POST: BlogPosts/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
try
{
var blogPost = await _context.BlogPosts.FindAsync(id);
if (blogPost != null)
{
_context.BlogPosts.Remove(blogPost);
await _context.SaveChangesAsync();
// Set success message
TempData["SuccessMessage"] = "Blog post deleted successfully.";
}
return RedirectToAction(nameof(Index));
}
catch (Exception ex)
{
ViewBag.ErrorMessage = "An error occurred while deleting the blog post.";
return View("Error");
}
}
private async Task<string> UploadFeaturedImageAsync(IFormFile featuredImage)
{
if (featuredImage != null && featuredImage.Length > 0)
{
var uploadsFolder = Path.Combine(_webHostEnvironment.WebRootPath, "uploads");
if (!Directory.Exists(uploadsFolder))
{
Directory.CreateDirectory(uploadsFolder);
}
var uniqueFileName = Guid.NewGuid().ToString() + "_" + featuredImage.FileName;
var filePath = Path.Combine(uploadsFolder, uniqueFileName);
using (var fileStream = new FileStream(filePath, FileMode.Create))
{
await featuredImage.CopyToAsync(fileStream);
}
return "/uploads/" + uniqueFileName;
}
return null;
}
private async Task<string> GenerateSlugAsync(string title)
{
// Slug generation with regex
var slug = System.Text.RegularExpressions.Regex.Replace(title.ToLowerInvariant(), @"\s+", "-").Trim();
// Ensure slug is unique by appending numbers if necessary
var uniqueSlug = slug;
int counter = 1;
while (await _context.BlogPosts.AnyAsync(b => b.Slug == uniqueSlug))
{
uniqueSlug = $"{slug}-{counter++}";
}
return uniqueSlug;
}
}
}
Key Methods:
Index:
  • Displays a paginated list of blog posts with optional search and category filters.
  • Facilitates browsing through blog posts, applying filters based on title and category, and navigating through different pages of posts.
Details:
  • This shows a detailed view of a single blog post identified by its slug. It increments the view count and sets SEO metadata.
  • Allows users to read the full content of a blog post and view associated comments.
PostsByCategory:
  • Displays blog posts filtered by a specific category with pagination support.
  • Enables users to browse posts within a selected category, enhancing content organization.
Create (GET):
  • Renders the form for creating a new blog post, including dropdowns for selecting authors and categories.
  • Provides the admin with the interface to compose and submit new blog posts.
Create (POST):
  • Handles the submission of the new blog post form, including image upload, slug generation, and validation.
  • Processes creating new blog posts, ensuring data integrity and user feedback upon success or failure.
Edit (GET):
  • Loads the form for editing an existing blog post, pre-populating it with current data.
  • Allows authors to modify existing blog posts, updating content, images, and metadata.
Edit (POST):
  • Processes the submission of the edit form, handling image updates, slug validation, and saving changes.
  • Manages updating blog posts, ensuring that modifications are correctly saved and validated.
Delete (GET):
  • Displays a confirmation view for deleting a specific blog post.
  • Provides a safety check before permanently removing a blog post from the system.
DeleteConfirmed (POST):
  • Executes the deletion of the specified blog post from the database.
  • Finalizes the removal of blog posts, ensuring that associated comments are handled appropriately.
Private Helper Methods (UploadFeaturedImageAsync, GenerateSlugAsync):
  • UploadFeaturedImageAsync: Handles the uploading and saving of featured images for blog posts.
  • GenerateSlugAsync: Creates a unique, URL-friendly slug based on the blog post’s title.
Comments Controller

So, create an Empty MVC controller named CommentsController within the Controllers folder and copy and paste the following code. The CommentsController handles the creation of comments on blog posts. It enables users to add comments to blog posts, providing interaction and engagement within the blog community. It also ensures that comments are properly validated and associated with their respective posts.

using BlogManagementApp.Data;
using BlogManagementApp.Models;
using BlogManagementApp.ViewModels;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace BlogManagementApp.Controllers
{
public class CommentsController : Controller
{
private readonly BlogManagementDBContext _context;
public CommentsController(BlogManagementDBContext context)
{
_context = context;
}
// POST: Comments/Create
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(int blogPostId, Comment comment)
{
if (ModelState.IsValid)
{
comment.BlogPostId = blogPostId;
comment.PostedOn = DateTime.UtcNow;
_context.Comments.Add(comment);
await _context.SaveChangesAsync();
return RedirectToAction("Details", "BlogPosts", new { slug = _context.BlogPosts?.Find(blogPostId)?.Slug });
}
// If validation fails, reload the blog post details with errors
var blogPost = await _context.BlogPosts
.Include(b => b.Author)
.Include(b => b.Comments)
.FirstOrDefaultAsync(b => b.Id == blogPostId);
if (blogPost == null)
{
ViewBag.ErrorMessage = "Blog Post Not Found";
return View("Error");
}
var viewModel = new BlogPostDetailsViewModel
{
BlogPost = blogPost,
Comment = comment
};
return View("../BlogPosts/Details", viewModel);
}
}
}
Create (POST) Method:

It processes the submission of a new comment associated with a specific blog post.

  • Validates the comment data using model validation and the custom CommentTextValidator.
  • Associate the comment with the relevant blog post and save it to the database.
  • It redirects users back to the blog post details view upon successful submission or reloads the view with errors if validation fails.
API Upload Controller

So, create an Empty API controller named UploadController within the Controllers folder and copy and paste the following code. The UploadController manages file uploads, specifically handling image uploads for blog posts and other content areas. It facilitates the uploading of images for blog posts, enhancing content richness and visual appeal. It also ensures that only valid image files are accepted and stored securely, maintaining the application’s integrity and security.

using Microsoft.AspNetCore.Mvc;
namespace BlogManagementApp.Controllers
{
[Route("file/[controller]")]
[ApiController]
public class UploadController : ControllerBase
{
private readonly IWebHostEnvironment _environment;
public UploadController(IWebHostEnvironment environment, ILogger<UploadController> logger)
{
_environment = environment;
}
//POST file/upload
[HttpPost]
[IgnoreAntiforgeryToken] // Disable CSRF for this endpoint
public async Task<IActionResult> FileUpload(IFormFile upload)
{
try
{
if (upload == null || upload.Length == 0)
{
return BadRequest(new { error = new { message = "No file uploaded." } });
}
// Validate file type (allow only images)
var allowedExtensions = new[] { ".jpg", ".jpeg", ".png", ".gif" };
var extension = Path.GetExtension(upload.FileName).ToLowerInvariant();
if (string.IsNullOrEmpty(extension) || !allowedExtensions.Contains(extension))
{
return BadRequest(new { error = new { message = "Invalid file type." } });
}
// Generate unique file name
var uniqueFileName = Guid.NewGuid().ToString() + extension;
var uploadsFolder = Path.Combine(_environment.WebRootPath, "uploads");
// Ensure the uploads folder exists
if (!Directory.Exists(uploadsFolder))
{
Directory.CreateDirectory(uploadsFolder);
}
var filePath = Path.Combine(uploadsFolder, uniqueFileName);
// Save the file
using (var stream = new FileStream(filePath, FileMode.Create))
{
await upload.CopyToAsync(stream);
}
var response = new
{
uploaded = true,
url = Url.Content($"~/uploads/{uniqueFileName}")
};
// Return the expected response format for CKEditor
return Ok(response);
// return Ok(new { uploaded = true, url = Url.Content($"~/uploads/{uniqueFileName}") });
}
catch (IOException ioEx)
{
return StatusCode(500, new { error = new { message = "An error occurred while saving the file. Please try again." } });
}
catch (Exception ex)
{
return StatusCode(500, new { error = new { message = "An unexpected error occurred. Please try again later." } });
}
}
}
}
FileUpload (POST) Method:

It handles the uploading of files, ensuring they meet specified criteria and are saved securely.

  • Validates that a file has been uploaded and checks its extension against allowed image formats (e.g., .jpg, .png).
  • Generates a unique filename to prevent conflicts and saves the file to the designated uploads directory.
  • Returns a response compatible with CKEditor, indicating success and providing the URL of the uploaded file.
Blog Management Application Views:

Now, let us proceed and create the required view for our Blog Management Application.

Index View

Create a view file named Index.cshtml within the Views/BlogPost folder and then copy and paste the following code. This view displays a paginated and filterable list of all blog posts, allowing users to search by title and filter by category. It also provides options to view, edit, or delete each blog post, as well as a button to create a new post.

@model BlogManagementApp.ViewModels.BlogPostsIndexViewModel
@{
ViewBag.Title = "Blog Posts";
}
<div class="container-fluid mt-4">
<partial name="_MessagesPartial" />
<!-- Search and Create Button Row -->
<div class="row mb-4">
<!-- Search Filter Form -->
<div class="col-lg-8">
<div class="card">
<div class="card-header bg-info text-white">
<h5 class="mb-0">Filter Blog Posts</h5>
</div>
<div class="card-body">
<form method="get" asp-action="Index" asp-controller="BlogPosts" class="row g-3">
<div class="col-md-5">
<div class="input-group">
<span class="input-group-text" id="searchTitleLabel">
<i class="bi bi-search"></i>
</span>
<input type="text" asp-for="SearchTitle" class="form-control" placeholder="Search by Title" aria-label="Search by Title" aria-describedby="searchTitleLabel" />
</div>
</div>
<div class="col-md-5">
<select asp-for="SearchCategoryId" class="form-select" asp-items="ViewBag.Categories" aria-label="Filter by Category">
<option value="0">-- All Categories --</option>
</select>
</div>
<div class="col-md-2 d-grid">
<button type="submit" class="btn btn-primary">
<i class="bi bi-filter"></i> Filter
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Create New Post Button -->
<div class="col-lg-4 d-flex align-items-end">
<a class="btn btn-success w-100" href="@Url.Action("Create")">
<i class="bi bi-plus-circle"></i> Create New Post
</a>
</div>
</div>
<!-- Blog Posts Table -->
<div class="card">
<div class="card-header bg-secondary text-white">
<h5 class="mb-0">Available Blog Posts</h5>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th scope="col">Title</th>
<th scope="col">Author</th>
<th scope="col">Published On</th>
<th scope="col">Views</th>
<th scope="col" class="text-center">Actions</th>
</tr>
</thead>
<tbody>
@if (Model.Posts != null && Model.Posts.Any())
{
foreach (var post in Model.Posts)
{
<tr>
<td>
<a href="@Url.Action("Details", new { slug = post.Slug })" class="text-decoration-none">
@post.Title
</a>
</td>
<td>@post.Author?.Name</td>
<td>@post.PublishedOn.ToLocalTime().ToString("MMMM dd, yyyy HH:mm")</td>
<td>@post.Views</td>
<td class="text-center">
<div class="btn-group" role="group" aria-label="Actions">
<a class="btn btn-info btn-sm" href="@Url.Action("Details", new { slug = post.Slug })" title="View">
<i class="bi bi-eye"></i> View
</a>
<a class="btn btn-warning btn-sm" href="@Url.Action("Edit", new { id = post.Id })" title="Edit">
<i class="bi bi-pencil-square"></i> Edit
</a>
<a class="btn btn-danger btn-sm" href="@Url.Action("Delete", new { id = post.Id })" title="Delete">
<i class="bi bi-trash"></i> Delete
</a>
</div>
</td>
</tr>
}
}
else
{
<tr>
<td colspan="5" class="text-center text-muted">No blog posts found.</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
<!-- Pagination -->
@if (Model.TotalPages > 1)
{
<nav aria-label="Blog Posts Pagination" class="mt-4">
<ul class="pagination justify-content-center">
<!-- First Page -->
<li class="page-item @(Model.CurrentPage == 1 ? "disabled" : "")">
<a class="page-link" href="@Url.Action("Index", new { pageNumber = 1, searchTitle = Model.SearchTitle, searchCategoryId = Model.SearchCategoryId })" aria-label="First">
<span aria-hidden="true">⏮️</span> First
</a>
</li>
<!-- Previous Page -->
<li class="page-item @(Model.CurrentPage == 1 ? "disabled" : "")">
<a class="page-link" href="@Url.Action("Index", new { pageNumber = Model.CurrentPage - 1, searchTitle = Model.SearchTitle, searchCategoryId = Model.SearchCategoryId })" aria-label="Previous">
<span aria-hidden="true">⬅️</span> Previous
</a>
</li>
<!-- Current Page -->
<li class="page-item active" aria-current="page">
<span class="page-link">
@Model.CurrentPage
<span class="visually-hidden">(current)</span>
</span>
</li>
<!-- Next Page -->
<li class="page-item @(Model.CurrentPage == Model.TotalPages ? "disabled" : "")">
<a class="page-link" href="@Url.Action("Index", new { pageNumber = Model.CurrentPage + 1, searchTitle = Model.SearchTitle, searchCategoryId = Model.SearchCategoryId })" aria-label="Next">
Next <span aria-hidden="true">➡️</span>
</a>
</li>
<!-- Last Page -->
<li class="page-item @(Model.CurrentPage == Model.TotalPages ? "disabled" : "")">
<a class="page-link" href="@Url.Action("Index", new { pageNumber = Model.TotalPages, searchTitle = Model.SearchTitle, searchCategoryId = Model.SearchCategoryId })" aria-label="Last">
Last <span aria-hidden="true">⏭️</span>
</a>
</li>
</ul>
</nav>
}
</div>
Key Features:
  • Filtering: Users can search for blog posts by entering a title and selecting a category from a dropdown. This helps them quickly locate specific posts or browse posts within a particular category.
  • Pagination: If there are more blog posts than the defined PageSize, pagination controls allow users to navigate through different pages of posts.
Actions:

For each blog post listed, users can:

  • View: Navigate to the detailed view of the post.
  • Edit: Modify the post’s content or metadata.
  • Delete: Remove the post from the system.
  • Create New Post: A button enables users to create a new blog post and directs them to the Create view.
Key Components:
  • Partial View _MessagesPartial: This view displays success or error messages based on user actions (e.g., post-creation and deletion).
  • Search and Filter Form: Users can input search criteria and select categories to filter the displayed posts.
  • Blog Posts Table: This table lists all available blog posts with essential details like title, author, publication date, views, and action buttons.
  • Pagination Controls: Facilitates navigation between different pages of blog posts when the total number exceeds the PageSize.
Create View:

Create a view file named Create.cshtml within the Views/BlogPost folder, and then copy and paste the following code. The Create View provides a form for users to create a new blog post, capturing all necessary details such as title, content, featured image, category, author, slug, and SEO metadata.

@model BlogManagementApp.Models.BlogPost
@{
ViewBag.Title = "Create Blog Post";
}
<div class="container my-5">
<h1 class="mb-4">Create Blog Post</h1>
<form asp-action="Create" enctype="multipart/form-data" method="post" novalidate>
<div class="row">
<div class="col-md-8">
<!-- Title Field -->
<div class="mb-4">
<label asp-for="Title" class="form-label">Title</label>
<input asp-for="Title" class="form-control" placeholder="Enter the blog post title" />
<div class="form-text">Provide a clear and concise title for your blog post.</div>
<span asp-validation-for="Title" class="text-danger"></span>
</div>
<!-- Content Field -->
<div class="mb-4">
<label asp-for="Body" class="form-label">Content</label>
<textarea asp-for="Body" class="form-control" id="editor" rows="10" placeholder="Write your content here..."></textarea>
<div class="form-text">Use the editor to format your blog post content.</div>
<span asp-validation-for="Body" class="text-danger"></span>
</div>
</div>
<div class="col-md-4">
<!-- Featured Image Field -->
<div class="mb-4">
<label asp-for="FeaturedImage" class="form-label">Featured Image</label>
<input asp-for="FeaturedImage" class="form-control" type="file" accept="image/*" />
<div class="form-text">Upload a high-resolution image to represent your blog post.</div>
<span asp-validation-for="FeaturedImage" class="text-danger"></span>
</div>
<!-- Category Field -->
<div class="mb-4">
<label asp-for="CategoryId" class="form-label">Category</label>
<select asp-for="CategoryId" class="form-select" asp-items="@(new SelectList(ViewBag.Categories, "Id", "Name"))">
<option value="">-- Select Category --</option>
</select>
<div class="form-text">Select the category that best fits your blog post.</div>
<span asp-validation-for="CategoryId" class="text-danger"></span>
</div>
<!-- Author Field -->
<div class="mb-4">
<label asp-for="AuthorId" class="form-label">Author</label>
<select asp-for="AuthorId" class="form-select" asp-items="@(new SelectList(ViewBag.Authors, "Id", "Name"))">
<option value="">-- Select Author --</option>
</select>
<div class="form-text">Choose the author who is writing this blog post.</div>
<span asp-validation-for="AuthorId" class="text-danger"></span>
</div>
<!-- Slug Field -->
<div class="mb-4">
<label asp-for="Slug" class="form-label">Slug</label>
<input asp-for="Slug" class="form-control" placeholder="e.g., my-blog-post" />
<div class="form-text">A URL-friendly version of the title. It should be unique and lowercase.</div>
<span asp-validation-for="Slug" class="text-danger"></span>
</div>
<!-- Meta Title Field -->
<div class="mb-4">
<label asp-for="MetaTitle" class="form-label">Meta Title</label>
<input asp-for="MetaTitle" class="form-control" placeholder="SEO title" />
<div class="form-text">Provide a concise SEO title for better search engine visibility.</div>
<span asp-validation-for="MetaTitle" class="text-danger"></span>
</div>
<!-- Meta Description Field -->
<div class="mb-4">
<label asp-for="MetaDescription" class="form-label">Meta Description</label>
<input asp-for="MetaDescription" class="form-control" placeholder="SEO description" />
<div class="form-text">Write a brief description of the blog post for search engines.</div>
<span asp-validation-for="MetaDescription" class="text-danger"></span>
</div>
<!-- Meta Keywords Field -->
<div class="mb-4">
<label asp-for="MetaKeywords" class="form-label">Meta Keywords</label>
<input asp-for="MetaKeywords" class="form-control" placeholder="keyword1, keyword2" />
<div class="form-text">Enter relevant keywords separated by commas to improve SEO.</div>
<span asp-validation-for="MetaKeywords" class="text-danger"></span>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="d-flex justify-content-end mt-4">
<button type="submit" class="btn btn-success me-2">
<i class="bi bi-save"></i> Create
</button>
<a asp-action="Index" class="btn btn-secondary">
<i class="bi bi-x-circle"></i> Cancel
</a>
</div>
</form>
</div>
@section Styles {
<style>
.ck-editor__editable_inline {
min-height: 400px; /* Minimum height */
overflow-y: auto; /* Adds scrollbar if content exceeds max-height */
}
</style>
}
@section Scripts {
<partial name="_ValidationScriptsPartial" />
<script>
ClassicEditor
.create(document.querySelector('#editor'), {
ckfinder: {
uploadUrl: '/file/upload'
}
})
.catch(error => {
// Logs any errors during initialization
console.error(error);
});
</script>
}
Key Features:
  • Form Submission: Users fill out the form fields and submit to create a new blog post. The form handles file uploads for the featured image and integrates a rich text editor (CKEditor) for composing the post content.
  • Validation: Ensures all required fields are filled out correctly, displaying validation messages for errors.
  • SEO Optimization: This feature allows users to input SEO-related fields (MetaTitle, MetaDescription, MetaKeywords) to enhance the post’s visibility in search engines.
  • Rich Text Editor Integration: Enhances content creation with formatting tools provided by CKEditor.
Details View

Create a view file named Details.cshtml within the Views/BlogPost folder and then copy and paste the following code. The Details View displays the full details of a specific blog post, including its content, featured image, author information, publication date, and associated comments. It also provides a form for users to submit new comments.

@model BlogManagementApp.ViewModels.BlogPostDetailsViewModel
@{
ViewBag.Title = Model.BlogPost.Title;
}
<div class="container my-5">
<!-- Blog Post Content -->
<div class="row justify-content-center">
<div class="col-lg-10">
<article class="mb-5">
<h1 class="mb-3">@Model.BlogPost.Title</h1>
<div class="mb-3 text-muted">
<span><strong>Author:</strong> @Model.BlogPost.Author?.Name</span> |
<span><strong>Published On:</strong> @Model.BlogPost.PublishedOn.ToLocalTime().ToString("MMMM dd, yyyy hh:mm tt")</span> |
<span><strong>Views:</strong> @Model.BlogPost.Views</span>
</div>
@if (!string.IsNullOrEmpty(Model.BlogPost.FeaturedImage))
{
<div class="mb-4">
<img src="@Model.BlogPost.FeaturedImage" alt="Featured Image" class="img-fluid rounded shadow-lg">
</div>
}
<div class="content mb-5">
@Html.Raw(Model.BlogPost.Body)
</div>
</article>
<hr />
<!-- Comments Section -->
<section class="mb-5">
<h3 class="mb-4">Comments (@Model.BlogPost.Comments?.Count())</h3>
@if (Model.BlogPost.Comments != null && Model.BlogPost.Comments.Any())
{
@foreach (var comment in Model.BlogPost.Comments.OrderByDescending(c => c.PostedOn))
{
<div class="card mb-3">
<div class="card-body">
<div class="d-flex mb-2">
<div class="ms-3">
<h5 class="card-title mb-1">@comment.Name</h5>
<p class="text-muted small">@comment.PostedOn.ToLocalTime().ToString("MMMM dd, yyyy h:mm tt")</p>
</div>
</div>
<p class="card-text">@comment.Text</p>
</div>
</div>
}
}
else
{
<p>No comments yet. Be the first to comment!</p>
}
</section>
<!-- Comment Form -->
<section>
<h4 class="mb-4">Leave a Comment</h4>
<form asp-controller="Comments" asp-action="Create" method="post">
<input type="hidden" name="blogPostId" value="@Model.BlogPost.Id" />
<div class="row g-3">
<div class="col-md-6">
<label for="Name" class="form-label">Name</label>
<input asp-for="Comment.Name" class="form-control" placeholder="Your Name" />
<span asp-validation-for="Comment.Name" class="text-danger"></span>
</div>
<div class="col-md-6">
<label for="Email" class="form-label">Email</label>
<input asp-for="Comment.Email" class="form-control" type="email" placeholder="Your Email" />
<span asp-validation-for="Comment.Email" class="text-danger"></span>
</div>
</div>
<div class="mt-3">
<label for="Text" class="form-label">Comment</label>
<textarea asp-for="Comment.Text" class="form-control" rows="4" placeholder="Your Comment"></textarea>
<span asp-validation-for="Comment.Text" class="text-danger"></span>
</div>
<div class="mt-4">
<button type="submit" class="btn btn-primary">
<i class="bi bi-send-fill"></i> Submit Comment
</button>
</div>
</form>
</section>
</div>
</div>
</div>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
<script>
document.addEventListener("DOMContentLoaded", function () {
// Select all images within the content div
var contentImages = document.querySelectorAll('.content img');
contentImages.forEach(function (img) {
// Add Bootstrap's img-fluid class to make images responsive
img.classList.add('img-fluid');
// Optionally, add the 'rounded' class for consistent styling
img.classList.add('rounded');
});
});
</script>
}
Key Features:
  • Post Viewing: Users can read a blog post’s complete content, view its featured image, and view metadata like author and publication date.
  • Comments Section: This section displays all comments associated with the post, ordered by the most recent. It encourages user engagement by allowing visitors to add their own comments.
  • Comment Submission: Users can submit new comments directly from the post details page. The form handles validation and displays any errors related to comment submission.
  • SEO Integration: Uses meta tags for better search engine visibility based on the post’s SEO metadata.
  • Scripts for Image Responsiveness: Ensures images within the post content are responsive and styled consistently.
Edit View

Create a view file named Edit.cshtml within the Views/BlogPost folder and then copy and paste the following code. The Edit View provides a form for users to edit an existing blog post, allowing modifications to all aspects of the post, including content, featured image, category, author, slug, and SEO metadata.

@model BlogManagementApp.Models.BlogPost
@{
ViewBag.Title = "Edit Blog Post";
}
<div class="container my-5">
<h1 class="mb-4">Edit Blog Post</h1>
<form asp-action="Edit" asp-controller="BlogPosts" enctype="multipart/form-data" method="post" novalidate>
<input type="hidden" asp-for="Id" />
<input type="hidden" asp-for="PublishedOn" />
<div class="row">
<div class="col-md-8">
<!-- Title Field -->
<div class="mb-4">
<label asp-for="Title" class="form-label">Title</label>
<input asp-for="Title" class="form-control" placeholder="Enter the blog post title" />
<div class="form-text">Provide a clear and concise title for your blog post.</div>
<span asp-validation-for="Title" class="text-danger"></span>
</div>
<!-- Content Field -->
<div class="mb-4">
<label asp-for="Body" class="form-label">Content</label>
<textarea asp-for="Body" class="form-control" id="editor" rows="10" placeholder="Update your content here..."></textarea>
<div class="form-text">Use the editor to format your blog post content.</div>
<span asp-validation-for="Body" class="text-danger"></span>
</div>
</div>
<div class="col-md-4">
<!-- Featured Image Field -->
<div class="mb-4">
<label asp-for="FeaturedImage" class="form-label">Featured Image</label>
<input asp-for="FeaturedImage" class="form-control" type="file" accept="image/*" />
<div class="form-text">Upload a high-resolution image to represent your blog post.</div>
<span asp-validation-for="FeaturedImage" class="text-danger"></span>
@if (!string.IsNullOrEmpty(Model.FeaturedImage))
{
<div class="mt-3 text-center">
<img src="@Model.FeaturedImage" alt="Featured Image" class="img-thumbnail mb-2" style="max-width: 100%;" />
<p class="mb-0"><small>Current Image</small></p>
</div>
}
</div>
<!-- Category Field -->
<div class="mb-4">
<label asp-for="CategoryId" class="form-label">Category</label>
<select asp-for="CategoryId" class="form-select" asp-items="@(new SelectList(ViewBag.Categories, "Id", "Name"))">
<option value="">-- Select Category --</option>
</select>
<div class="form-text">Select the category that best fits your blog post.</div>
<span asp-validation-for="CategoryId" class="text-danger"></span>
</div>
<!-- Author Field -->
<div class="mb-4">
<label asp-for="AuthorId" class="form-label">Author</label>
<select asp-for="AuthorId" class="form-select" asp-items="@(new SelectList(ViewBag.Authors, "Id", "Name", Model.AuthorId))">
<option value="">-- Select Author --</option>
</select>
<div class="form-text">Choose the author who is writing this blog post.</div>
<span asp-validation-for="AuthorId" class="text-danger"></span>
</div>
<!-- Slug Field -->
<div class="mb-4">
<label asp-for="Slug" class="form-label">Slug</label>
<input asp-for="Slug" class="form-control" placeholder="e.g., my-blog-post" />
<div class="form-text">A URL-friendly version of the title. It should be unique and lowercase.</div>
<span asp-validation-for="Slug" class="text-danger"></span>
</div>
<!-- Meta Title Field -->
<div class="mb-4">
<label asp-for="MetaTitle" class="form-label">Meta Title</label>
<input asp-for="MetaTitle" class="form-control" placeholder="SEO title" />
<div class="form-text">Provide a concise SEO title for better search engine visibility.</div>
<span asp-validation-for="MetaTitle" class="text-danger"></span>
</div>
<!-- Meta Description Field -->
<div class="mb-4">
<label asp-for="MetaDescription" class="form-label">Meta Description</label>
<input asp-for="MetaDescription" class="form-control" placeholder="SEO description" />
<div class="form-text">Write a brief description of the blog post for search engines.</div>
<span asp-validation-for="MetaDescription" class="text-danger"></span>
</div>
<!-- Meta Keywords Field -->
<div class="mb-4">
<label asp-for="MetaKeywords" class="form-label">Meta Keywords</label>
<input asp-for="MetaKeywords" class="form-control" placeholder="keyword1, keyword2" />
<div class="form-text">Enter relevant keywords separated by commas to improve SEO.</div>
<span asp-validation-for="MetaKeywords" class="text-danger"></span>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="d-flex justify-content-end mt-4">
<button type="submit" class="btn btn-primary me-2">
<i class="bi bi-save"></i> Save Changes
</button>
<a asp-action="Index" class="btn btn-secondary">
<i class="bi bi-x-circle"></i> Cancel
</a>
</div>
</form>
</div>
@section Styles {
<style>
.ck-editor__editable_inline {
min-height: 400px; /* Minimum height */
overflow-y: auto; /* Adds scrollbar if content exceeds max-height */
}
</style>
}
@section Scripts {
<partial name="_ValidationScriptsPartial" />
<script>
ClassicEditor
.create(document.querySelector('#editor'), {
ckfinder: {
uploadUrl: '/file/upload'
}
})
.catch(error => {
// Logs any errors during initialization
console.error(error);
});
</script>
}
Key Features:
  • Post Modification: Users can update the blog post’s title, content, and other details to keep the content current or make corrections.
  • Image Management: Allows users to upload a new featured image or retain the existing one.
  • Slug Management: Enables users to edit the slug, ensuring it remains unique and URL-friendly.
  • SEO Updates: Users can refine SEO-related fields to improve the post’s search engine performance.
  • Validation and Feedback: Ensures all changes adhere to validation rules, providing immediate feedback on any issues.
  • Hidden Fields: ID and PublishedOn are hidden inputs to maintain the post’s identity and original publication date.
  • Featured Image Display: Shows the current featured image with an option to upload a new one.
Delete View

Create a view file named Delete.cshtml within the Views/BlogPost folder, and copy and paste the following code. The Delete View Provides a confirmation interface for users to delete a specific blog post, ensuring that deletions are intentional and preventing accidental data loss. After deletion, the user is redirected with a success message or shown an error if the process fails.

@model BlogManagementApp.Models.BlogPost
@{
ViewBag.Title = "Delete Blog Post";
}
<h1 class="mb-4 text-danger">Delete Blog Post</h1>
<div class="card">
<div class="card-body">
<h4 class="card-title">@Model.Title</h4>
<p class="card-text">@Html.Raw(Model.Body)</p>
</div>
<div class="card-footer d-flex justify-content-between">
<form asp-action="Delete" asp-controller="BlogPosts" method="post">
<input type="hidden" asp-for="Id" />
<button type="submit" class="btn btn-danger">
<i class="bi bi-trash"></i> Delete
</button>
</form>
<a asp-action="Index" asp-controller="BlogPosts" class="btn btn-secondary">
<i class="bi bi-x-circle"></i> Cancel
</a>
</div>
</div>
CategoryPosts View:

Create a view file named CategoryPosts.cshtml within the Views/BlogPost folder, and then copy and paste the following code. This view displays a paginated list of all blog posts based on the selected category.

@model BlogManagementApp.ViewModels.CategoryPostsViewModel
@{
ViewBag.Title = "Posts in " + Model.CategoryName;
}
<div class="container mt-4">
<h2 class="mb-4">Posts in @Model.CategoryName</h2>
@if (Model.Posts == null || !Model.Posts.Any())
{
<div class="alert alert-warning text-center" role="alert">
No posts found in this category.
</div>
}
else
{
@foreach (var post in Model.Posts)
{
<div class="card mb-3 shadow-sm">
<div class="row g-0">
@if (!string.IsNullOrEmpty(post.FeaturedImage))
{
<div class="col-md-4">
<img src="@post.FeaturedImage" class="img-fluid rounded-start" alt="Featured Image">
</div>
<div class="col-md-8">
<div class="card-body">
<h5 class="card-title">@post.Title</h5>
<p class="card-text">@Html.Raw(post.Body.Length > 300 ? post.Body.Substring(0, 300) + "..." : post.Body)</p>
<p class="card-text">
<small class="text-muted">By @post.Author.Name on @post.PublishedOn.ToString("MMMM dd, yyyy")</small>
</p>
<a href="@Url.Action("Details", "BlogPosts", new { slug = post.Slug })" class="btn btn-primary">
<i class="bi bi-eye"></i> View More
</a>
</div>
</div>
}
else
{
<div class="col-md-12">
<div class="card-body">
<h5 class="card-title">@post.Title</h5>
<p class="card-text">@Html.Raw(post.Body.Length > 300 ? post.Body.Substring(0, 300) + "..." : post.Body)</p>
<p class="card-text">
<small class="text-muted">By @post.Author.Name on @post.PublishedOn.ToString("MMMM dd, yyyy")</small>
</p>
<a href="@Url.Action("Details", "BlogPosts", new { slug = post.Slug })" class="btn btn-primary">
<i class="bi bi-eye"></i> View More
</a>
</div>
</div>
}
</div>
</div>
}
<!-- Pagination -->
@if (Model.TotalPages > 1)
{
<nav aria-label="Category Posts Pagination">
<ul class="pagination justify-content-center">
<!-- First Page -->
<li class="page-item @(Model.CurrentPage == 1 ? "disabled" : "")">
<a class="page-link" href="@Url.Action("PostsByCategory", new { id = Model.CategoryId, pageNumber = 1 })" aria-label="First">
<span aria-hidden="true">⏮️</span> First
</a>
</li>
<!-- Previous Page -->
<li class="page-item @(Model.CurrentPage == 1 ? "disabled" : "")">
<a class="page-link" href="@Url.Action("PostsByCategory", new { id = Model.CategoryId, pageNumber = Model.CurrentPage - 1 })" aria-label="Previous">
<span aria-hidden="true">⬅️</span> Previous
</a>
</li>
<!-- Current Page -->
<li class="page-item active" aria-current="page">
<span class="page-link">
@Model.CurrentPage
<span class="visually-hidden">(current)</span>
</span>
</li>
<!-- Next Page -->
<li class="page-item @(Model.CurrentPage == Model.TotalPages ? "disabled" : "")">
<a class="page-link" href="@Url.Action("PostsByCategory", new { id = Model.CategoryId, pageNumber = Model.CurrentPage + 1 })" aria-label="Next">
Next <span aria-hidden="true">➡️</span>
</a>
</li>
<!-- Last Page -->
<li class="page-item @(Model.CurrentPage == Model.TotalPages ? "disabled" : "")">
<a class="page-link" href="@Url.Action("PostsByCategory", new { id = Model.CategoryId, pageNumber = Model.TotalPages })" aria-label="Last">
Last <span aria-hidden="true">⏭️</span>
</a>
</li>
</ul>
</nav>
}
}
</div>
Modifying _Layout View:

Modify the _Layout.cshtml view as follows. The _Layout view defines the overall layout and structure of the application’s web pages, including the header, navigation bar, main content area, and footer. It ensures a consistent look and feel across all views.

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>@ViewBag.Title - Blog Management App</title>
<meta name="description" content="@ViewBag.MetaDescription" />
<meta name="keywords" content="@ViewBag.MetaKeywords" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css">
<link rel="stylesheet" href="~/css/site.css" /> <!-- Optional: Your custom styles -->
</head>
<body class="d-flex flex-column min-vh-100">
<nav class="navbar navbar-expand-lg navbar-dark bg-primary shadow-sm">
<div class="container">
<a class="navbar-brand fw-bold" href="@Url.Action("Index", "BlogPosts")">
<i class="bi bi-journal-richtext me-2"></i> Blog Management
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<!-- Home menu item -->
<li class="nav-item">
<a class="nav-link @(ViewContext.RouteData.Values["action"].ToString() == "Index" && ViewContext.RouteData.Values["controller"].ToString() == "BlogPosts" ? "active" : "")" href="@Url.Action("Index", "BlogPosts")">
<i class="bi bi-house-door me-1"></i> Manage Blog
</a>
</li>
<!-- Invoke the Categories View Component to display each category as a menu item -->
@await Component.InvokeAsync("Categories")
</ul>
</div>
</div>
</nav>
<main class="flex-fill container mt-4">
@RenderBody()
</main>
<footer class="bg-light text-center text-lg-start mt-auto border-top">
<div class="text-center p-3">
© @DateTime.Now.Year Blog Management App. All rights reserved.
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<!-- Include CKEditor via CDN -->
<script src="https://cdn.ckeditor.com/ckeditor5/35.3.1/classic/ckeditor.js"></script>
<script src="~/lib/jquery/dist/jquery.min.js"></script>
@RenderSection("Scripts", required: false)
@RenderSection("Styles", required: false)
</body>
</html>
Key Features:
  • Navigation Bar: Includes links to different parts of the application, such as managing blog posts and browsing categories.
  • Dynamic Titles and Meta Tags: ViewBag dynamically sets the page title and SEO-related meta tags (MetaDescription, MetaKeywords) based on the current view.
  • Responsive Design: Bootstrap ensures the application is responsive and accessible on various devices.
  • Integration of View Components: Incorporates reusable components like the Categories view component to display categories in the navigation bar.
  • Footer: Includes a footer with copyright information.
  • Scripts and Styles: This function loads necessary CSS and JavaScript libraries, including Bootstrap and CKEditor, and provides sections for additional scripts and styles specific to individual views.
_MessagesPartial View:

Create a Partial view named _MessagesPartial.cshtml within the Views/Shared folder, then copy and paste the following code. This view displays temporary success or error messages to users based on their actions, such as creating, editing, or deleting blog posts. It provides users immediate feedback after performing actions like submitting a form or deleting a post.

@if (TempData["SuccessMessage"] != null)
{
<div class="alert alert-success alert-dismissible fade show" role="alert">
@TempData["SuccessMessage"]
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
}
@if (TempData["ErrorMessage"] != null)
{
<div class="alert alert-danger alert-dismissible fade show" role="alert">
@TempData["ErrorMessage"]
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
}
Key Components:

This View Can be included in multiple views to display messages across the application consistently.

  • Success Message: When TempData[“SuccessMessage”] is set, a green alert box indicates successful operations.
  • Error Message: When TempData[“ErrorMessage”] is set, a red alert box appears, indicating issues or failures.
  • Close Button: Allows users to dismiss the alert messages manually.
_ValidationScriptsPartialView:

Create a Partial view named _ValidationScriptsPartial.cshtml within the Views/Shared folder, then copy and paste the following code. This Partial view includes the client-side validation scripts to enable real-time form validation, enhancing user experience by providing immediate feedback on form inputs. This can be included in any view that contains forms requiring validation, promoting consistency, and reducing code duplication.

<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>
Key Components:
  • jQuery Validation Script: Adds functionality to validate form inputs based on defined rules.
  • Unobtrusive Validation Script: Bridges the gap between server-side validation attributes and client-side validation.
Error View:

Create a view named Error.cshtml within the Views/Shared folder and then copy and paste the following code. The Error View provides a standardized error page that informs users when an unexpected issue occurs within the application, ensuring errors are correctly handled. It displays a descriptive message about the error, either custom-provided via ViewBag.ErrorMessage or a default message.

@{
ViewBag.Title = "Error";
string errorMessage = ViewBag.ErrorMessage as string ?? "An unexpected error has occurred. Please try again later.";
}
<div class="container my-5">
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="alert alert-danger text-center" role="alert">
<h4 class="alert-heading">Oops!</h4>
<p>@errorMessage</p>
<hr>
<a href="@Url.Action("Index", "BlogPosts")" class="btn btn-primary">
<i class="bi bi-house-door-fill"></i> Go to Home
</a>
<a href="javascript:history.back()" class="btn btn-secondary">
<i class="bi bi-arrow-left-circle-fill"></i> Go Back
</a>
</div>
</div>
</div>
</div>

That’s it. We have completed our Blog Management Application Development using ASP.NET Core MVC and Entity Framework Core. Run the application and test the functionalities; it should work as expected. I hope this will give you a very good idea of how to develop a Blog Management Application using ASP.NET Core MVC and Entity Framework Core.

In the next article, I will discuss developing a Real-time e-commerce application using ASP.NET Core MVC. I hope you enjoy this in-depth article on Blog Management Application Development using ASP.NET Core MVC and EF Core. Please give your valuable feedback about this Blog Management Application using ASP.NET Core MVC and Entity Framework Core article and tell me how we can improve this project.

Leave a Reply

Your email address will not be published. Required fields are marked *