• 文章管理:发布、编辑、删除文章。
  • 分类管理:对文章进行分类。
  • 标签管理:为文章打上标签。
  • 评论系统:访客可以对文章进行评论。
  • 用户管理:博主可以登录进行管理。
  • 响应式设计:适配电脑和手机。

技术栈选择

我们选择现代、高性能且社区活跃的技术栈:

  • 后端框架ASP.NET Core 8.0 (或更高版本)
  • Web 框架MVC (Model-View-Controller) 模式,结构清晰,易于理解。
  • 数据库Entity Framework Core (EF Core) 作为 ORM,SQLite 作为数据库(轻量级,无需安装数据库服务器,非常适合个人博客)。
  • 前端样式Bootstrap 5Tailwind CSS,快速构建响应式布局,这里我们以 Bootstrap 5 为例,因为它开箱即用。
  • 身份认证ASP.NET Core Identity,用于用户登录和权限管理。

项目结构

一个标准的 ASP.NET Core MVC 项目结构如下:

MyPersonalBlog/
├── Controllers/          # 控制器,处理用户请求
│   ├── HomeController.cs
│   ├── BlogPostsController.cs
│   └── AccountController.cs
├── Models/              # 数据模型 (EF Core)
│   ├── BlogPost.cs
│   ├── Category.cs
│   ├── Tag.cs
│   ├── Comment.cs
│   └── ApplicationUser.cs (来自 Identity)
├── Views/               # 视图,展示 HTML 页面
│   ├── Home/
│   ├── BlogPosts/
│   ├── Account/
│   ├── Shared/          # 共享布局 _Layout.cshtml
│   │   └── _Layout.cshtml
│   └── _ViewStart.cshtml
├── Data/                # 数据库上下文
│   └── ApplicationDbContext.cs
├── wwwroot/             # 静态文件 (CSS, JS, 图片)
│   ├── css/
│   │   └── site.css
│   └── js/
├── appsettings.json     # 应用配置
├── Program.cs           # 应用入口点
└── MyPersonalBlog.csproj # 项目文件

核心代码实现

步骤 1:创建项目

  1. 打开终端,运行命令:
    dotnet new mvc -n MyPersonalBlog
    cd MyPersonalBlog
  2. 安装必要的 NuGet 包:
    dotnet add package Microsoft.EntityFrameworkCore.Sqlite
    dotnet add package Microsoft.EntityFrameworkCore.Tools
    dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
    dotnet add package Microsoft.AspNetCore.Identity.UI

步骤 2:创建数据模型

Models 文件夹中创建以下类:

BlogPost.cs

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace MyPersonalBlog.Models
{
    public class BlogPost
    {
        public int Id { get; set; }
        [Required]
        [StringLength(100)]
        public string Title { get; set; }
        [Required]
        [StringLength(200, MinimumLength = 10)]
        public string Slug { get; set; } // URL 友好的标题,如 "my-first-post"
        [Required]
        [Column(TypeName = "text")]
        public string Content { get; set; }
        public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
        public DateTime? UpdatedAt { get; set; }
        public int CategoryId { get; set; }
        public virtual Category Category { get; set; }
        public List<Tag> Tags { get; set; } = new List<Tag>();
        public List<Comment> Comments { get; set; } = new List<Comment>();
    }
}

Category.cs

using System.ComponentModel.DataAnnotations;
namespace MyPersonalBlog.Models
{
    public class Category
    {
        public int Id { get; set; }
        [Required]
        [StringLength(50)]
        public string Name { get; set; }
    }
}

Tag.cs

using System.ComponentModel.DataAnnotations;
namespace MyPersonalBlog.Models
{
    public class Tag
    {
        public int Id { get; set; }
        [Required]
        [StringLength(50)]
        public string Name { get; set; }
        public List<BlogPost> BlogPosts { get; set; } = new List<BlogPost>();
    }
}

Comment.cs

using System.ComponentModel.DataAnnotations;
namespace MyPersonalBlog.Models
{
    public class Comment
    {
        public int Id { get; set; }
        [Required]
        [StringLength(200)]
        public string Author { get; set; }
        [Required]
        [StringLength(200)]
        [EmailAddress]
        public string Email { get; set; }
        [Required]
        [Column(TypeName = "text")]
        public string Content { get; set; }
        public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
        public int BlogPostId { get; set; }
        public virtual BlogPost BlogPost { get; set; }
    }
}

步骤 3:配置数据库上下文

Data 文件夹中创建 ApplicationDbContext.cs

