将SpringBoot的先进理念与C#的简洁优雅合二为一,声明式编程,专注于”做什么”而不是”如何去做”。在更高层面写代码,更关心的是目标,而不是底层算法实现的过程,SummerBoot,致力于打造一个人性化框架,让.net开发变得更简单优雅。
这是一个注解 + 接口的方式实现各种调用的全声明式框架,框架会通过Reflection Emit技术,自动生成接口的实现类。
群号:799648362
你可以运行以下命令在你的项目中安装 SummerBoot。
PM> Install-Package SummerBoot
net core 3.1,net 6
- 感谢jetbrain提供的ide许可证
- SummerBoot的核心理念
- 框架说明
- 加入QQ群反馈建议
- Getting Started
- 支持框架
- 文档目录
- SummerBoot中操作数据库
- SummerBoot中使用feign进行http调用
- 1.在startup.cs类中注册服务
- 2.定义接口
- 3.设置请求头(header)
- 4.自定义拦截器
- 5.定义方法
- 5.1方法里的普通参数
- 5.2方法里的特殊参数
- 5.2.1参数添加Query注解
- 5.2.2参数添加Body(BodySerializationKind.Form)注解
- 5.2.3参数添加Body(BodySerializationKind.Json)注解
- 5.2.4使用特殊类HeaderCollection作为方法参数,即可批量添加请求头
- 5.2.5使用特殊类BasicAuthorization作为方法参数,即可添加basic认证的Authorization请求头
- 5.2.6使用特殊类MultipartItem作为方法参数,并且在方法上标注Multipart注解,即可上传附件
- 5.2.7使用类Stream作为方法返回类型,即可接收流式数据,比如下载文件。
- 5.2.8使用类HttpResponseMessage作为方法返回类型,即可获得最原始的响应消息。
- 5.2.9使用类Task作为方法返回类型,即无需返回值。
- 6. 微服务-接入nacos
- 7. 在上下文中使用cookie
- SummerBoot中使用cache进行缓存操作
- SummerBoot中的人性化的设计
summerBoot基于工作单元与仓储模式开发了自己的ORM模块,即repository,底层基于dapper,上层通过模板模式,支持了常见的4种数据库类型(sqlserver,mysql,oracle,sqlite)的增删改查操作,如果有其他数据库需求,可以参考以上4个的源码,给本项目贡献代码。orm不支持多表的lambda查询,因为多表查询直接写sql更好更易理解。
需要自己通过nuget安装相应的数据库依赖包,比如SqlServer的Microsoft.Data.SqlClient,mysql的Mysql.data, oracle的Oracle.ManagedDataAccess.Core
services.AddSummerBoot();
services.AddSummerBootRepository(it =>
{
//-----------以下为必填参数---------
//注册数据库类型,比如SqliteConnection,MySqlConnection,OracleConnection,SqlConnection
it.DbConnectionType = typeof(SqliteConnection);
//添加数据库连接字符串
it.ConnectionString = "Data source=./mydb.db";
//-----------以下为可选参数---------
//插入的时候自动添加创建时间,数据库实体类必须继承于BaseEntity,oracle继承于OracleBaseEntity
it.AutoUpdateLastUpdateOn = true;
//插入的时候自动添加创建时间,使用utc时间
it.AutoUpdateLastUpdateOnUseUtc = false;
//update的时候自动更新最后更新时间字段,数据库实体类必须继承于BaseEntity,oracle继承于OracleBaseEntity
it.AutoAddCreateOn = true;
//update的时候自动更新最后更新时间字段,使用utc时间
it.AutoAddCreateOnUseUtc = false;
//启用软删除,数据库实体类必须继承于BaseEntity,oracle继承于OracleBaseEntity
it.IsUseSoftDelete = true;
});
其中注解大部分来自于系统命名空间System.ComponentModel.DataAnnotations 和 System.ComponentModel.DataAnnotations.Schema,比如表名Table,列名Column,主键Key,主键自增DatabaseGenerated(DatabaseGeneratedOption.Identity),列名Column,不映射该字段NotMapped等,同时自定义了一部分注解,比如更新时忽略该列IgnoreWhenUpdateAttribute(主要用在创建时间这种在update的时候不需要更新的字段), 同时SummerBoot自带了一个基础实体类BaseEntity(oracle 为OracleBaseEntity),实体类里包括自增的id,创建人,创建时间,更新人,更新时间以及软删除标记,推荐实体类直接继承BaseEntity
public class Customer : BaseEntity
{
public string Name { set; get; }
public int Age { set; get; } = 0;
/// <summary>
/// 会员号
/// </summary>
public string CustomerNo { set; get; }
/// <summary>
/// 总消费金额
/// </summary>
public decimal TotalConsumptionAmount { set; get; }
}
sqlserver里命名空间即schemas,oracle里命名空间即模式,sqlite和mysql里命名空间即数据库, 如果要定义不同命名空间下的表,添加[Table("CustomerWithSchema", Schema = "test")]注解即可。
[Table("CustomerWithSchema", Schema = "test")]
public class CustomerWithSchema
{
public string Name { set; get; }
public int Age { set; get; } = 0;
}
public class TestController : Controller
{
private readonly IDbGenerator dbGenerator;
public TestController(IDbGenerator dbGenerator)
{
this.dbGenerator = dbGenerator;
}
[HttpGet("GenerateSql")]
public async Task<IActionResult> GenerateSql()
{
var generateSqls = dbGenerator.GenerateSql(new List<Type>() { typeof(Customer) });
return Content("ok");
}
}
这里以mysql为例,生成的sql如下:
CREATE TABLE Customer (
`Id` int NOT NULL AUTO_INCREMENT,
`Name` text NULL ,
`Age` int NOT NULL ,
`CustomerNo` text NULL ,
`TotalConsumptionAmount` decimal(18,2) NOT NULL ,
`LastUpdateOn` datetime NULL ,
`LastUpdateBy` text NULL ,
`CreateOn` datetime NULL ,
`CreateBy` text NULL ,
`Active` int NULL ,
PRIMARY KEY (`Id`)
)
那么生成的sql为,新增字段的sql或者更新注释的sql,为了避免数据丢失,不会有删除字段的sql,这里以Customer表举例,如果刚开始没有继承BaseEntity,生成了表,后来继承BaseEntity了,那么此时生成的sql为
ALTER TABLE Customer ADD `Id` int NOT NULL PRIMARY KEY AUTO_INCREMENT
ALTER TABLE Customer ADD `LastUpdateOn` datetime NULL
ALTER TABLE Customer ADD `LastUpdateBy` text NULL
ALTER TABLE Customer ADD `CreateOn` datetime NULL
ALTER TABLE Customer ADD `CreateBy` text NULL
ALTER TABLE Customer ADD `Active` int NULL
把生成sql和执行sql分成2部分操作,对于日常而言是更方便的,我们可以快速拿到要执行的sql,进行检查,确认没问题后,可以保存下来,在正式发布应用时,留给dba审查。执行sql的代码如下
var generateSqls = dbGenerator.GenerateSql(new List<Type>() { typeof(Customer) });
foreach (var sqlResult in generateSqls)
{
dbGenerator.ExecuteGenerateSql(sqlResult);
}
这里统一使用column注解,如[Column("Age",TypeName = "float")]
public class Customer : BaseEntity
{
public string Name { set; get; }
[Column("Age",TypeName = "float")]
public int Age { set; get; } = 0;
/// <summary>
/// 会员号
/// </summary>
public string CustomerNo { set; get; }
/// <summary>
/// 总消费金额
/// </summary>
public decimal TotalConsumptionAmount { set; get; }
}
生成的sql如下
CREATE TABLE `Customer2` (
`Id` int NOT NULL AUTO_INCREMENT,
`Name` text NULL ,
`Age` float NOT NULL ,
`CustomerNo` text NULL ,
`TotalConsumptionAmount` decimal(18,2) NOT NULL ,
PRIMARY KEY (`Id`)
)
参数为数据库表名的集合和生成的实体类的命名空间
public class TestController : Controller
{
private readonly IDbGenerator dbGenerator;
public TestController(IDbGenerator dbGenerator)
{
this.dbGenerator = dbGenerator;
}
[HttpGet("GenerateClass")]
public async Task<IActionResult> GenerateClass()
{
var generateClasses = dbGenerator.GenerateCsharpClass(new List<string>() { "Customer" },"Test.Model");
return Content("ok");
}
}
生成的c#实体类如下,新建一个类文件并把文本黏贴进去即可
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Test.Model
{
[Table("Customer")]
public class Customer
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[Column("Id")]
public int Id { get; set; }
[Column("Name")]
public string Name { get; set; }
[Column("Age")]
public int Age { get; set; }
[Column("CustomerNo")]
public string CustomerNo { get; set; }
[Column("TotalConsumptionAmount")]
public decimal TotalConsumptionAmount { get; set; }
[Column("LastUpdateOn")]
public DateTime? LastUpdateOn { get; set; }
[Column("LastUpdateBy")]
public string LastUpdateBy { get; set; }
[Column("CreateOn")]
public DateTime? CreateOn { get; set; }
[Column("CreateBy")]
public string CreateBy { get; set; }
[Column("Active")]
public int? Active { get; set; }
}
}
[AutoRepository]
public interface ICustomerRepository : IBaseRepository<Customer>
{
}
通过DI注入自定义仓储接口以后,就可以开始查了,支持正常查询与分页查询,查询有2种方式。
//常规查询
var customers= customerRepository.Where(it => it.Age > 5).OrderBy(it => it.Id).Take(10).ToList();
//分页
var page2 = await customerRepository.Where(it => it.Age > 5).Skip(0).Take(10).ToPageAsync();
然后在Select,Update,Delete里写sql语句,如
[AutoRepository]
public interface ICustomerRepository : IBaseRepository<Customer>
{
//async
[Select("select od.productName from customer c join orderHeader oh on c.id=oh.customerid" +
" join orderDetail od on oh.id=od.OrderHeaderId where c.name=@name")]
Task<List<CustomerBuyProduct>> QueryAllBuyProductByNameAsync(string name);
[Select("select * from customer where age>@age order by id")]
Task<Page<Customer>> GetCustomerByPageAsync(IPageable pageable, int age);
//sync
[Select("select od.productName from customer c join orderHeader oh on c.id=oh.customerid" +
" join orderDetail od on oh.id=od.OrderHeaderId where c.name=@name")]
List<CustomerBuyProduct> QueryAllBuyProductByName(string name);
[Select("select * from customer where age>@age order by id")]
Page<Customer> GetCustomerByPage(IPageable pageable, int age);
}
使用方法:
var result = await customerRepository.QueryAllBuyProductByNameAsync("testCustomer");
//page
var pageable = new Pageable(1, 10);
var page = customerRepository.GetCustomerByPage(pageable, 5);
注意:4.1.2查询里的分页支持,方法的返回值由Page这个类包裹,同时方法参数里必须包含 IPageable这个分页参数,sql语句里也要有order by,例如:
[Select("select * from customer where age>@age order by id")]
Page<Customer> GetCustomerByPage(IPageable pageable, int age);
配置的json如下:
{
"mysqlSql": {
"QueryListSql": "select * from customer ",
"QueryByPageSql": "select * from customer order by age",
"UpdateByNameSql": "update customer set age=@age where name=@name",
"DeleteByNameSql": "delete from customer where name=@name "
}
}
配置项通过${}包裹,接口如下,:
[AutoRepository]
public interface ICustomerTestConfigurationRepository : IBaseRepository<Customer>
{
//异步
[Select("${mysqlSql:QueryListSql}")]
Task<List<Customer>> QueryListAsync();
[Select("${mysqlSql:QueryByPageSql}")]
Task<Page<Customer>> QueryByPageAsync(IPageable pageable);
//异步
[Update("${mysqlSql:UpdateByNameSql}")]
Task<int> UpdateByNameAsync(string name, int age);
[Delete("${mysqlSql:DeleteByNameSql}")]
Task<int> DeleteByNameAsync(string name);
//同步
[Select("${mysqlSql:QueryListSql}")]
List<Customer> QueryList();
[Select("${mysqlSql:QueryByPageSql}")]
Page<Customer> QueryByPage(IPageable pageable);
//异步
[Update("${mysqlSql:UpdateByNameSql}")]
int UpdateByName(string name,int age);
[Delete("${mysqlSql:DeleteByNameSql}")]
int DeleteByName(string name);
}
将单个查询条件用{{}}包裹起来,一个条件里只能包括一个变量,同时在定义方法的时候,参数定义为WhereItem<T>,T为泛型参数,表示真正的参数类型,这样summerboot就会自动处理查询条件,处理规则如下,如果whereItem的active为true,即激活该条件,则sql语句中{{ }}包裹的查询条件会展开并参与查询,如果active为false,则sql语句中{{ }}包裹的查询条件自动替换为空字符串,不参与查询,为了使whereItem更好用,提供了WhereBuilder这种方式,使用例子如下所示:
//definition
[AutoRepository]
public interface ICustomerRepository : IBaseRepository<Customer>
{
[Select("select * from customer where 1=1 {{ and name=@name}}{{ and age=@age}}")]
Task<List<CustomerBuyProduct>> GetCustomerByConditionAsync(WhereItem<string> name, WhereItem<int> age);
[Select("select * from customer where 1=1 {{ and name=@name}}{{ and age=@age}} order by id")]
Task<Page<Customer>> GetCustomerByPageByConditionAsync(IPageable pageable, WhereItem<string> name, WhereItem<int> age);
}
//use
var nameEmpty = WhereBuilder.Empty<string>();//var nameEmpty = new WhereItem<string>(false,"");
var ageEmpty = WhereBuilder.Empty<int>();
var nameWhereItem = WhereBuilder.HasValue("page5");//var nameWhereItem =WhereItem<string>(true,"page5");
var ageWhereItem = WhereBuilder.HasValue(5);
var pageable = new Pageable(1, 10);
var bindResult = customerRepository.GetCustomerByCondition(nameWhereItem, ageEmpty);
Assert.Single(bindResult);
var bindResult2 = customerRepository.GetCustomerByCondition(nameEmpty, ageEmpty);
Assert.Equal(102, bindResult2.Count);
var bindResult5 = customerRepository.GetCustomerByPageByCondition(pageable, nameWhereItem, ageEmpty);
Assert.Single(bindResult5.Data);
var bindResult6 = customerRepository.GetCustomerByPageByCondition(pageable, nameEmpty, ageEmpty);
如果还有更复杂的自定义查询怎么办?参考 6.如果有些特殊情况要求自己手写实现类怎么办?
Get方法,通过id获取结果,GetAll(),获取表里的所有结果集。
var customer = new Customer() { Name = "testCustomer" };
customerRepository.Insert(customer);
var customer2 = new Customer() { Name = "testCustomer2" };
var customer3 = new Customer() { Name = "testCustomer3" };
var customerList = new List<Customer>() { customer2, customer3 };
customerRepository.Insert(customerList);
框架不会自动为实体的ID这个字段赋值,同时数据库为mysql的情况下,有一些特殊,首先驱动库必须有MySqlConnector,这个库可以和mysql.data共存,并不会冲突,所以无需担心,且数据库连接字符串后面必须加";AllowLoadLocalInfile=true",同时在mysql数据库上执行"set global local_infile=1"开启批量上传。sqlite不支持批量快速插入。
var customer2 = new Customer() { Name = "testCustomer2" };
var customer3 = new Customer() { Name = "testCustomer3" };
var customerList = new List<Customer>() { customer2, customer3 };
customerRepository.FastBatchInsert(customerList);
customerRepository.Delete(customer);
customerRepository.Delete(customerList);
var deleteCount = customerRepository.Delete(it => it.Age > 5);
customerRepository.Update(customer);
customerRepository.Update(customerList);
var updateCount= customerRepository.Where(it=>it.Name == "testCustomer")
.SetValue(it=>it.Age,5)
.SetValue(it=>it.TotalConsumptionAmount,100)
.ExecuteUpdate();
事务支持,需要在注入自定义仓储接口的同时,也注入框架自带的IUnitOfWork接口,用法如下
//uow is IUnitOfWork interface
try
{
uow.BeginTransaction();
customerRepository.Insert(new Customer() { Name = "testCustomer2" });
var orderDetail3 = new OrderDetail
{
OrderHeaderId = orderHeader.Id,
ProductName = "ball",
Quantity = 3
};
orderDetailRepository.Insert(orderDetail3);
uow.Commit();
}
catch (Exception e)
{
uow.RollBack();
}
注意,此时该接口无需添加AutoRepository注解
public interface ICustomCustomerRepository : IBaseRepository<Customer>
{
Task<List<Customer>> GetCustomersAsync(string name);
Task<Customer> GetCustomerAsync(string name);
Task<int> UpdateCustomerNameAsync(string oldName, string newName);
Task<int> CustomQueryAsync();
}
注解的参数为这个类对应的自定义接口的类型和服务的声明周期ServiceLifetime(周期默认为scope级别),添加AutoRegister注解的目的是让模块自动将自定义接口和自定义类注册到IOC容器中,后续直接注入使用即可,BaseRepository自带了Execute,QueryFirstOrDefault和QueryList方法,如果要接触更底层的dbConnection进行查询,参考下面的CustomQueryAsync方法,首先OpenDb(),然后查询,查询中一定要带上transaction:dbTransaction这个参数,查询结束以后CloseDb();
[AutoRegister(typeof(ICustomCustomerRepository))]
public class CustomCustomerRepository : BaseRepository<Customer>, ICustomCustomerRepository
{
public CustomCustomerRepository(IUnitOfWork uow, IDbFactory dbFactory, RepositoryOption repositoryOption) : base(uow, dbFactory, repositoryOption)
{
}
public async Task<Customer> GetCustomerAsync(string name)
{
var result =
await this.QueryFirstOrDefaultAsync<Customer>("select * from customer where name=@name", new { name });
return result;
}
public async Task<List<Customer>> GetCustomersAsync(string name)
{
var result = await this.QueryListAsync<Customer>("select * from customer where name=@name", new { name });
return result;
}
public async Task<int> UpdateCustomerNameAsync(string oldName, string newName)
{
var result = await this.ExecuteAsync("update customer set name=@newName where name=@oldName", new { newName, oldName });
return result;
}
public async Task<int> CustomQueryAsync()
{
this.OpenDb();
var grid = await this.dbConnection.QueryMultipleAsync("select id from customer",transaction:dbTransaction);
var id = grid.Read().FirstOrDefault()?.id;
this.CloseDb();
return id;
}
}
feign底层基于httpClient。
services.AddSummerBoot();
services.AddSummerBootFeign();
定义一个接口,并且在接口上添加FeignClient注解,FeignClient注解里可以自定义http接口url的公共部分-url(整个接口请求的url由FeignClient里的url加上方法里的path组成),是否忽略远程接口的https证书校验-IsIgnoreHttpsCertificateValidate,接口超时时间-Timeout(单位s),自定义拦截器-InterceptorType。
[FeignClient(Url = "http://localhost:5001/home", IsIgnoreHttpsCertificateValidate = true, InterceptorType = typeof(MyRequestInterceptor),Timeout = 100)]
public interface ITestFeign
{
[GetMapping("/query")]
Task<Test> TestQuery([Query] Test tt);
}
同时,url和path可以通过读取配置获取,配置项通过${}包裹,配置的json如下:
{
"configurationTest": {
"url": "http://localhost:5001/home",
"path": "/query"
}
}
接口如下:
[FeignClient(Url = "${configurationTest:url}")]
public interface ITestFeignWithConfiguration
{
[GetMapping("${configurationTest:path}")]
Task<Test> TestQuery([Query] Test tt);
}
有时候我们只希望使用方法里的path作为完整url发起http请求,则可以定义接口如下,设置UsePathAsUrl为true(默认为false)
[FeignClient(Url = "http://localhost:5001/home")]
public interface ITestFeign
{
[PostMapping("http://localhost:5001/home/json", UsePathAsUrl = true)]
Task TestUsePathAsUrl([Body(BodySerializationKind.Json)] Test tt);
}
接口上可以选择添加Headers注解,代表这个接口下所有http请求都带上注解里的请求头。Headers的参数为变长的string类型的参数,同时Headers也可以添加在方法上,代表该方法调用的时候,会加该请求头,接口上的Headers参数可与方法上的Headers参数互相叠加,同时headers里可以使用变量,变量的占位符为{{}},如
[FeignClient(Url = "http://localhost:5001/home", IsIgnoreHttpsCertificateValidate = true, InterceptorType = typeof(MyRequestInterceptor),Timeout = 100)]
[Headers("a:a","b:b")]
public interface ITestFeign
{
[GetMapping("/testGet")]
Task<Test> TestAsync();
[GetMapping("/testGetWithHeaders")]
[Headers("c:c")]
Task<Test> TestWithHeadersAsync();
//header替换
[Headers("a:{{methodName}}")]
[PostMapping("/abc")]
Task<Test> TestHeaderAsync(string methodName);
}
await TestFeign.TestAsync()
>>> get, http://localhost:5001/home/testGet,header为 "a:a" 和 "b:b"
await TestFeign.TestWithHeadersAsync()
>>> get, http://localhost:5001/home/testGetWithHeaders,header为 "a:a" ,"b:b"和 "c:c"
await TestFeign.TestHeaderAsync("abc");
>>> post, http://localhost:5001/home/abc,同时请求头为 "a:abc"
自定义拦截器对接口下的所有方法均生效,拦截器的应用场景主要是在请求前做一些操作,比如请求第三方业务接口前,需要先登录第三方系统,那么就可以在拦截器里先请求第三方登录接口,获取到凭证以后,放到header里,拦截器需要实现IRequestInterceptor接口,例子如下
//先定义一个用来登录的loginFeign客户端
[FeignClient(Url = "http://localhost:5001/login", IsIgnoreHttpsCertificateValidate = true,Timeout = 100)]
public interface ILoginFeign
{
[PostMapping("/login")]
Task<LoginResultDto> LoginAsync([Body()] LoginDto loginDto );
}
//接着自定义登录拦截器
public class LoginInterceptor : IRequestInterceptor
{
private readonly ILoginFeign loginFeign;
private readonly IConfiguration configuration;
public LoginInterceptor(ILoginFeign loginFeign, IConfiguration configuration)
{
this.loginFeign = loginFeign;
this.configuration = configuration;
}
public async Task ApplyAsync(RequestTemplate requestTemplate)
{
var username = configuration.GetSection("username").Value;
var password = configuration.GetSection("password").Value;
var loginResultDto = await this.loginFeign.LoginAsync(new LoginDto(){Name = username,Password = password});
if (loginResultDto != null)
{
requestTemplate.Headers.Add("Authorization", new List<string>() { "Bearer "+loginResultDto.Token });
}
await Task.CompletedTask;
}
}
//定义访问业务接口的testFegn客户端,在客户端上定义拦截器为loginInterceptor
[FeignClient(Url = "http://localhost:5001/home", IsIgnoreHttpsCertificateValidate = true, InterceptorType = typeof(LoginInterceptor),Timeout = 100)]
public interface ITestFeign
{
[GetMapping("/testGet")]
Task<Test> TestAsync();
}
await TestFeign.TestAsync();
>>> get to http://localhost:5001/home/testGet,header为 "Authorization:Bearer abc"
忽略拦截器,有时候我们接口中的某些方法,是不需要拦截器的,那么就可以在方法上添加注解IgnoreInterceptor,那么该方法发起的请求,就会忽略拦截器,如
//定义访问业务接口的testFegn客户端,在客户端上定义拦截器为loginInterceptor
[FeignClient(Url = "http://localhost:5001/home", IsIgnoreHttpsCertificateValidate = true, InterceptorType = typeof(LoginInterceptor),Timeout = 100)]
public interface ITestFeign
{
[GetMapping("/testGet")]
[IgnoreInterceptor]
Task<Test> TestAsync();
}
await TestFeign.TestAsync();
>>> get to http://localhost:5001/home/testGet,没有header
每个方法都应该添加注解代表发起请求的类型和要访问的url,有4个内置注解, GetMapping,PostMapping,PutMapping,DeleteMapping,同时方法的返回值必须是Task<>类型
[FeignClient(Url = "http://localhost:5001/home", IsIgnoreHttpsCertificateValidate = true, InterceptorType = typeof(MyRequestInterceptor),Timeout = 100)]
public interface ITestFeign
{
[GetMapping("/testGet")]
Task<Test> TestAsync();
[PostMapping("/testPost")]
Task<Test> TestPostAsync();
[PutMapping("/testPut")]
Task<Test> TestPutAsync();
[DeleteMapping("/testDelete")]
Task<Test> TestDeleteAsync();
}
参数如果没有特殊注解,或者不是特殊类,均作为动态参数参与url,header里变量的替换,(参数如果为类,则读取类的属性值),url和header中的变量使用占位符{{}},如果变量名和参数名不一致,则可以使用AliasAs注解(可以用在参数或者类的属性上)来指定别名,如
[FeignClient(Url = "http://localhost:5001/home", IsIgnoreHttpsCertificateValidate = true, InterceptorType = typeof(MyRequestInterceptor),Timeout = 100)]
public interface ITestFeign
{
//url替换
[PostMapping("/{{methodName}}")]
Task<Test> TestAsync(string methodName);
//header替换
[Headers("a:{{methodName}}")]
[PostMapping("/abc")]
Task<Test> TestHeaderAsync(string methodName);
//AliasAs指定别名
[Headers("a:{{methodName}}")]
[PostMapping("/abc")]
Task<Test> TestAliasAsAsync([AliasAs("methodName")] string name);
}
await TestFeign.TestAsync("abc");
>>> post to http://localhost:5001/home/abc
await TestFeign.TestAliasAsAsync("abc");
>>> post, http://localhost:5001/home/abc
await TestFeign.TestHeaderAsync("abc");
>>> post, http://localhost:5001/home/abc,同时请求头为 "a:abc"
参数添加query注解后参数值将以key1=value1&key2=value2的方式添加到url后面。
[FeignClient(Url = "http://localhost:5001/home", IsIgnoreHttpsCertificateValidate = true, InterceptorType = typeof(MyRequestInterceptor),Timeout = 100)]
public interface ITestFeign
{
[GetMapping("/TestQuery")]
Task<Test> TestQuery([Query] string name);
[GetMapping("/TestQueryWithClass")]
Task<Test> TestQueryWithClass([Query]Test tt);
}
await TestFeign.TestQuery("abc");
>>> get, http://localhost:5001/home/TestQuery?name=abc
await TestFeign.TestQueryWithClass(new Test() { Name = "abc", Age = 3 });
>>> get, http://localhost:5001/home/TestQueryWithClass?Name=abc&Age=3
public class EmbeddedTest2
{
public int Age { get; set; }
}
public class EmbeddedTest3
{
public string Name { get; set; }
[Embedded]
public EmbeddedTest2 Test { get; set; }
}
[FeignClient(Url = "http://localhost:5001/home")]
public interface ITestFeign
{
/// <summary>
/// 测试Embedded注解,表示参数是否内嵌,该测试嵌入
/// </summary>
/// <param name="tt"></param>
/// <returns></returns>
[GetMapping("/testEmbedded")]
Task<string> TestEmbedded([Query] EmbeddedTest3 tt);
}
await testFeign.TestEmbedded(new EmbeddedTest3()
{
Name = "sb",
Test = new EmbeddedTest2()
{
Age = 3
}
});
>>> get, http://localhost:5001/home/testEmbedded?Name=sb&Test=%7B%22Age%22%3A%223%22%7D
如果没有Embedded注解,则请求变成
>>> get, http://localhost:5001/home/testEmbedded?Name=sb&Age=3
相当于模拟html里的form提交,参数值将被URL编码后,以key1=value1&key2=value2的方式添加到载荷(body)里。
[FeignClient(Url = "http://localhost:5001/home", IsIgnoreHttpsCertificateValidate = true, InterceptorType = typeof(MyRequestInterceptor),Timeout = 100)]
public interface ITestFeign
{
[PostMapping("/form")]
Task<Test> TestForm([Body(BodySerializationKind.Form)] Test tt);
}
await TestFeign.TestForm(new Test() { Name = "abc", Age = 3 });
>>> post, http://localhost:5001/home/form,同时body里的值为Name=abc&Age=3
即以application/json的方式提交,参数值将会被json序列化后添加到载荷(body)里,同样的,如果类里的字段有别名,也可以使用AliasAs注解。
[FeignClient(Url = "http://localhost:5001/home", IsIgnoreHttpsCertificateValidate = true, InterceptorType = typeof(MyRequestInterceptor),Timeout = 100)]
public interface ITestFeign
{
[PostMapping("/json")]
Task<Test> TestJson([Body(BodySerializationKind.Json)] Test tt);
}
await TestFeign.TestJson(new Test() { Name = "abc", Age = 3 });
>>> post, http://localhost:5001/home/json,同时body里的值为{"Name":"abc","Age":3}
[FeignClient(Url = "http://localhost:5001/home", IsIgnoreHttpsCertificateValidate = true, InterceptorType = typeof(MyRequestInterceptor),Timeout = 100)]
public interface ITestFeign
{
[PostMapping("/json")]
Task<Test> TestJson([Body(BodySerializationKind.Json)] Test tt, HeaderCollection headers);
}
var headerCollection = new HeaderCollection()
{ new KeyValuePair<string, string>("a", "a"),
new KeyValuePair<string, string>("b", "b") };
await TestFeign.TestJson(new Test() { Name = "abc", Age = 3 },headerCollection);
>>> post, http://localhost:5001/home/json,同时body里的值为{"Name":"abc","Age":3},header为 "a:a" 和 "b:b"
[FeignClient(Url = "http://localhost:5001/home", IsIgnoreHttpsCertificateValidate = true, InterceptorType = typeof(MyRequestInterceptor),Timeout = 100)]
public interface ITestFeign
{
[GetMapping("/testBasicAuthorization")]
Task<Test> TestBasicAuthorization(BasicAuthorization basicAuthorization);
}
var username="abc";
var password="123";
await TestFeign.TestBasicAuthorization(new BasicAuthorization(username,password));
>>> get, http://localhost:5001/home/testBasicAuthorization,header为 "Authorization:Basic YWJjOjEyMw=="
[FeignClient(Url = "http://localhost:5001/home", IsIgnoreHttpsCertificateValidate = true, InterceptorType = typeof(MyRequestInterceptor),Timeout = 100)]
public interface ITestFeign
{
//仅上传文件
[Multipart]
[PostMapping("/multipart")]
Task<Test> MultipartTest(MultipartItem item);
//在上传附件的同时,也可以附带参数
[Multipart]
[PostMapping("/multipart")]
Task<Test> MultipartTest([Body(BodySerializationKind.Form)] Test tt, MultipartItem item);
}
//仅上传文件
var basePath = Path.Combine(AppContext.BaseDirectory, "123.txt");
var name="file";
var fileName="123.txt";
//方式1,使用byteArray
var byteArray= File.ReadAllBytes(basePath);
var result = await testFeign.MultipartTest(new MultipartItem(byteArray, name, fileName));
//方式2 ,使用stream
var fileStream= new FileInfo(basePath).OpenRead();
var result = await testFeign.MultipartTest(new MultipartItem(fileStream, name, fileName));
//方式3,使用fileInfo
var result = await testFeign.MultipartTest(new MultipartItem(new FileInfo(basePath),name,fileName));
>>> post, http://localhost:5001/home/multipart,同时body里带有附件
//在上传附件的同时,也可以附带参数
var basePath = Path.Combine(AppContext.BaseDirectory, "123.txt");
var name="file";
var fileName="123.txt";
//方式1,使用byteArray
var byteArray= File.ReadAllBytes(basePath);
var result = await testFeign.MultipartTest(new Test() { Name = "sb", Age = 3 }, new MultipartItem(byteArray, name, fileName));
//方式2 ,使用stream
var fileStream= new FileInfo(basePath).OpenRead();
var result = await testFeign.MultipartTest(new Test() { Name = "sb", Age = 3 }, new MultipartItem(fileStream, name, fileName));
//方式3,使用fileInfo
var result = await testFeign.MultipartTest(new Test() { Name = "sb", Age = 3 },new MultipartItem(new FileInfo(basePath),name,fileName));
>>> post, http://localhost:5001/home/multipart,同时body里的值为Name=abc&Age=3,并且带有附件
[FeignClient(Url = "http://localhost:5001/home", IsIgnoreHttpsCertificateValidate = true, InterceptorType = typeof(MyRequestInterceptor),Timeout = 100)]
public interface ITestFeign
{
[GetMapping("/downLoadWithStream")]
Task<Stream> TestDownLoadWithStream();
}
using var streamResult =await testFeign.TestDownLoadStream();
using var newfile = new FileInfo("D:\\123.txt").OpenWrite();
streamResult.CopyTo(newfile);
>>> get, http://localhost:5001/home/downLoadWithStream,返回值为流式数据,然后就可以保存为文件。
[FeignClient(Url = "http://localhost:5001/home", IsIgnoreHttpsCertificateValidate = true, InterceptorType = typeof(MyRequestInterceptor),Timeout = 100)]
public interface ITestFeign
{
[GetMapping("/test")]
Task<HttpResponseMessage> Test();
}
var rawResult =await testFeign.Test();
>>> get, http://localhost:5001/home/Test,返回值为httpclient的原始返回数据。
[FeignClient(Url = "http://localhost:5001/home", IsIgnoreHttpsCertificateValidate = true, InterceptorType = typeof(MyRequestInterceptor),Timeout = 100)]
public interface ITestFeign
{
[GetMapping("/test")]
Task Test();
}
await testFeign.Test();
>>> get, http://localhost:5001/home/Test,忽略返回值
在appsettings.json/appsettings.Development.json配置文件中添加配置
"nacos": {
//--------使用nacos则serviceAddress和namespaceId必填------
//nacos服务地址,如http://172.16.189.242:8848
"serviceAddress": "http://172.16.189.242:8848/",
//命名空间id,如832e754e-e845-47db-8acc-46ae3819b638或者public
"namespaceId": "dfd8de72-e5ec-4595-91d4-49382f500edf",
//--------如果只是访问nacos中的微服务,则仅配置lbStrategy即可。------
//客户端负载均衡算法,一个服务下有多个实例,lbStrategy用来挑选服务下的实例,默认为Random(随机),也可以选择WeightRandom(根据服务权重加权后再随机)
"lbStrategy": "Random",
//--------如果需要使用nacos配置中心,则ConfigurationOption必填------
"configurationOption": {
//配置的分组
"groupName": "DEFAULT_GROUP",
//配置的dataId,
"dataId": "prd"
},
//-------如果是要将本应用注册为服务实例,则全部参数均需配置--------------
//是否要把应用注册为服务实例
"registerInstance": true,
//要注册的服务名
"serviceName": "test",
//服务的分组名
"groupName": "DEFAULT_GROUP",
//权重,一个服务下有多个实例,权重越高,访问到该实例的概率越大,比如有些实例所在的服务器配置高,那么权重就可以大一些,多引流到该实例,与上面的参数lbStrategy设置为WeightRandom搭配使用
"weight": 1,
//本应用对外的网络协议,http或https
"protocol": "http",
//本应用对外的端口号,比如5000
"port": 5000
}
接入nacos配置中心十分简单,仅需在Program.cs中添加一行.UseNacosConfiguration()即可,当前支持json格式,xml格式和yaml格式。
net core3.1示例如下
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseNacosConfiguration()
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>().UseUrls("http://*:5001");
});
net6示例如下
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseNacosConfiguration();
如果是把当前应用注册为微服务实例,那么到这一步就结束了,feign会自动根据配置文件里的配置将本应用注册为微服务实例。如果是本应用要调用微服务接口,请看6.3.2
services.AddSummerBoot();
services.AddSummerBootFeign(it =>
{
it.AddNacos(Configuration);
});
设置微服务的名称ServiceName,分组名称NacosGroupName(不填则默认DEFAULT_GROUP),命名空间NacosNamespaceId(不填则默认public),以及MicroServiceMode设为true即可。url不用配置,剩下的就和正常的feign接口一样。
[FeignClient( ServiceName = "test", MicroServiceMode = true,NacosGroupName = "DEFAULT_GROUP", NacosNamespaceId = "dfd8de72-e5ec-4595-91d4-49382f500edf")]
public interface IFeignService
{
[GetMapping("/home/index")]
Task<string> TestGet();
}
同时ServiceName,NacosGroupName,NacosNamespaceId也支持从配置文件中读取,如
{
"ServiceName": "test",
"NacosGroupName": "DEFAULT_GROUP",
"NacosNamespaceId": "dfd8de72-e5ec-4595-91d4-49382f500edf"
}
[FeignClient( ServiceName = "${ServiceName}", MicroServiceMode = true,NacosGroupName = "${NacosGroupName}", NacosNamespaceId = "${NacosNamespaceId}")]
public interface IFeignService
{
[GetMapping("/home/index")]
Task<string> TestGet();
}
feign中的工作单元模式,可以在上下文中设置cookie,这样接口在上下文中发起http请求时就会自动带上cookie,使用工作单元模式需要注入IFeignUnitOfWork接口,然后操作如下:
var feignUnitOfWork = serviceProvider.GetRequiredService<IFeignUnitOfWork>();
//开启上下文
feignUnitOfWork.BeginCookie();
//添加cookie
feignUnitOfWork.AddCookie("http://localhost:5001/home/TestCookieContainer2", "abc=1");
await testFeign.TestCookieContainer2();
//结束上下文
feignUnitOfWork.StopCookie();
同时,如果接口返回了设置cookie的信息,工作单元也会保存下cookie,并且在上下文作用域内的接口发起http访问时,会自动带上这些cookie信息,一个很典型的场景是,我们在第一个接口登录后,接口会返回给我们cookie,在我们访问后续接口时,要带上第一个接口返回给我们的cookie。:
var feignUnitOfWork = serviceProvider.GetRequiredService<IFeignUnitOfWork>();
//开启上下文
feignUnitOfWork.BeginCookie();
//登录后获取cookie
await testFeign.LoginAsync("sb","123");
//请求时自动带上登录后的cookie
await testFeign.TestCookieContainer3();
//结束上下文
feignUnitOfWork.StopCookie();
缓存分为内存缓存和redis缓存,内存缓存注册方式如下:
services.AddSummerBoot();
services.AddSummerBootCache(it => it.UseMemory());
redis缓存注册方式如下,connectionString为redis连接字符串:
services.AddSummerBoot();
services.AddSummerBootCache(it =>
{
it.UseRedis(connectionString);
});
ICache接口主要有以下几个方法,以及对应的异步方法
/// <summary>
/// 绝对时间缓存,固定时间后缓存值失效
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="key"></param>
/// <param name="value"></param>
/// <param name="absoluteExpiration"></param>
/// <returns></returns>
bool SetValueWithAbsolute<T>(string key, T value, TimeSpan absoluteExpiration);
/// <summary>
/// 滑动时间缓存,如果在时间内有命中,则继续延长时间,未命中则缓存值失效
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="key"></param>
/// <param name="value"></param>
/// <param name="slidingExpiration"></param>
/// <returns></returns>
bool SetValueWithSliding<T>(string key, T value, TimeSpan slidingExpiration);
/// <summary>
/// 获取值
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="key"></param>
/// <returns></returns>
CacheEntity<T> GetValue<T>(string key);
/// <summary>
/// 移除值
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
bool Remove(string key);
var cache = serviceProvider.GetRequiredService<ICache>();
//设置固定时间缓存
cache.SetValueWithAbsolute("test", "test", TimeSpan.FromSeconds(3));
//设置滑动时间缓存
var cache = serviceProvider.GetRequiredService<ICache>();
cache.SetValueWithSliding("test", "test", TimeSpan.FromSeconds(3));
//获取缓存
var value = cache.GetValue<string>("test");
//移除缓存
cache.Remove("test");
1.先说一个net core mvc自带的功能,如果我们想要在appsettings.json里配置web应用的ip和port该怎么办?在appsettings.json里直接写
{
"urls":"http://localhost:7002;http://localhost:7012"
}
- AutoRegister注解,作用是让框架自动将接口和接口的实现类注册到IOC容器中,标注在实现类上,注解的参数为这个类对应的自定义接口的type和服务的生命周期ServiceLifetime(周期默认为scope级别),使用方式如下:
public interface ITest
{
}
[AutoRegister(typeof(ITest),ServiceLifetime.Transient)]
public class Test:ITest
{
}
- ApiResult 接口返回值包装类,包含 code,msg和data,3个字段,让整个系统的返回值统一有序,有利于前端的统一拦截,统一操作。使用方式如下:
[HttpPost("CreateServerConfigAsync")]
public async Task<ApiResult<bool>> CreateServerConfigAsync(ServerConfigDto dto)
{
var result = await serverConfigService.CreateServerConfigAsync(dto);
return ApiResult<bool>.Ok(result);
}
- 对net core mvc的一些增强操作,包括全局错误拦截器,和接口参数校验失败后的处理,配合ApiResult,使得系统报错时,也能统一返回,使用方式如下,首先在startUp里注册该服务,注意,要放在mvc注册之后:
services.AddControllersWithViews();
services.AddSummerBootMvcExtension(it =>
{
//是否启用全局错误处理
it.UseGlobalExceptionHandle = true;
//是否启用参数校验处理
it.UseValidateParameterHandle = true;
});
4.1 全局错误拦截器使用后的效果 我们可以直接在业务代码里抛出错误,全局错误拦截器会捕捉到该错误,然后使用统一格式返回给前端,业务代码如下:
private void ValidateData(EnvConfigDto dto)
{
if (dto == null)
{
throw new ArgumentNullException("参数不能为空");
}
if(dto.ServerConfigs==null|| dto.ServerConfigs.Count==0)
{
throw new ArgumentNullException("环境下没有配置服务器");
}
}
如果业务代码里报错,则返回值如下:
{
"code": 40000,
"msg": "Value cannot be null. (Parameter '环境下没有配置服务器')",
"data": null
}
4.2 接口参数校验失败后的处理的效果 我们在接口的参数dto里添加校验注解,代码如下
public class EnvConfigDto : BaseEntity
{
/// <summary>
/// 环境名
/// </summary>
[Required(AllowEmptyStrings = false, ErrorMessage = "环境名称不能为空")]
public string Name { get; set; }
/// <summary>
/// 环境下对应的服务器
/// </summary>
[NotMapped]
public List<int> ServerConfigs { get; set; }
}
如果参数校验不通过,则返回值如下:
{
"code": 40000,
"msg": "环境名称不能为空",
"data": null
}
- QueryCondition,lambda查询条件组合,解决前端传条件过来进行过滤查询的痛点,除了基本的And和Or方法,还添加了更人性化的方法,一般前端传过来的dto里的属性,有字符串类型,如果他们有值则添加到查询条件里,所以特地提取了2个方法,包括了AndIfStringIsNotEmpty(如果字符串不为空则进行and操作,否则返回原表达式),OrIfStringIsNotEmpty(如果字符串不为空则进行or操作,否则返回原表达式), 同时dto里的属性,还有可能是nullable类型,即可空类型,比如 int? test代表用户是否填写某个过滤条件,如果hasValue则添加到查询条件里,所以特地提取了2个方法,AndIfNullableHasValue(如果可空值不为空则进行and操作,否则返回原表达式),OrIfNullableHasValue(如果可空值不为空则进行and操作,否则返回原表达式)用法如下:
//dto
public class ServerConfigPageDto : IPageable
{
public int PageNumber { get; set; }
public int PageSize { get; set; }
/// <summary>
/// ip地址
/// </summary>
public string Ip { get; set; }
/// <summary>
/// 连接名
/// </summary>
public string ConnectionName { get; set; }
public int? Test { get; set; }
}
//condition
var queryCondition = QueryCondition.True<ServerConfig>()
.And(it => it.Active == 1)
//如果字符串不为空则进行and操作,否则返回原表达式
.AndIfStringIsNotEmpty(dto.Ip, it => it.Ip.Contains(dto.Ip))
//如果可空值不为空则进行and操作,否则返回原表达式
.AndIfNullableHasValue(dto.Test,it=>it.Test==dto.Test)
.AndIfStringIsNotEmpty(dto.ConnectionName,it=>it.ConnectionName.Contains(dto.ConnectionName));
var queryResult = await serverConfigRepository.Where(queryCondition)
.Skip((dto.PageNumber - 1) * dto.PageSize).Take(dto.PageSize).ToPageAsync();