实战项目-笔记
实战项目-苍穹外卖
这是一个基于ASP.NET Core WebAPI框架(.NET6.0)重置的苍穹外卖
我会在这里记录学习过程中的任何问题和笔记
项目
目录名 | 作用 |
---|---|
Storage | 用于存放实体类和DbContext,也可以叫做EFCore |
Dal | 根据业务进一步调用DbContext,封装成具体业务方法,像是仓储模式 |
Model | 存放Vo,Dto,常量和枚举类型,自定义特性 |
Bll | 业务层代码 |
Commen | 存放工具类 |
EFCore
在EFCore的DbContext做实体类和表的映射时,我们通常用实体类加s来作为属性名
这是一种约定,通常用来表示这是一组实体,一个集合
在Code First开发模式中,迁移插件会自动将这个加上s的属性作为表名来迁移(可以手动配置)
isAsNoTracking
在对Dao层方法创建时,我们可以提供一个形参isAsNoTracking = true来决定是否进行实体跟踪
如果该Dao层方法仅用来查询,我们可以禁用实体跟踪,可以减少内存使用并提高查询性能
注:虽然EFCore不进行实体跟踪,但其数据库查询到的主键仍然会回显
// 常规查询,不禁用系统跟踪
public xiaobai_cangqiong_Storage.Entity.Employee? GetByUsername(string username)
{
return _dbContext.Employees.FirstOrDefault(e => e.Username == username);
}
// 利用isAsNoTracking形参和AsNoTracking方法禁用系统跟踪
public xiaobai_cangqiong_Storage.Entity.Employee? GetByUsername(string username, bool isAsNoTracking = true)
{
var data = _dbContext.Employees.Where(e => e.Username == username);
if (isAsNoTracking)
{
return data.AsNoTracking().FirstOrDefault();
}
return data.AsNoTracking().FirstOrDefault();
}
SaveChange
在Java中,我们通过添加过滤器的方式来实现对公共字段的填充
在DBContext中,我们可以通过重写SaveChanges方法的方式,在每次调用前进行一些操作
public override int SaveChanges()
{
OnBeforeSaving();
return base.SaveChanges();
}
public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess,
CancellationToken cancellationToken = default)
{
OnBeforeSaving();
return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
}
private void OnBeforeSaving()
{
}
IAuditable接口
-
类型安全:通过实现
IAuditable
接口,你可以确保所有需要自动填充的实体类都具有相同的属性结构。这有助于避免在运行时出现类型不匹配的问题。 -
代码复用:接口提供了一种标准化的方式来定义一组公共属性,使得你在处理这些实体时可以使用统一的逻辑。
-
易于扩展:如果你将来需要添加更多的审计字段或修改现有的字段,只需要修改接口即可,而不需要逐一修改每个实体类。
public interface IAuditable
{
DateTime CreateTime { get; set; }
DateTime UpdateTime { get; set; }
long CreateUser { get; set; }
long UpdateUser { get; set; }
}
让具有公共字段的实体类实现这个接口
OnBeforeSaving
private void OnBeforeSaving()
{
// 遍历所有实现了IAuditable接口的实体条目
foreach (var entry in ChangeTracker.Entries<IAuditable>())
{
// 检查实体的状态
if (entry.State == EntityState.Added)
{
// 如果实体是新添加的
entry.Entity.CreateTime = DateTime.Now;
entry.Entity.UpdateTime = DateTime.Now;
entry.Entity.CreateUser =
long.Parse(_httpContextAccessor.HttpContext?.Items["UserId"]?.ToString() ?? string.Empty);
entry.Entity.UpdateUser =
long.Parse(_httpContextAccessor.HttpContext?.Items["UserId"]?.ToString() ?? string.Empty);
}
else if (entry.State == EntityState.Modified)
{
// 如果实体已被修改
entry.Entity.UpdateTime = DateTime.Now;
entry.Entity.UpdateUser =
long.Parse(_httpContextAccessor.HttpContext?.Items["UserId"]?.ToString() ?? string.Empty);
}
}
}
AutoMapper
在调用AutoMapper时,有两种方式
将employee实体类映射到EmployeeLoginResp实体类
// 在调用方法时,实例化被映射对象
var employeeLoginResp = _mapper.Map<EmployeeLoginResp>(employee);
// 已经存在被映射对象,可直接映射
var employeeLoginResp = new EmployeeLoginResp();
_mapper.Map(employee, employeeLoginResp);
奇淫巧计
这里非常感谢雨哥和翔哥给出纠正,再正式项目中,表中一定会存在特别为其置空的字段,那么对与这种方法是完全错误的思路,还是要针对于某一个业务制定特定的接口才行
我们将update更新方法通过if判断的方式写成动态的update,可以满足所有业务的更新需求
public void Update(xiaobai_cangqiong_Storage.Entity.Category category)
{
var single = _dbContext.Categories.Single(c => c.Id == category.Id);
if (category.Type != null)
{
single.Type = category.Type;
}
if (category.Name != null)
{
single.Name = category.Name;
}
if (category.Sort != null)
{
single.Sort = category.Sort;
}
if (category.Status != null)
{
single.Status = category.Status;
}
if (category.UpdateTime != null)
{
single.UpdateTime = category.UpdateTime;
}
if (category.UpdateUser != null)
{
single.UpdateUser = category.UpdateUser;
}
_dbContext.SaveChanges();
}
但这样写,当实体类属性(表中的列)过多时,我们需要动态判断的属性太多了,太过于麻烦
我们可以通过AutoMapper映射自己,做非空字段的映射
CreateMap<Category, Category>()
.ForAllMembers(opt =>
opt.Condition((src, dest, srcMember) =>
srcMember != null));
ForAllMembers
方法用于为所有成员(属性)应用相同的条件。opt.Condition
方法定义一个条件,只有当条件满足时才进行属性映射。src
:源对象(即要映射的对象)。dest
:目标对象(即被映射的对象)。srcMember
:源对象的成员(属性)。srcMember != null
:只有当源对象的属性值不为null
时,才进行映射。
需要注意的是,自我映射的非空判断仅仅会判断引用类型的数据是否为空,但在我们数据表中,很多字段为基本数据类型,默认为0
对于想要进行自我映射实现动态更新,必须保证实体类中基本类型属性,例如:int,long……这一类型数据必须为int?,long?
因为如果为基本数据类型,自我映射时进行非空判断会出现问题,基本数据类型的默认为0,但如果是int?数据类型,默认为null
JWT
我们选择使用JWT来签发Token进行项目的身份验证
依赖导入
Microsoft.AspNetCore.Authentication.JwtBearer
配置文件
"Jwt": {
"Key": "<your_secret_key>",
"Issuer": "<your_issuer>",
"Audience": "<your_audience>"
}
注册服务
#region 注册JWT服务
// 注册JWT工具类
builder.Services.AddScoped<JwtUtil>();
// 配置身份验证服务
builder.Services.AddAuthentication(x =>
{
// 设置默认的身份验证方案为JWT承载认证
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
// 设置默认的挑战方案为JWT承载认证
// 挑战方案用于处理未认证的请求,例如返回401状态码
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
// 是否要求HTTPS元数据,默认为true。设置为false允许在非HTTPS连接下接收和验证JWT令牌
x.RequireHttpsMetadata = false;
// 是否在认证过程中保存令牌,这对于调试和日志记录很有用
x.SaveToken = true;
// 配置令牌验证参数
x.TokenValidationParameters = new TokenValidationParameters
{
// 是否验证签名密钥,确保令牌是由正确的密钥签名的
ValidateIssuerSigningKey = true,
// 提供用于验证令牌的密钥
// 因为SymmetricSecurityKey方法需求的是一个字节数组,所以在此调用Encoding.ASCII.GetBytes将字符串转化
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(builder.Configuration["Jwt:Key"] ?? throw new InvalidOperationException())),
// 是否验证发行者
ValidateIssuer = true,
// 发行者值
ValidIssuer = builder.Configuration["Jwt:Issuer"],
// 是否验证受众
ValidateAudience = true,
// 受众值
ValidAudience = builder.Configuration["Jwt:Audience"]
};
// 使用自定义JWT Bearer事件
x.EventsType = typeof(CustomJwtBearerEvents);
});
#endregion
注册中间件
// 在授权之前添加身份验证中间件
app.UseAuthentication();
app.UseAuthorization();
JWTUtil工具类
public class JwtUtil
{
// 私有字段,用于存储JWT密钥、发行者和受众
private readonly string _jwtKey;
private readonly string _jwtIssuer;
private readonly string _jwtAudience;
// 构造函数,通过依赖注入接收 IConfiguration 对象
// IConfiguration 对象用于从配置文件中读取JWT相关的设置
public JwtUtil(IConfiguration configuration)
{
_jwtKey = configuration["Jwt:Key"] ?? throw new InvalidOperationException(); // 读取JWT密钥
_jwtIssuer = configuration["Jwt:Issuer"] ?? throw new InvalidOperationException(); // 读取发行者
_jwtAudience = configuration["Jwt:Audience"] ?? throw new InvalidOperationException(); // 读取受众
}
// 生成JWT令牌的方法
// 参数:
// - userId: 用户ID,用于生成令牌的唯一标识
// - expirationTime: 令牌的有效期
// 返回值:
// - 生成的JWT令牌字符串
public string GenerateToken(string userId, TimeSpan expirationTime)
{
// 创建一个新的JwtSecurityTokenHandler实例
var tokenHandler = new JwtSecurityTokenHandler();
// 将JWT密钥字符串转换为字节数组
var key = Encoding.ASCII.GetBytes(_jwtKey);
// 创建一个SecurityTokenDescriptor对象,用于描述令牌的属性
var tokenDescriptor = new SecurityTokenDescriptor
{
// 设置令牌的主题,包含一个Claim,表示用户的唯一标识
Subject = new ClaimsIdentity(new Claim[]
{
new Claim(ClaimTypes.Name, userId)
}),
// 设置令牌的过期时间
Expires = DateTime.UtcNow.Add(expirationTime),
// 设置令牌的发行者
Issuer = _jwtIssuer,
// 设置令牌的受众
Audience = _jwtAudience,
// 设置签名凭据,使用对称密钥和HMAC SHA256算法进行签名
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
// 使用JwtSecurityTokenHandler创建一个JwtSecurityToken对象
var token = tokenHandler.CreateToken(tokenDescriptor);
// 将JwtSecurityToken对象序列化为字符串
return tokenHandler.WriteToken(token);
}
}
注意事项
JWT签发的token为键值对Token,而JWT通过特性来验证Token时默认采用以下格式:
Authorization: Bearer <token>
但苍穹外面的前端项目的token负载采用的是以下格式:
Token: <token>
CustomJwtBearerEvents
自定义JWT处理业务方式与逻辑
public class CustomJwtBearerEvents : JwtBearerEvents
{
// 当接收到消息时触发,用于处理自定义的令牌头
public override Task MessageReceived(MessageReceivedContext context)
{
// 从请求头中获取 "Token" 字段的值
var token = context.Request.Headers["Token"].FirstOrDefault();
// 如果 "Token" 字段存在且不为空,则将其赋值给 context.Token
if (!string.IsNullOrEmpty(token))
{
context.Token = token;
}
// 返回基类的实现
return base.MessageReceived(context);
}
}
MD5工具类
public static class MD5Util
{
public static string GetMd5Hash(string input)
{
// 使用MD5CryptoServiceProvider创建一个MD5哈希对象
using (MD5 md5 = MD5.Create())
{
// 将输入字符串转换为字节数组,并计算哈希
byte[] inputBytes = Encoding.UTF8.GetBytes(input);
byte[] hashBytes = md5.ComputeHash(inputBytes);
// 将字节数组转换成十六进制字符串
StringBuilder sb = new StringBuilder();
for (int i = 0; i < hashBytes.Length; i++)
{
sb.Append(hashBytes[i].ToString("x2"));
}
return sb.ToString();
}
}
}
Swagger
AddSwaggerGen
// 添加Swagger生成器
builder.Services.AddSwaggerGen(options =>
{
//创建一个Swagger文档,命名为"v1",并提供API的基本信息
options.SwaggerDoc("v1",new OpenApiInfo()
{
Version = "v1",
Title = "xiaobai-cangqiong API",
});
}
);
这里如果不创建Swagger文档,系统也会默认帮我们创建一个文档
AddSecurityDefinition与AddSecurityRequirement
在Swagger中添加一个可以携带token请求头的按钮,
#region MyRegion Swagger
builder.Services.AddSwaggerGen(
options =>
{
// 添加一个安全定义,定义名称为"Token"
options.AddSecurityDefinition("Token", new OpenApiSecurityScheme
{
// 描述如何使用Token进行身份验证
Description = "Token: <token>",
// 指定认证头的名称,这里是"Token"
Name = "Token",
// 指定认证信息应该放置的位置,这里是HTTP头部
In = ParameterLocation.Header,
// 指定安全方案的类型,这里是指API密钥类型
Type = SecuritySchemeType.ApiKey,
});
// 添加安全要求,这将告诉Swagger UI哪些安全方案是必需的
options.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
// 引用之前定义的安全方案
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Token" // 对应于上面定义的安全方案的Id
},
},
new List<string>() // 空列表表示不需要特定的作用域
}
});
});
#endregion
HttpContext.Items
ASP.NETCore提供了非常好用的方式来获取到用户信息,而不是通过丑陋的过滤器手动将用户信息保存来实现
在Java中,我们通过ThreadLocal来存储过滤器从Token中解析出来的UserId
在ASP.NETCore中,HttpContext.Items可以解决中间件、过滤器、控制器和其他组件在处理同一个请求时共享数据,而无需通过参数传递
TokenValidationFilter
public class TokenValidationFilter : IAsyncActionFilter
{
private readonly JwtUtil _jwtUtil;
public TokenValidationFilter(JwtUtil jwtUtil)
{
_jwtUtil = jwtUtil;
}
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
// 从请求头中获取Token
var token = context.HttpContext.Request.Headers["Token"].ToString();
if (!string.IsNullOrEmpty(token))
{
try
{
// 使用 JWTUtils 解析 Token 获取 UserId
var userId = _jwtUtil.ParseToken(token);
// 将 UserId 添加到 HttpContext.Items,以便后续处理逻辑使用
context.HttpContext.Items["UserId"] = userId;
}
catch (System.Exception ex)
{
// 如果解析失败,返回未授权的状态码
context.Result = new UnauthorizedResult();
return;
}
}
else
{
// 如果没有提供Token,也可以选择返回未授权
context.Result = new UnauthorizedResult();
return;
}
// 继续执行下一个过滤器或控制器操作
await next();
}
}
注册filter
在需要使用UserId的请求的控制器中加入[TypeFilter(typeof(TokenValidationFilter))]特性
一般为增加和修改的业务上
/// <summary>
/// 新增员工
/// </summary>
/// <param name="employeeSaveReq"></param>
/// <param name="token"></param>
/// <returns></returns>
[HttpPost]
[TypeFilter(typeof(TokenValidationFilter))]
public Result<string> Save([FromBody] EmployeeSaveReq employeeSaveReq)
{
_logger.LogInformation("新增员工:{}", employeeSaveReq);
_employeeService.Save(employeeSaveReq);
return Result<string>.Success();
}
IHttpContextAccessor
在使用HttpContext.Items时,我们要注册服务之后才能在控制器/服务层中调用,获取到存储在Items中的值
注册服务
builder.Services.AddHttpContextAccessor();
// 注入依赖
private readonly IHttpContextAccessor _httpContextAccessor;
public UserService(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
employee.CreateUser = long.Parse(_httpContextAccessor.HttpContext?.Items["UserId"]?.ToString() ?? string.Empty);
employee.UpdateUser = long.Parse(_httpContextAccessor.HttpContext?.Items["UserId"]?.ToString() ?? string.Empty);
配合OnBeforeSaving实现每次存储前自动填充字段
注意事项
默认情况下,类库项目只会引用.NET框架而不会引用ASP.NETCore框架,所以会存在在别的层无法注入依赖的情况
比如_httpContextAccessor.HttpContext对象无法注入在Bll的Service中
我们可以通过项目文件来为项目添加ASP.NETCore框架
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
OSS
依赖安装
Aliyun.OSS.SDK.NetCore
注:依赖导入Aliyun.OSS.SDK可能会出现意料之外的问题
配置文件
"OSS": {
"Endpoint": "oss-cn-qingdao.aliyuncs.com",
"AccessKeyId": "your_access_key_id",
"AccessKeySecret": "your_access_key_secret",
"BucketName": "your_bucket_name"
}
工具类
public class AliOssUtil
{
private readonly string _endpoint;
private readonly string _accessKeyId;
private readonly string _accessKeySecret;
private readonly string _bucketName;
public AliOssUtil(IConfiguration configuration)
{
_endpoint = configuration["OSS:Endpoint"] ?? throw new InvalidOperationException();
_accessKeyId = configuration["OSS:AccessKeyId"] ?? throw new InvalidOperationException();
_accessKeySecret = configuration["OSS:AccessKeySecret"] ?? throw new InvalidOperationException();
_bucketName = configuration["OSS:BucketName"] ?? throw new InvalidOperationException();
}
public string Upload(Stream fileStream, string fileName)
{
var ossClient = new OssClient(_endpoint, _accessKeyId, _accessKeySecret);
// 上传文件到指定的Bucket
ossClient.PutObject(_bucketName, fileName, fileStream);
//返回拼接好的url
return $"https://{_bucketName}.{_endpoint}/{fileName}";
}
}
注册服务
services.AddSingleton<AliOssUtil>();
阿里云oss注册为单例服务即可
使用服务
[HttpPost("upload")]
public Result<string> Upload([FromForm]IFormFile file)
{
_logger.LogInformation("文件上传:{}", file);
using var stream = file.OpenReadStream();
return Result<string>.Success(_aliOssUtil.Upload(stream, file.FileName));
}
Settings
我们在Jwt和Oss一类的工具类中,直接使用了IConfiguration注入的方式来注入配置参数
private readonly string _endpoint;
private readonly string _accessKeyId;
private readonly string _accessKeySecret;
private readonly string _bucketName;
public AliOssUtil(IConfiguration configuration)
{
_endpoint = configuration["OSS:Endpoint"] ?? throw new InvalidOperationException();
_accessKeyId = configuration["OSS:AccessKeyId"] ?? throw new InvalidOperationException();
_accessKeySecret = configuration["OSS:AccessKeySecret"] ?? throw new InvalidOperationException();
_bucketName = configuration["OSS:BucketName"] ?? throw new InvalidOperationException();
}
我们可以创建Settings配置类,来简化配置注入,将服务注入为强类型
配置类
public class OssSettings
{
public string Endpoint { get; set; } = null!;
public string AccessKeyId { get; set; } = null!;
public string SecretAccessKey { get; set; } = null!;
public string BucketName { get; set; } = null!;
}
注册服务
builder.Services.Configure<JwtSettings>(builder.Configuration.GetSection("Jwt"));
builder.Services.Configure<OssSettings>(builder.Configuration.GetSection("OSS"));
注入服务
private readonly OssSettings _ossSettings;
public AliOssUtil(IOptions<OssSettings> ossSettings)
{
_ossSettings = ossSettings.Value;
}
事务
在此项目中,我们使用的是DBFirst开发模式,并且用的是逻辑外键的开发形式,不存在数据库层级的物理外键,也不存在级联关系的插入和查询
所以我们要在Service层完成对两个表的分别操作(增删改),我们就需要在Service层加入事务,来保证操作的原子性
在原Java项目中,我们可以通过Spring框架提供的Transactional注解来自动实现整个方法的事务,在ASP.NETCore中,没有类似这个注解的特性
所以我们要通过TransactionScope对象来实现事务
using (var scope = new TransactionScope())
{
scope.Complete();
}
小贴士
对于List<T>, ICollection<T>, IReadOnlyCollection<T> 等实现了 IEnumerable<T> 接口的集合
dishFlavors!=null && dishFlavors.Any();
// 替换
dishFlavors!=null && dishFlavors.Length>0;
使用Any方法替换Length方法对集合做非空判断,性能更好,但数组的情况下无法使用Any方法,仍使用Length做非空判断
EFCore的事务实现
EFCore的SaveChanges
会自动实现事务,也可以使用DbContext.Database.BeginTransaction()
方法开始一个事务,并且使用 Commit()
或 Rollback()
来提交或回滚事务
- 优点:避免了TransactionScope事务的性能问题、连接池问题、外部依赖问题
- 缺点:需要在service层注入DbContext对象
// 异步方法的调用
using (var transaction = await _dbContext.Database.BeginTransactionAsync())
{
try
{
// 提交事务
await transaction.CommitAsync();
}
catch
{
// 回滚事务
await transaction.RollbackAsync();
throw;
}
}
Redis
注入依赖
Microsoft.Extensions.Caching.StackExchangeRedis
注册服务
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = builder.Configuration["Redis:Configuration"];
options.InstanceName = builder.Configuration["Redis:InstanceName"];
});
注入服务
// Redis
private readonly IDistributedCache _distributedCache;
public ShopController(ILogger<ShopController> logger, IDistributedCache distributedCache)
{
_distributedCache = distributedCache;
}
Get&Set
_distributedCache(分布式缓存)服务常用的两个方法为Get(GetAsync)和Set(SetAsync),使用起来简单灵活
但需要注意,这两个方法的参数为字节数组,而通常我们是需要将其他数据类型存入Redis,这就涉及到了数据类型转换的问题
简单数据类型
对于简单的数据类型(如整数、布尔值等),可以直接使用 BitConverter.GetBytes
转换为 byte[]
在反序列化时,调用BitConverter.ToInt32
字符串类型
通过System.Text.Encoding
将string
转换为byte[]
// 序列化字符串到byte[]
Encoding.UTF8.GetBytes(str);
// 反序列化
Encoding.UTF8.GetString(bytes);
复杂类型
对于复杂类型的对象,我们通常先调用JsonSerializer.Serialize()
将其序列化成字符串,然后再将字符串序列化为byte[]
// 序列化
byte[] bytes = Encoding.UTF8.GetBytes(jsonString);
await _distributedCache.SetAsync("SHOP_STATUS", bytes, new DistributedCacheEntryOptions());
// 反序列化
string jsonString = Encoding.UTF8.GetString(cachedBytes);
var deserializedStatusInfo = JsonSerializer.Deserialize<YourType>(jsonString);
SetString&GetString
在.NET6之后的版本中,_distributedCache为我们额外提供了SetString(SetStringAsync)和GetString(GetStringAsync)
简化了我们序列化和反序列化的操作,至此,我们可以将所有想要存入redis的对象都序列化为字符串直接存入
注意事项
在这个项目中,我们为用户端的菜品及套餐的查询操作应用了缓存,在管理端的菜品增删改中实现了清空缓存达到及时更新数据
我们使用工具类CacheUtils将增删查的操作封装,简化了代码
控制器命名
在spring中,如果同时存在user包的ShopController和Admin包的ShopController,在没有指定bean名称的时候,会抛出异常,因为这两个Controller都会被注入到IoC容器中,名称会冲突,但为什么ASP.NETCore中就可以存在两个相同名称的Controller呢?
在 ASP.NET Core 中,MVC 和 Razor Pages 的默认行为是基于约定来发现控制器。MVC 会扫描程序集中的所有类型,并根据命名规则(例如类名以 "Controller" 结尾)来识别控制器。当找到多个具有相同名称的控制器时,ASP.NET Core 不会直接抛出异常,而是通过路由系统来区分这些控制器。
- 路由配置:每个控制器通常有自己的路由前缀,即使两个控制器有相同的名称,它们的完整路径(包括命名空间)通常是不同的
- 命名空间:即使没有显式的路由配置,ASP.NET Core 也会使用控制器的命名空间作为默认的路由前缀。这意味着如果两个控制器位于不同的命名空间中,它们的默认路由也将是不同的。
这两种框架的不同设计哲学和实现方式导致了在处理同名控制器或 Bean 时的行为差异。ASP.NET Core 更加依赖于路由和命名空间来区分控制器,而 Spring 则要求开发者明确指定 Bean 的名称或使用限定符来避免冲突。
HttpClient
在.NETCore中已经内置了HttpClient
注册服务
// 注册HttpClient服务
builder.Services.AddHttpClient("WxLogin", client =>
{
client.BaseAddress = new Uri("https://api.weixin.qq.com/");
client.Timeout = TimeSpan.FromSeconds(5);
});
注入依赖
// 获取HttpClient对象
private readonly HttpClient _httpClient;
public UserServiceDao(IOptions<WxSetting> wxSetting, IHttpClientFactory httpClientFactory)
{
_httpClient = httpClientFactory.CreateClient("WxClient");
}
发送请求
// 拼接uri参数
var uriBuilder = new UriBuilder(_httpClient.BaseAddress);
uriBuilder.Query = QueryHelpers.AddQueryString(uriBuilder.Query, new Dictionary<string, string?>
{
{ "appId", _wxSetting.AppId },
{ "secret", _wxSetting.AppSecret },
{ "js_code", code },
{ "grant_type", _wxSetting.GrantType }
});
// 发送请求
var response = await _httpClient.GetAsync(uriBuilder.Uri);
if (response.IsSuccessStatusCode)
{
// 将响应中的内容读出来
result = await response.Content.ReadAsStringAsync();
}
时间区间
将开始时间和结束时间的日期区间中的每一天加入到集合中
List<DateTime> dateList = new List<DateTime>();
while (begin <= end)
{
dateList.Add(begin);
begin = begin.AddDays(1);
}
集合转换字符串
通过string.Join的工具方法,搭配Linq的select语句可以将集合转换为字符串并指定换行符
DateList = string.Join(",", dateList.Select(d => d.ToString("yyyy-MM-dd"))),
TurnoverList = string.Join(",", sum.Select(d => d.ToString("0.00")))