使用ASP.NET Core 3.x 构建 RESTful API P1 准备工作
使用ASP.NET Core 3.x 构建 RESTful API P1 准备工作
博客园文章Id:12554353
Web API
简介:
- Web API通常是指"使用HTTP协议并通过网络调用的API",由于它使用了HTTP协议,所以需要通过URL信息来指定端点.
- API 是
Application Programming Intrface
的缩写,是软件的外部接口. 也就是说,针对某个软件,人们可以知道它的外部功能,但并不知道(也不需要知道)它内部运作的具体细节,为了从外部调用某些功能,需要指定软件的调用规范等信息,这样的规范就是API,所以WebAPI就是一个Web系统,通过访问URL可以与其进行信息交互.
什么是RESTful API
REST一词是在2000年首次出现的,它是由Roy Fielding博士在《架构风格以及基于网络的软件架构设计》这篇论文中提到的.他为REST风格的API制定了一套约束规范或者叫做架构风格.
所以准确的说,只有符合了Roy Fielding架构风格的Web API才能称作是 RESTful API.但是在实际开发中,有时候也有不完全符合Roy Fileding架构风格的情况出现.
MVC模式
- MVC (Model-View-Controller) 是一种主要用来构建UI的架构模式.
- 松耦合.
- 关注点分离. (separation of concerns)
- MVC 不是一个完整的应用程序架构.
MVC映射为API
- Model,它负责处理程序处理的逻辑.
- View,它是程序里负责展示数据的那部分.在构建API的时候,View就是数据或资源的展示.现在通常使用JSON格式.
- Controller,它是负责View和Model之间的交互.
三者之间的关系
创建 ASP .Net Core 项目步骤
Startup.cs 类的介绍
在 Asp .Net Core 中 Startup.cs 类主要是用来进行动态配置的工作.
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace Routine.Api
{
public class Startup
{
private readonly IConfiguration _configuration;
/*
* 通过IServiceCollection 我们可以获取或修改到一些配置文件中的信息,
* 比如系统默认的appSettings.json文件.
*/
public Startup(IConfiguration configuration)
{
_configuration = configuration;
}
/*
* Asp.Net Core 拥有一个服务的容器,ConfigureServices方法,
* 就是向服务的容器中,注册一些服务,已经注册的服务就可以通过依赖注入的方式,
* 在整个应用程序的其它地方进行使用.
*/
public void ConfigureServices(IServiceCollection services)
{
/*
* AddMvc() 方法不仅包含了构建WebApi方面的服务,它还有其它服务,比如构建视图,
* 或者是TagHelper相关的服务等.
*/
//services.AddMvc();
/*
* AddController() 方法支持构建WebApi服务,包括
* Controller的支持,Model的绑定,以及格式化器相关的服务等.
*/
services.AddControllers();
}
/*
* Configure 方法是在ConfigureServices方法调用之后被调用的,
* Asp.Net Core 主要是通过Configure 方法来配置Http请求管道中需要使用到的
* 中间件.
*/
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment()) //判断当前运行的环境是否是开发环境
{
/*
* 下列中间件是当,程序中有异常没有被捕获,通过此中间件会记录,并显示该异常信息
*/
app.UseDeveloperExceptionPage();
}
/*
* 配置路由中间件
*/
app.UseRouting();
/*
* 为整个WebApi 配置应用授权的能力,当我们需要考虑Api安全性的问题时,我们就需要,
* 使用并配置UseAuthentication()中间件,通常授权的配置,是在ConfigureServices
* 方法中完成的.
*/
app.UseAuthentication();
/*
* 端点中间件
*/
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/", async context =>
{
await context.Response.WriteAsync("Hello World!");
});
});
/*
* 在Asp.Net Core Http请求管道中中间件的配置的顺序实际上是很重要的.
*/
}
}
}
ASP .NET Core 请求管道
ASP .Net Core 启动项配置
Asp .Net Core 启动项的配置有两种方式.
- 修改
Properties
中的launchSettings.json
文件, 如:
- 或者我们可以通过vs的配置页面来修改
launchSettings.json
文件,如:
此处我们建议使用 .Net Core 自带的控制台程序来运行项目,而不是使用IIS来运行控制台项目来进行调试.
运行 .Net Core 项目时的效果如下:
下面我们将使用 Sqlite
以及 Entity Farmework Core
来构建一个小型的WebApi接口项目,针对Entity Farmework Core
如何在.Net Core 3.x
中运行,我们可以查阅官方文档. Entity Framework Core
开始项目
- 我们首先需要在项目中通过
Nuget
来添加Entity Framework Core
,以及Sqlite
的支持.
Micosoft.EntityFrameworkCore.tools 主要是用来做迁移工作的.
Micosoft.EntityFrameworkCore.sqlite 主要是用来使用EF Core 操作 Sqlite数据库的.
- 搭建Demo
在项目下创建 Controllers 文件夹,用于创建 WebApi 所必须的Controller控制器类
在项目下创建 Entitle 文件夹,用于创建 项目所需的实体类.
在项目下创建 Data 文件夹,用于创建 EF上下文类.
在项目下创建 Services文件夹,用于创建服务.
RoutineDbContext 类
using System;
using Microsoft.EntityFrameworkCore;
using Routine.Api.Entitle;
namespace Routine.Api.Data
{
public class RoutineDbContext:DbContext
{
/*
* Ef Core 需要 DbContextOptions 类型的配置才能进行工作,所以此处的配置,我们需要再Startup类中进行注入,
* 才能使用此DbContext.
*/
public RoutineDbContext(DbContextOptions<RoutineDbContext> options):base(options)
{
}
/// <summary>
/// 下面两个实例,将被映射到数据库的两个表中
/// </summary>
public DbSet<Company> Companies { get; set; }
public DbSet<Employee> Employees { get; set; }
/*
* 有的时候我们需要对实体,进行一些限制,比如是必填的,最大长度等,
* 这个使用我们就需要重写父类中的 OnModelCreating 方法
*/
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
//要求 Company类中 Name属性 是必填的.并且其长度不能超过100个字符
modelBuilder.Entity<Company>()
.Property(x => x.Name)
.IsRequired()
.HasMaxLength(100);
//要求 Company类中 Introduction属性 的长度不能超过500个字符
modelBuilder.Entity<Company>()
.Property(x => x.Introduction)
.HasMaxLength(500);
//要求 Employee类中 EmployeeNo 属性 是必填的.并且其长度不能超过10个字符
modelBuilder.Entity<Employee>()
.Property(x => x.EmployeeNo)
.IsRequired()
.HasMaxLength(10);
//要求 Employee类中 FirstName 属性 是必填的.并且其长度不能超过50个字符
modelBuilder.Entity<Employee>()
.Property(x => x.FirstName)
.IsRequired()
.HasMaxLength(50);
//要求 Employee类中 LastName 属性 是必填的.并且其长度不能超过50个字符
modelBuilder.Entity<Employee>()
.Property(x => x.LastName)
.IsRequired()
.HasMaxLength(50);
//手动指明两个表的对应关系(可以不手动指明)
modelBuilder.Entity<Employee>()
.HasOne(x => x.Company) //指明 Employee 类的导航属性是Company
.WithMany(x => x.Employees) // Company 类中的导航属性是 Employees
.HasForeignKey(x => x.CompanyId) //外键是CompanyId
.OnDelete(DeleteBehavior.Restrict); //在做删除的时候Company下面存在Employee时,是不能删除的.
/*
* 我们还可以在 OnModelCreating 方法中,配置一些种子数据,在做数据迁移的时候,我们就可以使用到这些.
* 种子数据.
*/
modelBuilder.Entity<Company>().HasData(
new Company
{
Id = Guid.Parse("bbdee09c-089b-4d30-bece-44df5923716c"),
Name = "Microsoft",
Introduction = "Great Company"
},
new Company
{
Id = Guid.Parse("6fb600c1-9011-4fd7-9234-881379716440"),
Name = "Google",
Introduction = "Don't be evil"
},
new Company
{
Id = Guid.Parse("5efc910b-2f45-43df-afae-620d40542853"),
Name = "Alipapa",
Introduction = "Fubao Company"
}
);
}
}
}
实体模型 Company
using System;
using System.Collections.Generic;
namespace Routine.Api.Entitle
{
public class Company
{
public Guid Id { get; set; }
public string Name { get; set; }
public string Introduction { get; set; }
public ICollection<Employee> Employees { get; set; }
}
}
实体模型 Employee
using System;
using Microsoft.AspNetCore.Identity;
namespace Routine.Api.Entitle
{
public class Employee
{
public Guid Id { get; set; }
public Guid CompanyId { get; set; }
public string EmployeeNo { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public Gender Gender { get; set; }
public DateTime DateOfBirth { get; set; }
public Company Company { get; set; }
}
}
namespace Routine.Api.Entitle
{
public enum Gender
{
男 = 1,
女 = 2
}
}
声明服务
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Routine.Api.Entitle;
namespace Routine.Api.Services
{
public interface ICompanyRepository
{
Task<IEnumerable<Company>> GetCompaniesAsync();
Task<IEnumerable<Company>> GetCompaniesAsync(IEnumerable<Guid> companyIds);
Task<Company> GetCompanyAsync(Guid companyId);
void AddCompany(Company company);
void UpdateCompany(Company company);
void DeleteCompany(Company company);
Task<bool> CompanyExistsAsync(Guid companyId);
Task<IEnumerable<Employee>> GetEmployeeAsync(Guid companyId);
Task<Employee> GetEmployeeAsync(Guid companyId, Guid employeeId);
void AddEmployee(Guid companyId, Employee employee);
void UpdateEmployee(Employee employee);
void DeleteEmployee(Employee employee);
Task<bool> SaveAsync();
}
}
声明实现类
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Routine.Api.Data;
using Routine.Api.Entitle;
namespace Routine.Api.Services
{
public class CompanyImpl:ICompanyRepository
{
private readonly RoutineDbContext _content;
public CompanyImpl(RoutineDbContext content)
{
_content = content ?? throw new ArgumentNullException(nameof(content));
}
public async Task<IEnumerable<Company>> GetCompaniesAsync()
{
return await this._content.Companies.ToListAsync();
}
public async Task<IEnumerable<Company>> GetCompaniesAsync(IEnumerable<Guid> companyIds)
{
if (companyIds == null)
{
throw new ArgumentNullException(nameof(companyIds));
}
return await this._content.Companies
.Where(x => companyIds.Contains(x.Id))
.OrderBy(x => x.Name)
.ToListAsync();
}
public async Task<Company> GetCompanyAsync(Guid companyId)
{
if (companyId == null)
{
throw new ArgumentNullException(nameof(companyId));
}
return await this._content.Companies.FirstOrDefaultAsync(x => x.Id == companyId);
}
public void AddCompany(Company company)
{
this._content.Companies.AddAsync(company);
}
public void UpdateCompany(Company company)
{
//下面这句话不写也行,因为 Ef Core 对 实体是进行跟踪的,所以实体如果有更改的话,Ef core是知道的.
//this._content.Entry(company).State = EntityState.Modified;
}
public void DeleteCompany(Company company)
{
this._content.Entry(company).State = EntityState.Deleted;
}
public async Task<bool> CompanyExistsAsync(Guid companyId)
{
if (companyId == null)
{
throw new ArgumentNullException(nameof(companyId));
}
return await this._content.Companies.AnyAsync(x => x.Id == companyId);
}
public async Task<IEnumerable<Employee>> GetEmployeeAsync(Guid companyId)
{
if (companyId == null)
{
throw new ArgumentNullException(nameof(companyId));
}
return await this._content.Employees
.Where(x => x.CompanyId == companyId)
.OrderBy(x => x.EmployeeNo)
.ToListAsync();
}
public async Task<Employee> GetEmployeeAsync(Guid companyId, Guid employeeId)
{
if (companyId == null)
{
throw new ArgumentNullException(nameof(companyId));
}
if (employeeId == null)
{
throw new ArgumentNullException(nameof(employeeId));
}
return await this._content.Employees
.Where(x => x.CompanyId == companyId && x.Id == employeeId)
.FirstOrDefaultAsync();
}
public void AddEmployee(Guid companyId, Employee employee)
{
if (companyId == null)
{
throw new ArgumentNullException(nameof(companyId));
}
if (employee == null)
{
throw new ArgumentNullException(nameof(employee));
}
employee.CompanyId = companyId;
this._content.Employees.Add(employee);
}
public void UpdateEmployee(Employee employee)
{
//下面这句话不写也行,因为 Ef Core 对 实体是进行跟踪的,所以实体如果有更改的话,Ef core是知道的.
//this._content.Entry(employee).State = EntityState.Modified;
}
public void DeleteEmployee(Employee employee)
{
this._content.Employees.Remove(employee);
}
public async Task<bool> SaveAsync()
{
return await this._content.SaveChangesAsync()>=0;
}
}
}
接下来我们使用 .Net Core
自带的容器对 ICompanyRepository
服务进行注册,
以及Sqlite
数据库的连接配置工作.
在 Startup.cs
类中的ConfigureServices
方法中添加如下代码:
//将组件 CompanyImpl 注册给 ICompanyRepository 服务
services.AddScoped<ICompanyRepository, CompanyImpl>();
//注册Sqlite数据的链接配置
services.AddDbContext<RoutineDbContext>(option =>
{
option.UseSqlite("Date Source = routine.db ");
});
为了演示程序方便进行,我们需要修改Programe.cs
类中的 Main
方法中的代码,具体如下:
//我们需要再Build()方法之后Run()方法之前,添加一些数据库迁移的工作,具体Main函数方法代码如下:
public static void Main(string[] args)
{
var host = CreateHostBuilder(args).Build();
/*
* 代码执行到此处时,暂时还不可以进行传统的构造函数进行注入,所以我们需要通过下列方式,将容器
* 中的服务取出来.
*/
//服务的作用范围
using (var scope = host.Services.CreateScope())
{
try
{
// 获取数据库对象
var dbContext = scope.ServiceProvider.GetService<RoutineDbContext>();
dbContext.Database.EnsureDeleted(); //每次启动的时候,先把数据库中的数据给删除.
dbContext.Database.Migrate(); //删除完数据库之后,我们将种子数据迁移至数据库中.
}
catch (Exception e)
{
//当出现异常时,我们需要记录一下日志
var logger = scope.ServiceProvider.GetRequiredService<ILogger<Program>>();
logger.LogError(e, "Database Migration Error");
}
}
host.Run();
}
现在我们手动执行一下迁移,以确保数据库中,现在存在种子数据.
运行vs中的程序包管理器控制台(视图→其它窗口→程序包管理控制台) 执行如下代码:
Add-Migration InitialMigration
上面代码的意思表示添加一次迁移,并未此次的迁移添加一个名字(InititalMiaration)
执行结果:
并且自动会在工程中创建 Migrations
文件夹,以及两个类文件.
20200325154437_InitialMigration.cs
类文件表示此次迁移的变化.
RoutineDbContextModelSnapshot.cs
类文件表示当前数据库的快照.
以上两个迁移文件生成之后,我们就不在需要进行手动迁移了(因为我们在Main方法中已经写了迁移的代码了),当程序执行到Main方法中时,数据库连接对象会先删除数据库中就的数据信息,并将Migrations
文件夹中的迁移数据信息,迁移至数据库中.
到此,我们运行程序,就会在工程下产生一个Routine.db
的sqlite
数据库文件.
我们可以通过数据库可视化工具查看该数据库文件中的数据,再此我们使用DataGrip
来查看.
选择数据库:
指定数据库文件并测试连接:
此时我们可以看到数据库的Companies表中已经存在三条种子数据:
到此准备工作结束.
- 以上文档参考自
准备工作
- 完整使用教程
提取码:hsmj