C# 中使用 EFCore 和 Sqlite 的全过程

86 阅读7分钟

本文介绍 WPF + EFCore + Sqlite 实现复杂 C# 项目的全过程,设计数据库连接,依赖注入,网络通信及 WebApi 应用的创建等内容。

1. 安装接口说明文档完成代码编写

接口说明文档

接口名称

AddRequest 接口

接口描述

该接口用于接收一个 AddRequest 对象,并将其存储到内部数据库中。如果操作成功,返回一个 AddResponse 对象,表示操作结果。

请求方法

  • HTTP 方法POST
  • URLhttp://localhost:5000/api/values

请求体

请求体是一个 JSON 格式的 AddRequest 对象,包含以下字段:

字段名类型是否必填描述
ISBNstring书籍的 ISBN 编号
namestring书籍名称
pricedecimal书籍价格
datestring日期
authorsAuthor[]作者信息数组

Author 对象结构

字段名类型是否必填描述
namestring作者姓名
sexstring作者性别
birthdaystring作者生日

请求示例

{
    "ISBN": "201",
    "name": "从开始到结束",
    "price": 1.000,
    "date": "123",
    "authors": null
}

响应体

响应体是一个 JSON 格式的 AddResponse 对象,包含以下字段:

字段名类型描述
resultstring操作结果,"S" 表示成功,"E" 表示失败
messagestring操作消息,描述操作结果
ISBNstring书籍的 ISBN 编号

响应示例

成功响应

{
    "result": "S",
    "message": "交易成功",
    "ISBN": "201"
}

失败响应

{
    "result": "E",
    "message": "交易失败",
    "ISBN": "201"
}

2. 完成后端代码的开发

1. 构建项目框架

首先创建一个新目录 mkdir webapi && cd webapi。使用命令 dotnet new webapi --no-https --framework netcoreapp3.1 新建一个 webapi 项目。

2. 安装必要的依赖

安装如下代码所示,安装配置需要的两个依赖:

<!-- webapi.csproj -->
<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.32" />
    <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
  </ItemGroup>
</Project>

3. 配置 webapi 服务器

// Startup.cs

using webapi.Utils.Json; // 自定义的 JSON 工具类
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace webapi
{
    public class Startup
    {
        // 构造函数,注入 IConfiguration 用于读取配置文件
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        // 用于存储配置信息的属性
        public IConfiguration Configuration { get; }

        // ConfigureServices 方法用于注册服务到依赖注入容器
        public void ConfigureServices(IServiceCollection services)
        {
            // 添加 MVC 服务并设置兼容性版本为 3.0
            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_3_0);

            // 添加 HttpContextAccessor 服务,用于在其他地方访问当前 HTTP 上下文
            services.AddHttpContextAccessor();

            // 添加控制器支持,并配置 JSON 序列化选项
            services.AddControllers(options => options.EnableEndpointRouting = false)
                .AddNewtonsoftJson(options =>
                {
                    // 使用自定义的 ContractResolver 来处理 JSON 序列化
                    options.SerializerSettings.ContractResolver = new LowerContractResolver();
                    // 设置日期格式
                    options.SerializerSettings.DateFormatString = "yyyy-MM-dd HH:mm:ss";
                });

            #region CORS
            // 配置跨域资源共享 (CORS)
            services.AddCors(c =>
            {
                // 添加一个 CORS 策略,允许任何来源、方法和头
                c.AddPolicy("Any", policy =>
                {
                    policy.AllowAnyOrigin()
                          .AllowAnyMethod()
                          .AllowAnyHeader();
                });
            });
            #endregion CORS
        }

        // Configure 方法用于配置 HTTP 请求管道
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            // 如果是开发环境,启用开发者异常页面
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            // 启用路由中间件
            app.UseRouting();

            // 启用授权中间件
            app.UseAuthorization();

            // 配置终结点
            app.UseEndpoints(endpoints =>
            {
                // 映射控制器
                endpoints.MapControllers();
            });
        }
    }
}

4. 构建 LowerContractResolver

首先创建根目录下面的 Json 目录,然后在其中创建 LowerContractResolver.cs

using Newtonsoft.Json.Serialization;
using System;

namespace webapi.Utils.Json
{
    public class LowerContractResolver : DefaultContractResolver
    {
        protected override string ResolvePropertyName(string propertyName)
        {
            return RenameCamelCase(propertyName);
        }

        private string RenameCamelCase(string str)
        {
            var firstChar = str[0];

            if (firstChar == char.ToLowerInvariant(firstChar))
            {
                return str;
            }

            var name = str.ToCharArray();
            name[0] = char.ToLowerInvariant(firstChar);

            return new String(name);
        }
    }
}

5. 创建 Model 层

在 Models 文件夹下面创建 Book.cs 文件:

using System.ComponentModel.DataAnnotations;

namespace webapi.Models
{
    public class AddRequest
    {
        [Required]
        public string ISBN;

        [Required]
        public string name;

        [Required]
        public decimal price;

        public string date;

    }

    public class Author
    {
        public string name;
        public string sex;
        public string birthday;
    }