using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using MyPersonalBlog.Models;
namespace MyPersonalBlog.Data
{
    public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
            : base(options)
        {
        }
        public DbSet<BlogPost> BlogPosts { get; set; }
        public DbSet<Category> Categories { get; set; }
        public DbSet<Tag> Tags { get; set; }
        public DbSet<Comment> Comments { get; set; }
        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);
            // 配置多对多关系
            builder.Entity<BlogPost>()
                .HasMany(p => p.Tags)
                .WithMany(t => t.BlogPosts)
                .UsingEntity(j => j.ToTable("BlogPostTags"));
        }
    }
}

步骤 4:注册服务和数据库迁移

打开 Program.cs,在 var app = builder.Build(); 之前添加以下代码:

// 1. 添加数据库上下文
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection")));
// 2. 添加 Identity 服务
builder.Services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();
// 3. 添加服务
builder.Services.AddControllersWithViews();

然后在 appsettings.json 中添加数据库连接字符串:

{
  "ConnectionStrings": {
    "DefaultConnection": "Data Source=blog.db"
  },
  // ... 其他配置
}

在终端运行数据库迁移命令:

dotnet ef migrations add InitialCreate
dotnet ef database update

这会创建一个 blog.db 文件并生成所有必要的表。

步骤 5:创建控制器和视图

这是 MVC 的核心部分。

HomeController.cs (首页)

using Microsoft.AspNetCore.Mvc;
using MyPersonalBlog.Data;
using MyPersonalBlog.Models;
using Microsoft.EntityFrameworkCore;
namespace MyPersonalBlog.Controllers
{
    public class HomeController : Controller
    {
        private readonly ApplicationDbContext _context;
        public HomeController(ApplicationDbContext context)
        {
            _context = context;
        }
        public async Task<IActionResult> Index()
        {
            // 获取最新的5篇文章
            var latestPosts = await _context.BlogPosts
                .Include(p => p.Category)
                .OrderByDescending(p => p.CreatedAt)
                .Take(5)
                .ToListAsync();
            return View(latestPosts);
        }
    }
}

Views/Home/Index.cshtml

@model IEnumerable<MyPersonalBlog.Models.BlogPost>
@{
    ViewData["Title"] = "首页";
}
<div class="p-5 mb-4 bg-light rounded-3">
    <div class="container-fluid py-5">
        <h1 class="display-5 fw-bold">欢迎来到我的博客</h1>
        <p class="col-md-8 fs-4">这里记录了我的技术学习、生活感悟和项目经验,希望能对你有所帮助!</p>
        <a class="btn btn-primary btn-lg" asp-controller="BlogPosts" asp-action="Index">浏览文章</a>
    </div>
</div>
<h2 class="mb-4">最新文章</h2>
<div class="row">
    @foreach (var post in Model)
    {
        <div class="col-md-6 mb-4">
            <div class="card h-100">
                <div class="card-body">
                    <h5 class="card-title">
                        <a asp-controller="BlogPosts" asp-action="Details" asp-route-id="@post.Id" asp-route-slug="@post.Slug">@post.Title</a>
                    </h5>
                    <p class="card-text">@post.Content.Substring(0, Math.Min(post.Content.Length, 150))...</p>
                    <small class="text-muted">分类: @post.Category.Name | @post.CreatedAt.ToString("yyyy-MM-dd")</small>
                </div>
            </div>
        </div>
    }
</div>

BlogPostsController.cs (文章相关)

using Microsoft.AspNetCore.Mvc;
using MyPersonalBlog.Data;
using MyPersonalBlog.Models;
using Microsoft.EntityFrameworkCore;
namespace MyPersonalBlog.Controllers
{
    public class BlogPostsController : Controller
    {
        private readonly ApplicationDbContext _context;
        public BlogPostsController(ApplicationDbContext context)
        {
            _context = context;
        }
        // GET: /BlogPosts
        public async Task<IActionResult> Index()
        {
            var posts = _context.BlogPosts.Include(p => p.Category);
            return View(await posts.ToListAsync());
        }
        // GET: /BlogPosts/Details/5
        public async Task<IActionResult> Details(int? id, string slug)
        {
            if (id == null)
            {
                return NotFound();
            }
            var blogPost = await _context.BlogPosts
                .Include(p => p.Category)
                .Include(p => p.Tags)
                .Include(p => p.Comments)
                .FirstOrDefaultAsync(m => m.Id == id);
            if (blogPost == null)
            {
                return NotFound();
            }
            // 验证 Slug 是否匹配,防止恶意请求
            if (blogPost.Slug != slug)
            {
                return RedirectToAction("Details", new { id = blogPost.Id, slug = blogPost.Slug });
            }
            return View(blogPost);
        }
        // ... [博主登录后才能使用的] Create/Edit/Delete 方法 ...
    }
}

