本文介绍 WPF + EFCore + Sqlite 实现复杂 C# 项目的全过程,设计数据库连接,依赖注入,网络通信及 WebApi 应用的创建等内容。
1. 安装接口说明文档完成代码编写
接口说明文档
接口名称
AddRequest
接口
接口描述
该接口用于接收一个 AddRequest
对象,并将其存储到内部数据库中。如果操作成功,返回一个 AddResponse
对象,表示操作结果。
请求方法
- HTTP 方法:
POST
- URL:
http://localhost:5000/api/values
请求体
请求体是一个 JSON 格式的 AddRequest
对象,包含以下字段:
字段名 | 类型 | 是否必填 | 描述 |
---|---|---|---|
ISBN | string | 是 | 书籍的 ISBN 编号 |
name | string | 是 | 书籍名称 |
price | decimal | 是 | 书籍价格 |
date | string | 是 | 日期 |
authors | Author[] | 否 | 作者信息数组 |
Author
对象结构:
字段名 | 类型 | 是否必填 | 描述 |
---|---|---|---|
name | string | 是 | 作者姓名 |
sex | string | 是 | 作者性别 |
birthday | string | 是 | 作者生日 |
请求示例
{
"ISBN": "201",
"name": "从开始到结束",
"price": 1.000,
"date": "123",
"authors": null
}
响应体
响应体是一个 JSON 格式的 AddResponse
对象,包含以下字段:
字段名 | 类型 | 描述 |
---|---|---|
result | string | 操作结果,"S" 表示成功,"E" 表示失败 |
message | string | 操作消息,描述操作结果 |
ISBN | string | 书籍的 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 服务器就搭建完成了。