    public class AddResponse
    {
        public string result;
        public string message;
        public string ISBN;
    }
}

6. 创建 Controller

在 Controllers 文件夹下面创建名为 ValuesController.cs 的文件:

using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using webapi.Models;

namespace webapi.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class ValuesController : ControllerBase
    {
        private static Dictionary<string, AddRequest> DB = new Dictionary<string, AddRequest>();

        [HttpPost]
        public AddResponse Post([FromBody] AddRequest req)
        {
            AddResponse resp = new AddResponse();

            try 
            {
                DB.Add(req.ISBN, req);
                resp.ISBN = req.ISBN;
                resp.message = "交易成功";
                resp.result = "S";
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex);
                resp.ISBN = req.ISBN;
                resp.message = "交易失败";
                resp.result = "E";
            }

            return resp;
        }
    }
}

这个文件之所以能够自动抽象 payload 并完成 Post 请求,得益于在 Startup.cs 中的各种配置,尤其是 Json 相关的配置。

7. 启动项目

如果使用的是 vscode 编辑器,就依次执行 Run -> Start Debugging -> C# 默认端口号为:5000.

3. 完成客户端代码开发

1. 构建项目框架

首先创建一个新目录 mkdir frontend && cd frontend。使用命令 dotnet new webapi --no-https --framework netcoreapp3.1 新建一个 webapi 项目。

2. 安装必要的依赖

安装如下代码所示,安装配置需要的两个依赖:

<!-- frontend.csproj -->
<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
  </ItemGroup>

</Project>

3. Program.cs 大改造

由于这个项目作为客户端,因此需要对 Program.cs 中的代码进行大的修改,删除其中不必要的代码,如下所示:

using System;
using System.IO;
using System.Net;
using System.Text;
using frontend.Models;
using Newtonsoft.Json;

namespace frontend
{
    public class Program
    {
        public static string Post (string url, string postData)
        {
            HttpWebRequest request = (HttpWebRequest) WebRequest.Create(url);
            request.ContentType = "application/json";
            request.Method = "POST";
            request.Timeout = 10000;

            byte[] bytes = Encoding.UTF8.GetBytes(postData);
            request.ContentLength = bytes.Length;
            Stream writer = request.GetRequestStream();
            writer.Write(bytes, 0, bytes.Length);
            writer.Close();

            HttpWebResponse response = (HttpWebResponse) request.GetResponse();
            StreamReader reader = new StreamReader(response.GetResponseStream() ?? throw new InvalidOperationException(), Encoding.UTF8);
            string result = reader.ReadToEnd();
            response.Close();

            return result;
        }
        
        public static void Main(string[] args)
        {
            string url = "http://localhost:5000/api/values";

            AddRequest req = new AddRequest {
                ISBN = "201",
                name = "从开始到结束",
                authors = null,
                date= "123",
                price = 1.000M
            };

            string req_str = JsonConvert.SerializeObject(req);

            string result = Post(url, req_str);
            Console.WriteLine($"[RESP]{result}");

            AddResponse resp = JsonConvert.DeserializeObject<AddResponse>(result);

            Console.WriteLine($"[RESP]{resp}");
        }
    }
}

4. 创建 Model 层

在 Models 文件夹下面创建 Book.cs 文件,其内容和服务端保持一致:

using Newtonsoft.Json;
namespace frontend.Models
{
    public class AddRequest
    {
        [JsonProperty("ISBN")]
        public string ISBN;
        
        [JsonProperty("name")]
        public string name;

        [JsonProperty("price")]
        public decimal price;

        [JsonProperty("date")]
        public string date;

         [JsonProperty("authors")]
        public Author[] authors;

    }

    public class Author
    {
        public string name;
        public string sex;
        public string birthday;
    }

    public class AddResponse
    {
        public string result;
        public string message;
        public string ISBN;
    }
}

虽然内容是相同的,但是 annotation 是不同的。

5. 启动项目

如果使用的是 vscode 编辑器,就依次执行 Run -> Start Debugging -> C#, 观察控制行的输出信息,就可以知道项目是否成功运行起来了。

4. 小结

整个前后端通信过程中,最难的点在于服务端的入参解析和返回值序列化。通过合理的配置,保证入参能够被正确的反序列化,返回值又能够被自动序列化是最关键的。需要牢记的点是:网络通信只能用字符串

5. 后端服务器 EntityFrame 和 Sqlite

1. 安装依赖

首先安装 5.0.17 版本的 EntityFrameworkCore, 如下所示:

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.32" />
    <PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.17" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.17">
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.17" />
    <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
  </ItemGroup>
</Project>

使用下面的脚本快速安装:

dotnet add package Microsoft.EntityFrameworkCore --version 5.0.17
dotnet add package Microsoft.EntityFrameworkCore.Design --version 5.0.17
dotnet add package Microsoft.EntityFrameworkCore.Sqlite --version 5.0.17

2. 配置链接

appsettings.json 中配置 sqlite 链接凭证:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "DefaultConnection": "Data Source=sqlite.db"
  }
}

