实战项目-笔记

实战项目-苍穹外卖

这是一个基于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.Encodingstring转换为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")))