Views/BlogPosts/Details.cshtml (文章详情页)

@model MyPersonalBlog.Models.BlogPost
@{
    ViewData["Title"] = Model.Title;
}
<h1>@Model.Title</h1>
<p class="text-muted">分类: @Model.Category.Name | 发布于 @Model.CreatedAt.ToString("yyyy-MM-dd")</p>
<hr />
<div class="blog-post-content">
    @Html.Raw(Model.Content) // 使用 Raw 显示 HTML 格式的内容
</div>
<hr />
<h3>评论 (@Model.Comments.Count)</h3>
@foreach (var comment in Model.Comments)
{
    <div class="card mb-3">
        <div class="card-body">
            <h6 class="card-subtitle mb-2 text-muted">@comment.Author - @comment.CreatedAt.ToString("yyyy-MM-dd HH:mm")</h6>
            <p class="card-text">@comment.Content</p>
        </div>
    </div>
}
<!-- 添加评论的表单 -->
<h4>发表评论</h4>
<form asp-action="AddComment" asp-controller="BlogPosts" asp-route-id="@Model.Id" method="post">
    <div class="mb-3">
        <label for="author" class="form-label">昵称</label>
        <input type="text" class="form-control" id="author" name="Author" required>
    </div>
    <div class="mb-3">
        <label for="email" class="form-label">邮箱</label>
        <input type="email" class="form-control" id="email" name="Email" required>
    </div>
    <div class="mb-3">
        <label for="content" class="form-label">评论内容</label>
        <textarea class="form-control" id="content" name="Content" rows="3" required></textarea>
    </div>
    <button type="submit" class="btn btn-primary">提交评论</button>
</form>

BlogPostsController.cs 中添加评论方法

// POST: /BlogPosts/AddComment/5
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> AddComment(int id, [Bind("Author,Email,Content")] Comment comment)
{
    if (ModelState.IsValid)
    {
        comment.BlogPostId = id;
        comment.CreatedAt = DateTime.UtcNow;
        _context.Add(comment);
        await _context.SaveChangesAsync();
        return RedirectToAction("Details", new { id = id, slug = (await _context.BlogPosts.FindAsync(id)).Slug });
    }
    // 如果模型验证失败,返回详情页并显示错误
    var blogPost = await _context.BlogPosts
        .Include(p => p.Category)
        .Include(p => p.Tags)
        .Include(p => p.Comments)
        .FirstOrDefaultAsync(m => m.Id == id);
    return View("Details", blogPost);
}

美化与扩展

使用 Markdown 编辑器

为了更好的写作体验,可以使用 Markdown 作为文章内容格式,然后在前端渲染成 HTML。

  1. 后端:安装 Markdig NuGet 包。

    dotnet add package Markdig
  2. 保存:在 BlogPost 模型中,将 Content 字段类型改为 string,并直接存储 Markdown 字符串。

  3. 显示:在 Details.cshtml 中,使用 Markdig 渲染。

    @using Markdig;
    @* ... *@
    <div class="blog-post-content">
        @Html.Raw(Markdown.ToHtml(Model.Content))
    </div>

使用富文本编辑器

如果你更喜欢传统的富文本编辑器(如 TinyMCE, CKEditor):

  1. wwwroot 中引入编辑器的 JS 和 CSS 文件。
  2. 在创建/编辑文章的表单中,将 textarea 替换为编辑器的初始化代码。
  3. 在提交表单时,编辑器会将 HTML 内容填充到 textarea 中,你直接保存这个 HTML 字符串到数据库即可。

部署

完成开发后,你可以将项目部署到各种平台:

  • Windows:Azure App Service, IIS
  • Linux:Azure App Service, Docker 容器
  • 静态站点:可以导出为静态 HTML 文件(使用 Jekyll, Hugo 等工具),但 ASP.NET MVC 的动态功能(如评论)需要后端支持。

这个模板为你提供了一个功能完整、结构清晰的 ASP.NET Core 个人博客起点,你可以基于此进行扩展,

  • 文章归档:按月份或年份归档文章。
  • 搜索功能:集成全文搜索引擎。
  • RSS 订阅:生成 RSS feed。
  • 图片上传:集成 Azure Blob Storage 或本地存储。
  • SEO 优化:添加 sitemap.xmlrobots.txt

希望这个详细的指南能帮助你快速搭建起自己的 ASP.NET 个人博客!