创建一个基于 EF Core 和 SQLite 的完整 Demo,展示数据库连接、创建数据库、建表以及增删改查(CRUD)操作。该 Demo 将:
使用 EF Core 连接 SQLite 数据库。
自动创建数据库并定义表(基于 DeviceConfig 和 TestData 实体,适配你的项目)。
实现增删改查操作,集成仓储模式。
使用 LoggerHelper 记录操作日志,解决“日志没有保存成功”问题。
提供测试用例验证功能。
确保代码高效、稳定,与你的架构一致。
一、Demo 概述
功能:
连接 SQLite 数据库,自动创建 testsystem.db。
定义两个表:DeviceConfigs(设备配置)、TestData(测试数据)。
实现 CRUD 操作(增删改查)。
使用仓储模式解耦数据访问。
集成日志记录(基于 Common 层的 LoggerHelper)。
技术栈:
.NET 8.0
EF Core 8.0.4
SQLite
log4net(日志)
NUnit(测试)
项目结构:
SQLiteDemo
├── SQLiteDemo.Common
│ ├── LoggerHelper.cs
├── SQLiteDemo.Data
│ ├── Entities
│ │ ├── DeviceConfig.cs
│ │ ├── TestData.cs
│ ├── Repositories
│ │ ├── IDeviceConfigRepository.cs
│ │ ├── ITestDataRepository.cs
│ │ ├── DeviceConfigRepository.cs
│ │ ├── TestDataRepository.cs
│ ├── DataContext.cs
├── SQLiteDemo.Tests
│ ├── DataTests
│ │ ├── DeviceConfigRepositoryTests.cs
│ │ ├── TestDataRepositoryTests.cs
├── log4net.config
二、代码实现
以下是完整的 Demo 代码,包含数据库连接、建表、CRUD 操作和测试。
1. 项目文件
SQLiteDemo.Common.csproj:
xml
net8.0
SQLiteDemo.Data.csproj:
xml
net8.0
runtime; build; native; contentfiles; analyzers; buildtransitive
all
SQLiteDemo.Tests.csproj:
xml
net8.0
2. Common 层
LoggerHelper.cs:
csharp
// SQLiteDemo.Common/LoggerHelper.cs
using log4net;
using log4net.Config;
using Microsoft.Extensions.Logging;
using System;
using System.IO;
using System.Reflection;
namespace SQLiteDemo.Common
{
public static class LoggerHelper
{
private static bool _isInitialized;
public static ILogger CreateLogger(string categoryName)
{
if (!_isInitialized)
{
try
{
var repository = LogManager.CreateRepository(Assembly.GetEntryAssembly() ?? Assembly.GetCallingAssembly(), typeof(log4net.Repository.Hierarchy.Hierarchy));
XmlConfigurator.Configure(repository, new FileInfo("log4net.config"));
Directory.CreateDirectory("logs");
_isInitialized = true;
}
catch (Exception ex)
{
File.WriteAllText("logger_init_error.txt", ex.ToString());
throw;
}
}
return new Log4NetLogger(LogManager.GetLogger(categoryName));
}
public static void LogOperation(ILogger logger, string message, string context = "System")
{
logger?.LogInformation("[Context: {Context}] {Message}", context, message);
}
public static void LogException(ILogger logger, Exception ex, string operation, string context = "System")
{
logger?.LogError(ex, "[Context: {Context}] {Operation} failed: {Message}", context, operation, ex.Message);
}
public static void Flush()
{
LogManager.Flush(1000);
}
public static void Shutdown()
{
LogManager.Shutdown();
}
}
public class Log4NetLogger : ILogger
{
private readonly ILog _log;
public Log4NetLogger(ILog log)
{
_log = log;
}
public IDisposable BeginScope(TState state) => null;
public bool IsEnabled(LogLevel logLevel) => true;
public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter)
{
var message = formatter(state, exception);
switch (logLevel)
{
case LogLevel.Information:
_log.Info(message, exception);
break;
case LogLevel.Error:
_log.Error(message, exception);
break;
default:
_log.Debug(message, exception);
break;
}
}
}
}
log4net.config(根目录):
xml
设置 Copy to Output Directory: Copy always.
3. Data 层
DeviceConfig.cs:
csharp
// SQLiteDemo.Data/Entities/DeviceConfig.cs
using System.ComponentModel.DataAnnotations;
namespace SQLiteDemo.Data.Entities
{
public class DeviceConfig
{
[Key]
public string DeviceId { get; set; }
[Required]
public string Protocol { get; set; }
public string Host { get; set; } = "127.0.0.1";
public int Port { get; set; } = 502;
public DateTime LastUpdated { get; set; } = DateTime.UtcNow;
}
}
TestData.cs:
csharp
// SQLiteDemo.Data/Entities/TestData.cs
using System.ComponentModel.DataAnnotations;
namespace SQLiteDemo.Data.Entities
{
public class TestData
{
[Key]
public long Id { get; set; }
[Required]
public string DeviceId { get; set; }
public double Voltage { get; set; }
public double Temperature { get; set; }
public DateTime Timestamp { get; set; }
}
}
DataContext.cs:
csharp
// SQLiteDemo.Data/DataContext.cs
using Microsoft.EntityFrameworkCore;
using SQLiteDemo.Data.Entities;
namespace SQLiteDemo.Data
{
public class DataContext : DbContext
{
public DataContext(DbContextOptions options) : base(options) { }
public DbSet DeviceConfigs { get; set; }
public DbSet TestData { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// 配置主键和索引
modelBuilder.Entity()
.HasKey(d => d.DeviceId);
modelBuilder.Entity()
.HasKey(t => t.Id);
modelBuilder.Entity()
.HasIndex(t => t.DeviceId);
}
}
}
IDeviceConfigRepository.cs:
csharp
// SQLiteDemo.Data/Repositories/IDeviceConfigRepository.cs
using SQLiteDemo.Data.Entities;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace SQLiteDemo.Data.Repositories
{
public interface IDeviceConfigRepository
{
Task AddAsync(DeviceConfig config, CancellationToken cancellationToken = default);
Task GetByIdAsync(string deviceId, CancellationToken cancellationToken = default);
Task> GetAllAsync(CancellationToken cancellationToken = default);
Task UpdateAsync(DeviceConfig config, CancellationToken cancellationToken = default);
Task DeleteAsync(string deviceId, CancellationToken cancellationToken = default);
Task SaveChangesAsync(CancellationToken cancellationToken = default);
}
}
DeviceConfigRepository.cs:
csharp
// SQLiteDemo.Data/Repositories/DeviceConfigRepository.cs
using Microsoft.EntityFrameworkCore;
using SQLiteDemo.Data.Entities;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace SQLiteDemo.Data.Repositories
{
public class DeviceConfigRepository : IDeviceConfigRepository
{
private readonly DataContext _context;
public DeviceConfigRepository(DataContext context)
{
_context = context;
}
public async Task AddAsync(DeviceConfig config, CancellationToken cancellationToken = default)
{
await _context.DeviceConfigs.AddAsync(config, cancellationToken);
}
public async Task GetByIdAsync(string deviceId, CancellationToken cancellationToken = default)
{
return await _context.DeviceConfigs
.FirstOrDefaultAsync(c => c.DeviceId == deviceId, cancellationToken);
}
public async Task> GetAllAsync(CancellationToken cancellationToken = default)
{
return await _context.DeviceConfigs
.ToListAsync(cancellationToken);
}
public async Task UpdateAsync(DeviceConfig config, CancellationToken cancellationToken = default)
{
_context.DeviceConfigs.Update(config);
}
public async Task DeleteAsync(string deviceId, CancellationToken cancellationToken = default)
{
var config = await GetByIdAsync(deviceId, cancellationToken);
if (config != null)
{
_context.DeviceConfigs.Remove(config);
}
}
public async Task SaveChangesAsync(CancellationToken cancellationToken = default)
{
await _context.SaveChangesAsync(cancellationToken);
}
}
}
ITestDataRepository.cs:
csharp
// SQLiteDemo.Data/Repositories/ITestDataRepository.cs
using SQLiteDemo.Data.Entities;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace SQLiteDemo.Data.Repositories
{
public interface ITestDataRepository
{
Task AddAsync(TestData testData, CancellationToken cancellationToken = default);
Task GetByIdAsync(long id, CancellationToken cancellationToken = default);
Task> GetByDeviceIdAsync(string deviceId, CancellationToken cancellationToken = default);
Task UpdateAsync(TestData testData, CancellationToken cancellationToken = default);
Task DeleteAsync(long id, CancellationToken cancellationToken = default);
Task SaveChangesAsync(CancellationToken cancellationToken = default);
}
}
TestDataRepository.cs:
csharp
// SQLiteDemo.Data/Repositories/TestDataRepository.cs
using Microsoft.EntityFrameworkCore;
using SQLiteDemo.Data.Entities;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace SQLiteDemo.Data.Repositories
{
public class TestDataRepository : ITestDataRepository
{
private readonly DataContext _context;
public TestDataRepository(DataContext context)
{
_context = context;
}
public async Task AddAsync(TestData testData, CancellationToken cancellationToken = default)
{
await _context.TestData.AddAsync(testData, cancellationToken);
}
public async Task GetByIdAsync(long id, CancellationToken cancellationToken = default)
{
return await _context.TestData
.FirstOrDefaultAsync(t => t.Id == id, cancellationToken);
}
public async Task> GetByDeviceIdAsync(string deviceId, CancellationToken cancellationToken = default)
{
return await _context.TestData
.Where(t => t.DeviceId == deviceId)
.ToListAsync(cancellationToken);
}
public async Task UpdateAsync(TestData testData, CancellationToken cancellationToken = default)
{
_context.TestData.Update(testData);
}
public async Task DeleteAsync(long id, CancellationToken cancellationToken = default)
{
var testData = await GetByIdAsync(id, cancellationToken);
if (testData != null)
{
_context.TestData.Remove(testData);
}
}
public async Task SaveChangesAsync(CancellationToken cancellationToken = default)
{
await _context.SaveChangesAsync(cancellationToken);
}
}
}
4. 主程序
Program.cs:
csharp
// SQLiteDemo/Program.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using SQLiteDemo.Common;
using SQLiteDemo.Data;
using SQLiteDemo.Data.Entities;
using SQLiteDemo.Data.Repositories;
using System;
using System.Threading.Tasks;
namespace SQLiteDemo
{
class Program
{
static async Task Main(string[] args)
{
// 配置依赖注入
var services = new ServiceCollection();
services.AddDbContext(options =>
options.UseSqlite("Data Source=testsystem.db"));
services.AddScoped();
services.AddScoped();
services.AddSingleton(sp => LoggerHelper.CreateLogger("Demo"));
var serviceProvider = services.BuildServiceProvider();
// 确保数据库创建
using (var scope = serviceProvider.CreateScope())
{
var context = scope.ServiceProvider.GetRequiredService();
await context.Database.EnsureCreatedAsync();
}
// 获取仓储和日志
using (var scope = serviceProvider.CreateScope())
{
var deviceRepo = scope.ServiceProvider.GetRequiredService();
var testDataRepo = scope.ServiceProvider.GetRequiredService();
var logger = scope.ServiceProvider.GetRequiredService();
// Demo:增删改查
try
{
// 增:添加设备配置
var config = new DeviceConfig
{
DeviceId = "PS001",
Protocol = "ModbusTcp",
Host = "127.0.0.1",
Port = 502
};
await deviceRepo.AddAsync(config);
await deviceRepo.SaveChangesAsync();
LoggerHelper.LogOperation(logger, "Added device config PS001");
// 增:添加测试数据
var testData = new TestData
{
DeviceId = "PS001",
Voltage = 5.0,
Temperature = 25.0,
Timestamp = DateTime.UtcNow
};
await testDataRepo.AddAsync(testData);
await testDataRepo.SaveChangesAsync();
LoggerHelper.LogOperation(logger, "Added test data for PS001");
// 查:获取设备配置
var retrievedConfig = await deviceRepo.GetByIdAsync("PS001");
Console.WriteLine($"Retrieved Config: DeviceId={retrievedConfig.DeviceId}, Protocol={retrievedConfig.Protocol}");
LoggerHelper.LogOperation(logger, "Retrieved device config PS001");
// 查:获取测试数据
var testDataList = await testDataRepo.GetByDeviceIdAsync("PS001");
foreach (var data in testDataList)
{
Console.WriteLine($"TestData: Voltage={data.Voltage}, Timestamp={data.Timestamp}");
}
LoggerHelper.LogOperation(logger, "Retrieved test data for PS001");
// 改:更新设备配置
retrievedConfig.Port = 503;
await deviceRepo.UpdateAsync(retrievedConfig);
await deviceRepo.SaveChangesAsync();
LoggerHelper.LogOperation(logger, "Updated device config PS001");
// 删:删除测试数据
await testDataRepo.DeleteAsync(testData.Id);
await testDataRepo.SaveChangesAsync();
LoggerHelper.LogOperation(logger, "Deleted test data");
// 验证删除
var deletedData = await testDataRepo.GetByIdAsync(testData.Id);
Console.WriteLine(deletedData == null ? "Test data deleted successfully" : "Deletion failed");
}
catch (Exception ex)
{
LoggerHelper.LogException(logger, ex, "Demo Operation");
}
finally
{
LoggerHelper.Flush();
LoggerHelper.Shutdown();
}
}
}
}
}
5. 测试用例
DeviceConfigRepositoryTests.cs:
csharp
// SQLiteDemo.Tests/DataTests/DeviceConfigRepositoryTests.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Moq;
using NUnit.Framework;
using SQLiteDemo.Common;
using SQLiteDemo.Data;
using SQLiteDemo.Data.Entities;
using SQLiteDemo.Data.Repositories;
using System;
using System.Linq;
using System.Threading.Tasks;
namespace SQLiteDemo.Tests.DataTests
{
[TestFixture]
public class DeviceConfigRepositoryTests
{
private DataContext _context;
private IDeviceConfigRepository _repository;
private ILogger _logger;
private string _logFile;
[SetUp]
public void Setup()
{
var options = new DbContextOptionsBuilder()
.UseSqlite("Data Source=:memory:")
.Options;
_context = new DataContext(options);
_context.Database.OpenConnection();
_context.Database.EnsureCreated();
_repository = new DeviceConfigRepository(_context);
_logger = LoggerHelper.CreateLogger("DeviceConfigRepositoryTests");
_logFile = Path.Combine("logs", $"{DateTime.Now:yyyy-MM-dd}.log");
if (File.Exists(_logFile))
{
File.Delete(_logFile);
}
}
[TearDown]
public void TearDown()
{
_context.Database.CloseConnection();
_context.Dispose();
LoggerHelper.Flush();
LoggerHelper.Shutdown();
}
[Test]
public async Task AddAndGetAsync_SavesAndRetrievesConfig()
{
// Arrange
var config = new DeviceConfig
{
DeviceId = "PS001",
Protocol = "ModbusTcp",
Host = "127.0.0.1",
Port = 502
};
// Act
await _repository.AddAsync(config);
await _repository.SaveChangesAsync();
var retrieved = await _repository.GetByIdAsync("PS001");
// Assert
Assert.That(retrieved, Is.Not.Null);
Assert.That(retrieved.DeviceId, Is.EqualTo("PS001"));
Assert.That(retrieved.Protocol, Is.EqualTo("ModbusTcp"));
Assert.That(File.Exists(_logFile), Is.True);
}
[Test]
public async Task UpdateAsync_UpdatesConfig()
{
// Arrange
var config = new DeviceConfig
{
DeviceId = "PS001",
Protocol = "ModbusTcp",
Host = "127.0.0.1",
Port = 502
};
await _repository.AddAsync(config);
await _repository.SaveChangesAsync();
// Act
config.Port = 503;
await _repository.UpdateAsync(config);
await _repository.SaveChangesAsync();
var updated = await _repository.GetByIdAsync("PS001");
// Assert
Assert.That(updated.Port, Is.EqualTo(503));
Assert.That(File.Exists(_logFile), Is.True);
}
[Test]
public async Task DeleteAsync_RemovesConfig()
{
// Arrange
var config = new DeviceConfig
{
DeviceId = "PS001",
Protocol = "ModbusTcp"
};
await _repository.AddAsync(config);
await _repository.SaveChangesAsync();
// Act
await _repository.DeleteAsync("PS001");
await _repository.SaveChangesAsync();
var deleted = await _repository.GetByIdAsync("PS001");
// Assert
Assert.That(deleted, Is.Null);
Assert.That(File.Exists(_logFile), Is.True);
}
}
}
TestDataRepositoryTests.cs:
csharp
// SQLiteDemo.Tests/DataTests/TestDataRepositoryTests.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Moq;
using NUnit.Framework;
using SQLiteDemo.Common;
using SQLiteDemo.Data;
using SQLiteDemo.Data.Entities;
using SQLiteDemo.Data.Repositories;
using System;
using System.Linq;
using System.Threading.Tasks;
namespace SQLiteDemo.Tests.DataTests
{
[TestFixture]
public class TestDataRepositoryTests
{
private DataContext _context;
private ITestDataRepository _repository;
private ILogger _logger;
private string _logFile;
[SetUp]
public void Setup()
{
var options = new DbContextOptionsBuilder()
.UseSqlite("Data Source=:memory:")
.Options;
_context = new DataContext(options);
_context.Database.OpenConnection();
_context.Database.EnsureCreated();
_repository = new TestDataRepository(_context);
_logger = LoggerHelper.CreateLogger("TestDataRepositoryTests");
_logFile = Path.Combine("logs", $"{DateTime.Now:yyyy-MM-dd}.log");
if (File.Exists(_logFile))
{
File.Delete(_logFile);
}
}
[TearDown]
public void TearDown()
{
_context.Database.CloseConnection();
_context.Dispose();
LoggerHelper.Flush();
LoggerHelper.Shutdown();
}
[Test]
public async Task AddAndGetAsync_SavesAndRetrievesTestData()
{
// Arrange
var testData = new TestData
{
DeviceId = "PS001",
Voltage = 5.0,
Temperature = 25.0,
Timestamp = DateTime.UtcNow
};
// Act
await _repository.AddAsync(testData);
await _repository.SaveChangesAsync();
var retrieved = await _repository.GetByIdAsync(testData.Id);
// Assert
Assert.That(retrieved, Is.Not.Null);
Assert.That(retrieved.Voltage, Is.EqualTo(5.0));
Assert.That(File.Exists(_logFile), Is.True);
}
[Test]
public async Task GetByDeviceIdAsync_RetrievesCorrectData()
{
// Arrange
var testData = new TestData
{
DeviceId = "PS001",
Voltage = 5.0,
Temperature = 25.0,
Timestamp = DateTime.UtcNow
};
await _repository.AddAsync(testData);
await _repository.SaveChangesAsync();
// Act
var results = await _repository.GetByDeviceIdAsync("PS001");
// Assert
Assert.That(results.Count, Is.EqualTo(1));
Assert.That(results[0].DeviceId, Is.EqualTo("PS001"));
Assert.That(File.Exists(_logFile), Is.True);
}
}
}
三、运行和验证
创建项目:
bash
dotnet new sln -n SQLiteDemo
dotnet new classlib -n SQLiteDemo.Common -f net8.0
dotnet new classlib -n SQLiteDemo.Data -f net8.0
dotnet new nunit -n SQLiteDemo.Tests -f net8.0
dotnet new console -n SQLiteDemo -f net8.0
dotnet sln add SQLiteDemo.Common SQLiteDemo.Data SQLiteDemo.Tests SQLiteDemo
添加依赖:
bash
cd SQLiteDemo.Common
dotnet add package log4net
dotnet add package Microsoft.Extensions.Logging.Abstractions
cd ../SQLiteDemo.Data
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add reference ../SQLiteDemo.Common
cd ../SQLiteDemo.Tests
dotnet add package NUnit
dotnet add package NUnit3TestAdapter
dotnet add package Microsoft.NET.Test.Sdk
dotnet add package Microsoft.EntityFrameworkCore.InMemory
dotnet add reference ../SQLiteDemo.Data
cd ../SQLiteDemo
dotnet add reference ../SQLiteDemo.Data
复制代码:
将上述代码放入对应文件。
添加 log4net.config 到根目录。
运行程序:
bash
cd SQLiteDemo
dotnet run
输出示例:
Retrieved Config: DeviceId=PS001, Protocol=ModbusTcp
TestData: Voltage=5, Timestamp=2025-05-31T08:39:00
Test data deleted successfully
检查 testsystem.db 和 logs/2025-05-31.log.
运行测试:
bash
cd SQLiteDemo.Tests
dotnet test
四、解决日志问题
问题:日志未保存(“日志没有保存成功”)。 修复:
验证 log4net.config:
确保文件存在且复制到输出目录。
检查路径:
csharp
Console.WriteLine(File.Exists("log4net.config") ? "Config found" : "Config missing");
检查权限:
bash
mkdir logs
chmod -R 777 logs
调试初始化:
若失败,查看 logger_init_error.txt:
csharp
if (File.Exists("logger_init_error.txt"))
Console.WriteLine(File.ReadAllText("logger_init_error.txt"));
五、与 SemiconductorTestSystem 集成
复用实体:
DeviceConfig 和 TestData 与你的项目一致,可直接替换 SemiconductorTestSystem.Data.Entities。
仓储模式:
IDeviceConfigRepository 和 ITestDataRepository 可集成到 SemiconductorTestSystem.Data.Repositories。
DataContext:
更新 SemiconductorTestSystem.Data.DataContext:
csharp
services.AddDbContext(options =>
options.UseSqlite("Data Source=testsystem.db"));
日志:
使用现有 LoggerHelper,确保 log4net.config 配置一致。
六、高效、稳定实践
异步操作:
所有数据库操作使用 async/await。
事务管理:
csharp
using var transaction = await _context.Database.BeginTransactionAsync();
await _context.DeviceConfigs.AddAsync(config);
await _context.SaveChangesAsync();
await transaction.CommitAsync();
索引优化:
TestData.DeviceId 已添加索引,提高查询效率。
异常处理:
csharp
catch (DbUpdateException ex)
{
LoggerHelper.LogException(logger, ex, "Database operation");
throw;
}
七、调试建议
数据库连接:
验证 testsystem.db:
csharp
Console.WriteLine(File.Exists("testsystem.db") ? "Database exists" : "Database missing");
检查连接字符串:
csharp
Console.WriteLine("Connection: Data Source=testsystem.db");
数据验证:
查询表:
csharp
var configs = await _context.DeviceConfigs.ToListAsync();
configs.ForEach(c => Console.WriteLine(c.DeviceId));
日志验证:
检查日志:
csharp
var logContent = File.ReadAllText(_logFile);
Console.WriteLine(logContent);
八、注意事项
环境:
确保 .NET 8.0 SDK:
bash
dotnet --version
安装 SQLite:
bash
sudo apt-get install sqlite3 # Linux
依赖:
验证 NuGet 包:
bash
dotnet restore
测试:
使用 SQLite :memory: 避免文件依赖:
csharp
.UseSqlite("Data Source=:memory:")
九、排查问题
若有问题,提供:
错误信息(堆栈跟踪)。
logs/ 或 logger_init_error.txt。
环境(OS、.NET、IDE)。
数据库文件(testsystem.db)。
示例问题:
问题:数据库未创建。
排查:
验证 EnsureCreatedAsync:
csharp
Console.WriteLine(await _context.Database.EnsureCreatedAsync() ? "Created" : "Failed");
检查连接:
csharp
await _context.Database.OpenConnectionAsync();
请确认是否需要:
扩展功能(如批量插入)。
集成 SemiconductorTestSystem 的具体模块。
其他数据库(如 SQL Server)。
希望这个 Demo 能满足你的需求!