本文主要记录在 octans 类 .NET Web 项目中接入 EF Core 与 SQLite 的过程:安装包、定义 MediaItem / Movie / MediaFile、配置 AppDbContext(TPH、JSON 列表转换、唯一索引与 Value Comparer),以及 dotnet ef migrations 生成库文件。
接上文 创建 Web 应用与 MediaInfo 接入(项目可由 MediaManager 更名为 octans,命名空间以 octans 为准)。本文只覆盖电影 + 物理文件落库骨架,剧集实体可后续扩展。代码为脚手架示意,与线上仓库实现可能有差异。
背景说明
目标:
- 用 EF Core 管理 SQLite 文件库(开发阶段零独立数据库服务)。
- 逻辑媒体(标题、海报等)与 物理文件(路径、编码探针)分离:一部影片可对应多个文件(多版本/多码率)。
- 音轨、字幕等
List<string>以 JSON 形式落库,并对FilePath建唯一索引防重复扫描。
环境信息
| 项目 | 说明 |
|---|---|
| 项目 | ASP.NET Core Web(Razor Pages 等均可) |
| 命名空间 | octans.Models / octans.Data |
| ORM | EF Core + SQLite 提供程序 |
| 数据库文件 | 连接串示例 Data Source=octans.db(生成于工作目录) |
| CLI | 全局工具 dotnet-ef |
安装 NuGet 包
在项目根目录(含 .csproj)执行:
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
dotnet add package Microsoft.EntityFrameworkCore.Relational
dotnet add package Microsoft.EntityFrameworkCore.Design
安装成功后,.csproj 的 PackageReference 中应出现上述包名。
数据模型
在项目下建立 Models/ 目录。
MediaItem 基类
Models/MediaItem.cs:
using System.ComponentModel.DataAnnotations;
namespace octans.Models;
// 抽象基类:库中存 Movie 等具体类型,而非“纯 MediaItem”实例
public abstract class MediaItem
{
[Key]
public int Id { get; set; }
public int? TMDBId { get; set; }
[Required]
[MaxLength(200)]
public string Title { get; set; } = string.Empty;
[MaxLength(200)]
public string? OriginalTitle { get; set; }
public string? Overview { get; set; }
public DateTime? ReleaseDate { get; set; }
[MaxLength(500)]
public string? PosterPath { get; set; }
[MaxLength(500)]
public string? BackdropPath { get; set; }
public DateTime DateAdded { get; set; } = DateTime.UtcNow;
public int? CollectionId { get; set; }
}
要点:[Key] / [Required] / [MaxLength] 为数据注解;int? / string? 表示可空列。
Movie
Models/Movie.cs:
namespace octans.Models;
public class Movie : MediaItem
{
public string? Tagline { get; set; }
}
在 TPH(Table-Per-Hierarchy)下,EF 通常将继承层次映射到同一表,并用鉴别器列区分类型。
MediaFile(物理文件)
Models/MediaFile.cs:
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace octans.Models;
public class MediaFile
{
[Key]
public int Id { get; set; }
[Required]
public int MediaItemId { get; set; }
[ForeignKey(nameof(MediaItemId))]
public MediaItem? MediaItem { get; set; }
[Required]
[MaxLength(1000)]
public string FilePath { get; set; } = string.Empty;
public long FileSize { get; set; }
public long Duration { get; set; }
[MaxLength(50)]
public string? VideoFormat { get; set; }
[MaxLength(100)]
public string? VideoProfile { get; set; }
[MaxLength(100)]
public string? PixelFormat { get; set; }
public int Width { get; set; }
public int Height { get; set; }
[MaxLength(50)]
public string? FrameRate { get; set; }
public long BitRate { get; set; }
[MaxLength(50)]
public string? HdrDisplay { get; set; }
[MaxLength(50)]
public string? OriginalSourceMedium { get; set; }
[MaxLength(50)]
public string? ReleaseType { get; set; }
[MaxLength(50)]
public string? ContainerFormat { get; set; }
public List<string> AudioTracks { get; set; } = new();
public List<string> Subtitles { get; set; } = new();
public DateTime DateScanned { get; set; } = DateTime.UtcNow;
}
关系示意:一张逻辑影片一行 MediaItem/Movie;多个物理版本多行 MediaFile,共用同一 MediaItemId(1:N)。
AppDbContext
建立 Data/AppDbContext.cs。最终版本应包含 Value Comparer,避免对 List<string> 做 value conversion 后出现更新丢失与警告。
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using octans.Models;
using System.Text.Json;
namespace octans.Data;
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
{
}
public DbSet<MediaItem> MediaItems { get; set; }
public DbSet<Movie> Movies { get; set; }
public DbSet<MediaFile> MediaFiles { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<MediaItem>()
.HasDiscriminator<string>("MediaType")
.HasValue<Movie>("Movie");
var stringListComparer = new ValueComparer<List<string>>(
(c1, c2) => c1!.SequenceEqual(c2!),
c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
c => c.ToList());
modelBuilder.Entity<MediaFile>()
.Property(e => e.AudioTracks)
.HasConversion(
v => JsonSerializer.Serialize(v, (JsonSerializerOptions?)null),
v => JsonSerializer.Deserialize<List<string>>(v, (JsonSerializerOptions?)null) ?? new List<string>())
.Metadata.SetValueComparer(stringListComparer);
modelBuilder.Entity<MediaFile>()
.Property(e => e.Subtitles)
.HasConversion(
v => JsonSerializer.Serialize(v, (JsonSerializerOptions?)null),
v => JsonSerializer.Deserialize<List<string>>(v, (JsonSerializerOptions?)null) ?? new List<string>())
.Metadata.SetValueComparer(stringListComparer);
modelBuilder.Entity<MediaFile>()
.HasIndex(e => e.FilePath)
.IsUnique();
}
}
说明:
- TPH:
MediaType鉴别器区分Movie等。 - HasConversion:SQLite 侧用文本存 JSON。
- ValueComparer:变更跟踪能感知列表元素变化;缺少时
dotnet ef可能告警10620,且SaveChanges可能漏更新。 - 唯一索引:同一
FilePath不可重复入库。
连接串与依赖注入
appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"DefaultConnection": "Data Source=octans.db"
},
"MediaConfig": {
"MovieDirectory": "E:\\Movies",
"LogRawMediaInfo": false,
"SupportedVideoExtensions": [
".mkv", ".mp4", ".ts", ".m2ts", ".m2t",
".avi", ".rmvb", ".wmv", ".mov", ".flv", ".webm"
]
}
}
路径与扩展名按环境修改。数据库文件名可改为绝对路径;勿将生产密钥写入公开仓库。
Program.cs 注册
using Microsoft.EntityFrameworkCore;
using octans.Data;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlite(connectionString));
var app = builder.Build();
// 其余中间件与 MapRazorPages 等保持项目原有结构
关于 SQLite:无需单独部署实例
SQLite 为进程内、单文件引擎:NuGet 提供程序已带引擎库,程序直接读写 octans.db,不需要本机再装 PostgreSQL 式服务或监听端口。
开发期可用 VS Code 的 SQLite 插件、DB Browser for SQLite 或 DBeaver 打开文件查看表结构。多进程并发写仍有锁限制,生产高并发写场景应评估换 PostgreSQL 等 Client-Server 库。
迁移与建库
安装全局工具(本机一次)
dotnet tool install --global dotnet-ef
# 已安装时可:dotnet tool update --global dotnet-ef
添加迁移并更新数据库
在项目根目录:
dotnet ef migrations add InitialCreate
dotnet ef database update
成功后项目下出现 Migrations/,工作目录生成 octans.db(路径取决于连接串与启动目录)。控制台可能打印 CREATE TABLE 一类语句并以 Done. 结束。
若在未挂 Value Comparer 时已 migrations add,可能看到:
warn: ... The property 'MediaFile.AudioTracks' is a collection or enumeration type
with a value converter but with no value comparer. ...
warn: ... 'MediaFile.Subtitles' ...
处理:补全上文 AppDbContext 中的 comparer 后:
dotnet ef migrations remove
dotnet ef migrations add InitialCreate
dotnet ef database update
(若库已应用旧迁移,remove 前需确认未在生产使用;必要时删库文件重来,仅限可丢弃的开发库。)
验证结果
| 检查项 | 期望 |
|---|---|
| 包引用 | .csproj 含 Sqlite / Design 等 |
| 迁移 | Migrations 目录存在且无 10620 警告 |
| 库文件 | 存在 octans.db(或连接串指定路径) |
| 表结构 | 含媒体表与 MediaFiles;FilePath 为 UNIQUE |
| 鉴别器 | TPH 相关列存在(如 MediaType) |
可用 SQLite 客户端打开 .db 核对列名与索引。
注意事项
octans.db、*-shm、*-wal应加入.gitignore,避免误提交(见 Git untrack 相关文)。- 迁移是结构变更的版本化手段;多人协作需约定迁移提交顺序,勿随意改已上线迁移哈希。
- JSON 转换与 comparer 的组合是常见坑,改集合元素后务必实测
SaveChanges。 - 本文未接入扫描服务写入逻辑;下一步才是把扫描结果
Add/SaveChanges进库。