3. 注入服务

Startup.cs 中注入服务类:

// 使用 SQLite 作为数据库提供程序
services.AddDbContext<Models.ApplicationDbContext>(options =>
    options.UseSqlite(Configuration.GetConnectionString("DefaultConnection"))
);

4. 构建要映射成数据库表的 Model

在 Models 目录下创建 Product.cs 文件:

namespace webapi.Models
{
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public decimal Price { get; set; }
    }
}

这实际上是约束了三个字段,对应到表中就是三个属性。

然后在 Models 目录下面创建一个名为 Storages 的目录,并创建名为 ApplicationDbContext.cs 的文件:

using Microsoft.EntityFrameworkCore;

namespace webapi.Models {
    public class ApplicationDbContext : DbContext
    {
        public DbSet<Product> Products { get; set; }

        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
            : base(options)
        {
        }
    }
}

5. 生成 sqlite 文件

appsettins.json 中,配置了 "DefaultConnection": "Data Source=sqlite.db" 这证明 sqlite.db 文件是在根目录下面的,但是我们不用手动创建这个文件,执行下面的脚本:

dotnet tool install --global dotnet-ef
dotnet ef migrations add InitialCreate
dotnet ef database update

上面的代码先安装了一个必要库,然后在根目录下生成了名为 sqlite.db 的文件,使用一些工具可以预览看到 db 文件中有一个名为 Products 的表,其中有 Id Name Price 三个字段,并且 Id 被自动指定成了 primary key.

6. 构建操作 Product Model 的 Controller

在 Controllers 目录下面创建名为 ProductsController.cs 的文件:

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using webapi.Models;

namespace webapi.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ProductsController : ControllerBase
    {
        private readonly ApplicationDbContext _context;

        public ProductsController(ApplicationDbContext context)
        {
            _context = context;
        }

        [HttpGet]
        public async Task<ActionResult<IEnumerable<Product>>> GetProducts()
        {
            return await _context.Products.ToListAsync();
        }

        [HttpGet("{id}")]
        public async Task<ActionResult<Product>> GetProduct(int id)
        {
            var product = await _context.Products.FindAsync(id);
            if (product == null)
            {
                return NotFound();
            }
            return product;
        }

        [HttpPost]
        public async Task<ActionResult<Product>> PostProduct(Product product)
        {
            _context.Products.Add(product);
            await _context.SaveChangesAsync();
            return CreatedAtAction(nameof(GetProduct), new { id = product.Id }, product);
        }

        [HttpPut("{id}")]
        public async Task<IActionResult> PutProduct(int id, Product product)
        {
            if (id != product.Id)
            {
                return BadRequest();
            }

            _context.Entry(product).State = EntityState.Modified;
            await _context.SaveChangesAsync();
            return NoContent();
        }

        [HttpDelete("{id}")]
        public async Task<IActionResult> DeleteProduct(int id)
        {
            var product = await _context.Products.FindAsync(id);
            if (product == null)
            {
                return NotFound();
            }

            _context.Products.Remove(product);
            await _context.SaveChangesAsync();
            return NoContent();
        }
    }
}

这个文件的逻辑相当简单:就是用注入的 ApplicationDbContext 实例操作 sqlite 数据库,并将各种操作封装成 Api 的过程。由于之前已经 debug 完了坑,因此这里很顺利就可以调通了。

7. 客户端使用 RestApi 和数据库通信

// Program.cs
...
public static void Main(string[] args)
{
    string url = "http://localhost:5000/api/products";

    Product req = new Product {
        Name = "New Product",
        Price = 99.99m
    };

    string req_str = JsonConvert.SerializeObject(req);

    string result = Post(url, req_str);
    Console.WriteLine($"[RESP]{result}");

    Task<ActionResult<Product>> resp = JsonConvert.DeserializeObject<Task<ActionResult<Product>>>(result);

    Console.WriteLine($"[RESP]{resp}");
    ...
}

8. Services 目录

来到 frontend 项目中,在根目录下面创建名为 Services 的子目录,并在其中创建 ApiService.cs 文件:

using System;
using System.IO;
using System.Net;
using System.Text;

namespace frontend.Services
{
    public static class ApiService
    {
        public static string Post(string url, string postData)
        {
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
            request.ContentType = "application/json";
            request.Method = "POST";
            request.Timeout = 10000;

            byte[] bytes = Encoding.UTF8.GetBytes(postData);
            request.ContentLength = bytes.Length;
            Stream writer = request.GetRequestStream();
            writer.Write(bytes, 0, bytes.Length);
            writer.Close();

            HttpWebResponse response = (HttpWebResponse)request.GetResponse();
            StreamReader reader = new StreamReader(response.GetResponseStream() ?? throw new InvalidOperationException(), Encoding.UTF8);
            string result = reader.ReadToEnd();
            response.Close();

            return result;
        }
    }
}

这样一来在需要使用的地方只需要先引用再调用就可以了:

using frontend.Services;
...

            string result = ApiService.Post(url, req_str);

7. 总结

到此,一个简单地 webapi 服务器就搭建完成了。