简介:开箱即用的.NET WebApi基础工程模板,内置Dapper和EFCore双ORM支持,适配不同数据访问场景;Autofac实现模块化依赖注入,已预配置生命周期管理;WebApi接口层提供AccountController基础认证、OAuth2授权服务(OAuthServerProvider)、HTTP客户端封装(HttpManager)及统一日志入口(LogManager);Swagger文档自动集成并增强接口描述能力(SwaggerControllerDescProvider);所有配置文件(Web.config、log4net.config、Swagger相关设置等)均按Debug/Release环境区分,repositories.config支持数据访问策略切换;目录中保留多版本.config文件,体现真实部署所需的环境适配逻辑,适合快速搭建中小型RESTful服务或作为团队标准化开发起点。
1. 项目概述:为什么这个模板值得你花十分钟看懂它
我带过三支.NET后端团队,从2016年用WebApi 2.2搭第一个微服务雏形,到2023年主导重构整套金融风控API网关,踩过的坑基本都刻在脑回沟里了。每次新项目启动,最耗时间的从来不是写业务逻辑——而是反复配置Dapper连接池、调EFCore的DbContext生命周期、改Autofac模块注册顺序、修Swagger在Release模式下不显示XML注释、查log4net在IIS里死活不写日志……这些事单次做可能就半小时,但一年下来,一个团队光在基础框架上重复劳动的时间,轻松超过200人小时。
这个模板,就是我把过去七年所有“又来了又是这一步”的瞬间,压缩成一套真正能双击.sln直接F5跑通的工程骨架。它不是玩具Demo,也不是教科书式示例——目录里那些带环境后缀的.config文件(Web.Debug.config、Web.Release.config)、repositories.config里明晃晃写着<add key="DataAccessStrategy" value="Dapper" />、LogManager类里那行LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType),全是你上线前夜还在手抖调试的真实战场痕迹。
核心关键词就五个:Dapper、EFCore、Autofac、WebApi、Swagger——但它们不是简单堆砌。Dapper负责报表导出、实时查询这类对性能敏感的场景;EFCore接管订单事务、库存扣减这类需要完整变更跟踪和复杂关系映射的业务;Autofac不是只注册个IDbConnection,而是按请求生命周期(InstancePerRequest)管理EFCore的DbContext,按单例(SingleInstance)复用Dapper的DbConnectionFactory;Swagger文档不是自动生成完就完事,而是通过SwaggerControllerDescProvider把/// <summary>里的中文注释,原样塞进接口描述框,连[ProducesResponseType(401)]都能自动转成“未授权”状态码说明。
适合谁?如果你正要:
- 三天内交付一个对接微信小程序的用户中心API;
- 给遗留系统补一套RESTful数据同步接口,但DB是Oracle老库,必须用Dapper绕过EFCore的兼容性问题;
- 带新人快速上手团队技术栈,不用解释“为什么Autofac要配PropertiesAutowired()”;
- 或者只是想确认自己写的HttpManager是否漏了HttpClient的DNS缓存配置……
那这个包就是你VS里该第一个打开的解决方案。
它不承诺“零配置”,但保证你改的每一行配置,都有明确上下文——比如Web.Release.config里<add key="EnableSwagger" value="false" />旁边,注释写着:“生产环境禁用Swagger UI,避免API结构暴露,但XML文档仍生成供内部SDK使用”。这种细节,才是省下你两小时排查时间的关键。
2. 整体架构设计与选型逻辑:为什么是这套组合,而不是其他方案
2.1 双ORM并存的设计哲学:不是炫技,是为真实业务兜底
很多人看到“同时集成Dapper和EFCore”第一反应是:“何必呢?选一个不就行了?”——这话在Demo里绝对成立,但在生产环境,它等于让外科医生只带一把手术刀进手术室。我们拆解两个真实场景:
场景一:财务对账报表接口
需求:每分钟拉取上一分钟全量交易流水(约5万条),按商户ID聚合统计,生成Excel返回。
- EFCore劣势:加载5万实体到内存,GC压力飙升,AsNoTracking()也救不了序列化开销;生成SQL时若用GroupBy嵌套子查询,Oracle 11g直接报错“ORA-00979: not a GROUP BY expression”。
- Dapper优势:QueryAsync<dynamic>(sql, param)直连数据库,结果集不映射实体,内存占用稳定在8MB以内;SQL可手写WITH t AS (...) SELECT * FROM t GROUP BY merchant_id,兼容性拉满。
场景二:订单创建事务
需求:插入订单主表→插入订单明细表→扣减商品库存→记录操作日志,四步必须原子性。
- Dapper劣势:手动写BEGIN TRAN/COMMIT易出错;库存扣减需UPDATE goods SET stock = stock - @num WHERE id = @id AND stock >= @num,失败时得自己解析SQL错误码。
- EFCore优势:DbContext.SaveChanges()天然事务包裹;stock >= @num条件可转为LINQ表达式,失败时抛DbUpdateConcurrencyException,捕获后直接返回“库存不足”。
所以模板里IDPRepository不是抽象接口摆设,而是明确划分职责:
// DapperRepository.cs —— 负责查询、报表、批量导入导出
public class DapperRepository : IDPRepository
{
private readonly IDbConnectionFactory _factory; // 单例,连接字符串由Web.config注入
public async Task<IEnumerable<dynamic>> GetReportData(string sql, object param)
=> await _factory.GetOpenConnection().QueryAsync<dynamic>(sql, param);
}
// EFCoreRepository.cs —— 负责增删改、事务、复杂关系
public class EFCoreRepository : IEFCoreRepository
{
private readonly AppDbContext _context; // InstancePerRequest,确保同一请求内共享DbContext
public async Task<Order> CreateOrder(Order order)
{
await _context.Orders.AddAsync(order);
await _context.SaveChangesAsync(); // 自动开启事务
return order;
}
}
提示:
repositories.config是运行时开关,不是编译时配置。应用启动时读取该文件,决定IDPRepository绑定到DapperRepository还是EFCoreRepository,切换无需重新编译——这点对灰度发布至关重要。
2.2 Autofac依赖注入:为什么不用.NET Core原生DI,而坚持Autofac
.NET 5+原生DI足够好,但Autofac在三个关键点上不可替代:
第一,模块化注册粒度更细。
原生DI的AddScoped<T>()只能按类型注册,而Autofac支持按命名空间批量注册:
// AutofacModule.cs
public class DataModule : Module
{
protected override void Load(ContainerBuilder builder)
{
// 所有继承IDPRepository的类,按实现类名注册(如DapperRepository → IDPRepository)
builder.RegisterAssemblyTypes(ThisAssembly)
.Where(t => t.IsClass && t.Name.EndsWith("Repository"))
.AsImplementedInterfaces()
.InstancePerRequest();
// 特殊处理:LogManager必须单例,且构造函数注入ILog
builder.RegisterType<LogManager>()
.AsSelf()
.SingleInstance()
.WithParameter(new ResolvedParameter(
(pi, ctx) => pi.ParameterType == typeof(ILog),
(pi, ctx) => LogManager.GetLogger("Global")));
}
}
这段代码意味着:当你新增ProductRepository类,只要它实现IDPRepository,Autofac自动注册,无需在Startup里追加一行services.AddScoped<IDPRepository, ProductRepository>()。
第二,属性注入(PropertiesAutowired)解决循环依赖硬伤。
AccountController需要IAuthManager校验Token,IAuthManager又需要LogManager记日志,LogManager初始化又要IConfiguration读配置——典型的三角依赖。原生DI遇到循环依赖直接抛异常,而Autofac允许:
builder.RegisterType<AccountController>()
.PropertiesAutowired(); // 关键!让Autofac在实例化后,自动注入public属性
此时AccountController可定义:
public class AccountController : ApiController
{
public IAuthManager AuthManager { get; set; } // Autofac自动赋值
public LogManager Logger { get; set; } // 同样自动赋值
}
第三,生命周期管理更贴近WebApi语义。
InstancePerRequest不是简单等同于Scoped——它绑定到HttpContext.Current,即使你在异步方法里await Task.Run(() => { /* 访问注入对象 */ }),Autofac仍能从当前请求上下文获取正确的实例。而原生DI的Scoped在Task.Run中会丢失作用域,导致OperationCanceledException。
2.3 Swagger集成策略:文档即契约,不是装饰品
模板里的Swagger不是app.UseSwagger()加app.UseSwaggerUI()就完事。它解决三个实际痛点:
痛点1:Debug环境能看文档,Release环境被禁用,但SDK开发者仍需XML注释。
解决方案:SwaggerConfig.cs里区分配置:
if (isDebug) // 从Web.config读取<add key="EnableSwagger" value="true" />
{
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "API v1");
c.DocExpansion(DocExpansion.None); // 折叠所有接口,避免页面卡顿
});
}
// 无论是否启用UI,XML文档始终生成
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, xmlFile);
if (File.Exists(xmlPath))
{
c.IncludeXmlComments(xmlPath); // 这行必须保留!
}
痛点2:[HttpGet]接口的<summary>注释,在Swagger里显示为“undefined”。
原因:ASP.NET WebApi默认不生成XML文档,或生成路径不对。模板强制在项目属性→“生成”页勾选“XML文档文件”,路径设为bin\$(ProjectName).xml,且SwaggerConfig.cs中IncludeXmlComments指向该路径。
痛点3:OAuth2授权按钮在Swagger UI里点不动。
因为OAuthServerProvider返回的Token格式与Swagger期望不符。模板在SwaggerControllerDescProvider.cs里重写:
public class SwaggerControllerDescProvider : IDocumentFilter
{
public void Apply(SwaggerDocument swaggerDoc, DocumentFilterContext context)
{
// 为所有带[Authorize]特性的控制器,自动添加OAuth2安全定义
foreach (var path in swaggerDoc.Paths.Values)
{
foreach (var operation in path.Operations.Values)
{
if (operation.OperationId.Contains("Authorize"))
{
operation.Security = new List<IDictionary<string, IEnumerable<string>>>
{
new Dictionary<string, IEnumerable<string>>
{
["oauth2"] = new[] { "read", "write" }
}
};
}
}
}
}
}
配合Web.config里<add key="OAuth2TokenUrl" value="http://localhost:5000/token" />,Swagger UI的“Authorize”按钮就能正确跳转。
3. 核心组件详解与实操要点:从配置到代码的逐层穿透
3.1 多环境配置体系:.config文件不是摆设,是部署流水线的基石
模板目录下的.config文件绝非冗余备份,而是CI/CD流程中环境变量注入的物理载体。我们以Web.config为例,拆解其分层结构:
第一层:基础配置(Web.config)
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net" />
</configSections>
<appSettings>
<!-- 公共配置,所有环境一致 -->
<add key="AppVersion" value="1.2.0" />
<add key="DefaultPageSize" value="20" />
</appSettings>
<log4net>
<appender name="FileAppender" type="log4net.Appender.FileAppender">
<file value="logs/app.log" /> <!-- 注意:此处路径是相对路径 -->
<appendToFile value="true" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date [%thread] %-5level %logger - %message%newline" />
</layout>
</appender>
<root>
<level value="INFO" />
<appender-ref ref="FileAppender" />
</root>
</log4net>
</configuration>
第二层:环境差异化配置(Web.Debug.config / Web.Release.config)
这是MSBuild的Transform机制生效处。Web.Debug.config内容:
<?xml version="1.0"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
<appSettings>
<!-- Debug环境覆盖 -->
<add key="EnableSwagger" value="true" xdt:Transform="SetAttributes" xdt:Locator="Match(key)" />
<add key="LogLevel" value="DEBUG" xdt:Transform="SetAttributes" xdt:Locator="Match(key)" />
<!-- 修改log4net输出路径 -->
<log4net>
<appender name="FileAppender" type="log4net.Appender.FileAppender" xdt:Transform="Replace">
<file value="logs/debug/app.log" />
</appender>
</log4net>
</appSettings>
</configuration>
Web.Release.config则相反:
<add key="EnableSwagger" value="false" xdt:Transform="SetAttributes" xdt:Locator="Match(key)" />
<add key="LogLevel" value="WARN" xdt:Transform="SetAttributes" xdt:Locator="Match(key)" />
<log4net>
<appender name="FileAppender" type="log4net.Appender.FileAppender" xdt:Transform="Replace">
<file value="logs/release/app.log" />
</appender>
</log4net>
注意:
xdt:Transform="Replace"表示整个<appender>节点替换,而非仅改<file>值。这是避免Debug配置残留到Release环境的关键。
第三层:运行时动态配置(repositories.config)
此文件不参与编译,部署时由运维修改:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<appSettings>
<!-- 数据访问策略:Dapper(高性能查询)或 EFCore(事务强一致性) -->
<add key="DataAccessStrategy" value="Dapper" />
<!-- Oracle连接串(Release环境专用) -->
<add key="OracleConnectionString" value="Data Source=PRODDB;User Id=app_user;Password=xxx;" />
</appSettings>
</configuration>
代码中读取:
var strategy = ConfigurationManager.AppSettings["DataAccessStrategy"];
if (strategy == "Dapper")
builder.RegisterType<DapperRepository>().As<IDPRepository>();
else
builder.RegisterType<EFCoreRepository>().As<IDPRepository>();
实操心得:
- 在Azure DevOps或Jenkins中,用File Transform任务替换.config文件,比写PowerShell脚本更稳定;
- log4net的<file>路径必须用相对路径(如logs/app.log),绝对路径在IIS中常因权限问题写入失败;
- Web.Release.config里禁用Swagger后,务必保留IncludeXmlComments,否则SDK生成工具(如NSwag)无法读取接口注释。
3.2 日志统一入口:LogManager如何做到“一处配置,全局生效”
LogManager不是简单封装ILog,而是解决三个现实问题:
问题1:不同类库用不同日志框架(log4net/NLog/serilog),日志格式不统一。
模板强制所有日志走LogManager:
public class LogManager
{
private static readonly ILog _globalLogger = LogManager.GetLogger("Global");
public static ILog GetLogger(Type type) => LogManager.GetLogger(type.FullName);
public static ILog GetLogger(string name) => LogManager.GetLogger(name);
// 关键:所有日志方法都经过此入口,便于后期扩展
public static void Info(string message, params object[] args)
=> _globalLogger.InfoFormat(message, args);
public static void Error(Exception ex, string message, params object[] args)
=> _globalLogger.ErrorFormat(ex, message, args);
}
问题2:WebApi中Controller的日志,想自动带上请求ID和用户ID。
LogManager在Application_BeginRequest中注入上下文:
protected void Application_BeginRequest(object sender, EventArgs e)
{
var context = HttpContext.Current;
if (context != null)
{
// 生成唯一请求ID
var requestId = Guid.NewGuid().ToString("N");
context.Items["RequestId"] = requestId;
// 尝试从Token解析用户ID(AccountController中已实现)
var userId = GetUserIdFromToken(context.Request.Headers["Authorization"]);
context.Items["UserId"] = userId ?? "Anonymous";
}
}
然后在LogManager的Info方法里:
public static void Info(string message, params object[] args)
{
var context = HttpContext.Current;
if (context != null && context.Items.Contains("RequestId"))
{
var reqId = context.Items["RequestId"].ToString();
var userId = context.Items["UserId"].ToString();
message = $"[Req:{reqId}][User:{userId}] {message}";
}
_globalLogger.InfoFormat(message, args);
}
问题3:日志文件按日期滚动,但磁盘空间爆满。
log4net.config中配置:
<appender name="RollingFileAppender" type="log4net.Appender.RollingFileAppender">
<file value="logs/app.log" />
<appendToFile value="true" />
<rollingStyle value="Date" /> <!-- 按日期滚动 -->
<datePattern value=".yyyy-MM-dd.lo\g" />
<maxSizeRollBackups value="30" /> <!-- 最多保留30天日志 -->
<maximumFileSize value="10MB" /> <!-- 单文件超10MB自动切分 -->
<staticLogFileName value="false" />
</appender>
实操心得:
<staticLogFileName value="false" />必须设为false,否则datePattern失效;maxSizeRollBackups和maximumFileSize组合使用,避免日志占满C盘——曾有个客户因此导致IIS崩溃,排查三天才发现是log4net配置漏了maximumFileSize。
3.3 OAuth2授权服务:OAuthServerProvider不是黑盒,是可控的令牌工厂
模板中的OAuthServerProvider基于Microsoft.Owin.Security.OAuth实现,但做了三项关键改造:
改造1:令牌有效期动态化
不写死AccessTokenExpireTimeSpan = TimeSpan.FromHours(2),而是从配置读取:
public class CustomOAuthProvider : OAuthAuthorizationServerProvider
{
public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
// 从Web.config读取客户端白名单
var allowedClients = ConfigurationManager.AppSettings["AllowedClients"]?.Split(',');
if (allowedClients?.Contains(context.ClientId) == true)
context.Validated();
}
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
// 校验用户名密码(此处应对接你的用户表)
var user = ValidateUser(context.UserName, context.Password);
if (user == null)
{
context.SetError("invalid_grant", "用户名或密码错误");
return;
}
// 动态设置过期时间:管理员账号7天,普通用户2小时
var expireHours = user.Role == "Admin" ? 168 : 2;
var props = new AuthenticationProperties(new Dictionary<string, string>
{
{ "userName", user.UserName },
{ "userId", user.Id.ToString() }
})
{
IssuedUtc = DateTimeOffset.UtcNow,
ExpiresUtc = DateTimeOffset.UtcNow.AddHours(expireHours)
};
var ticket = new AuthenticationTicket(user.Identity, props);
context.Validated(ticket);
}
}
改造2:刷新令牌(Refresh Token)安全加固
标准OAuth2中,Refresh Token一旦泄露,攻击者可无限续期。模板增加设备指纹绑定:
public override async Task ReceiveRefreshToken(OAuthReceiveRefreshTokenContext context)
{
var refreshTokenId = context.Ticket.Properties.IssuedUtc.ToString("O");
var deviceFingerprint = context.Request.Headers["X-Device-Fingerprint"]; // 前端传设备标识
// 查询数据库,确认该RefreshToken是否绑定当前设备
var isValid = await _tokenService.IsValidForDevice(refreshTokenId, deviceFingerprint);
if (!isValid)
{
context.Rejected();
return;
}
context.Validated();
}
改造3:令牌吊销黑名单
OAuthServerProvider本身不提供吊销能力,模板在AccountController中暴露/api/account/revoke接口:
[HttpPost]
[Route("revoke")]
public IHttpActionResult RevokeToken([FromBody] RevokeTokenRequest request)
{
// 将tokenId加入Redis黑名单,设置过期时间=原令牌剩余有效期
var remainingSeconds = GetRemainingSeconds(request.TokenId);
_redisDatabase.StringSet($"revoked:{request.TokenId}", "1", TimeSpan.FromSeconds(remainingSeconds));
return Ok();
}
并在CustomOAuthProvider.MatchEndpoint中拦截:
public override Task MatchEndpoint(OAuthMatchEndpointContext context)
{
if (context.IsTokenEndpoint && context.Request.Method == "POST")
{
// 检查Refresh Token是否在黑名单
var tokenId = context.Request.Query["refresh_token"];
if (_redisDatabase.KeyExists($"revoked:{tokenId}"))
{
context.Response.StatusCode = 400;
context.Response.Write("invalid_refresh_token");
context.RequestCompleted();
}
}
return Task.CompletedTask;
}
注意:
RevokeTokenRequest需包含tokenId(不是JWT字符串,而是数据库里存储的GUID),这是为防止前端传错参数导致误吊销。
4. 完整实操流程:从零开始搭建一个可用的API服务
4.1 环境准备与项目导入
步骤1:确认开发机环境
- Visual Studio 2022(17.4+)或 Rider 2023.2+
- .NET Framework 4.8 Runtime(必须!模板基于WebApi 2.2,不支持.NET 6+)
- SQL Server Express LocalDB(用于EFCore示例)或 Oracle XE(用于Dapper示例)
步骤2:解压模板包,打开解决方案
- 解压后进入根目录,双击WebApiTemplate.sln
- VS自动恢复NuGet包(若失败,右键解决方案→“还原NuGet包”)
- 检查References中是否有黄色感叹号——若有,手动安装缺失包:
bash Install-Package Dapper -Version 2.1.24 Install-Package EntityFramework -Version 6.4.4 Install-Package Autofac.WebApi2 -Version 6.0.0 Install-Package Swashbuckle.Core -Version 5.6.0
步骤3:配置数据库连接
- 打开Web.config,找到<connectionStrings>节:
xml <connectionStrings> <add name="DefaultConnection" connectionString="Data Source=(LocalDB)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\aspnet-WebApiTemplate-20230301.mdf;Integrated Security=True" providerName="System.Data.SqlClient" /> </connectionStrings>
- 若用SQL Server,保持默认即可;若用Oracle,需:
1. 安装Oracle.ManagedDataAccess NuGet包;
2. 修改providerName="Oracle.ManagedDataAccess.Client";
3. 更新connectionString为Oracle格式。
4.2 首次运行与Swagger验证
步骤1:设置启动项目与端口
- 右键WebApiTemplate项目→“设为启动项目”
- 右键项目→“属性”→“Web”选项卡→“项目 Url”设为http://localhost:5000(避免IIS Express随机端口)
步骤2:执行数据库迁移(仅EFCore首次需要)
- 打开“程序包管理器控制台”,选择WebApiTemplate项目:
powershell Enable-Migrations -ContextTypeName AppDbContext Add-Migration InitialCreate Update-Database
- 此时会在App_Data目录生成aspnet-WebApiTemplate-*.mdf文件,包含Users、Orders等示例表。
步骤3:启动并验证Swagger
- 按Ctrl+F5启动(不调试,避免断点干扰)
- 浏览器打开http://localhost:5000/swagger
- 应看到完整API列表,点击AccountController→POST /api/account/login→“Try it out”:
json { "username": "admin", "password": "123456" }
- 返回200,响应体含access_token字段,证明OAuth服务正常。
提示:若Swagger页面空白,检查浏览器控制台——常见原因是
Web.config中<compilation debug="true">未设为true(Debug环境必需)。
4.3 快速接入业务:以“商品查询接口”为例
假设你要加一个GET /api/products?category=phone接口,返回JSON商品列表。
步骤1:创建Product实体与DTO
- 在Models文件夹新建Product.cs:
csharp public class Product { public int Id { get; set; } public string Name { get; set; } public decimal Price { get; set; } public string Category { get; set; } public DateTime CreatedAt { get; set; } }
- 新建DTOs\ProductResponseDto.cs:
csharp public class ProductResponseDto { public int Id { get; set; } public string Name { get; set; } public string FormattedPrice => $"¥{Price:F2}"; public DateTime CreatedAt { get; set; } }
步骤2:编写Dapper查询逻辑
- 在Repositories\DapperRepository.cs中添加方法:
```csharp
public async Task
> GetProductsByCategory(string category)
{
var sql = @”
SELECT Id, Name, Price, CreatedAt
FROM Products
WHERE Category = @category AND Status = ‘Active’
ORDER BY CreatedAt DESC”;
using var conn = _factory.GetOpenConnection();
return await conn.QueryAsync<ProductResponseDto>(sql, new { category });
}
```
步骤3:创建ProductsController
- 新建Controllers\ProductsController.cs:
```csharp
[RoutePrefix(“api/products”)]
public class ProductsController : ApiController
{
private readonly IDPRepository _repository;
public ProductsController(IDPRepository repository)
{
_repository = repository;
}
/// <summary>
/// 根据分类获取商品列表
/// </summary>
/// <param name="category">商品分类,如phone、laptop</param>
/// <returns>商品列表</returns>
[HttpGet]
[Route("")]
[ResponseType(typeof(IEnumerable<ProductResponseDto>))]
public async Task<IHttpActionResult> GetProducts(string category)
{
try
{
var products = await _repository.GetProductsByCategory(category);
return Ok(products);
}
catch (Exception ex)
{
LogManager.Error(ex, "获取商品列表失败,category={0}", category);
return InternalServerError();
}
}
}
`` - 注意:[ResponseType]`特性确保Swagger能正确显示返回类型。
步骤4:注册路由与测试
- 确保WebApiConfig.cs中config.MapHttpAttributeRoutes()已启用;
- 启动项目,访问http://localhost:5000/api/products?category=phone;
- Swagger中该接口将自动出现,点击“Try it out”可直接测试。
实操心得:新增Controller后,若Swagger不显示,检查三点:1)Controller类名是否以
Controller结尾;2)方法是否有[HttpGet]等HTTP动词特性;3)WebApiConfig.cs中config.EnableSwagger()是否在MapHttpAttributeRoutes()之后调用。
5. 常见问题与排查技巧实录:那些让你凌晨三点还在改的坑
5.1 “Autofac注册失败:No scope with a Tag matching ‘AutofacWebRequest’”
现象:
启动时报错:DependencyResolutionException: No scope with a Tag matching 'AutofacWebRequest' is visible from the scope in which the instance was requested.
根本原因:
Autofac的InstancePerRequest依赖Autofac.Integration.WebApi的AutofacWebApiDependencyResolver,但该Resolver未被正确设置。
排查步骤:
1. 检查Global.asax.cs中Application_Start是否调用:
csharp var container = builder.Build(); GlobalConfiguration.Configuration.DependencyResolver = new AutofacWebApiDependencyResolver(container);
2. 检查Web.config中<system.webServer><modules>是否注册了AutofacWebApiModule:
xml <add name="AutofacWebApiModule" type="Autofac.Integration.WebApi.AutofacWebApiModule" preCondition="managedHandler" />
3. 若用IIS托管,确认应用程序池.NET版本为v4.0,且“托管管道模式”为“集成模式”。
终极方案:
在Global.asax.cs中强制初始化:
protected void Application_PostResolveRequestCache(object sender, EventArgs e)
{
if (HttpContext.Current != null && HttpContext.Current.Items["AutofacWebRequest"] == null)
{
HttpContext.Current.Items["AutofacWebRequest"] = new object();
}
}
5.2 “Swagger UI显示404,但/api/values能正常返回JSON”
现象:
http://localhost:5000/swagger返回404,但http://localhost:5000/api/values返回["value1","value2"]。
排查清单:
| 检查项 | 正确配置 | 错误示例 |
|--------|----------|----------|
| Web.config中<system.webServer><handlers> | <add name="Swagger" path="swagger/*" verb="*" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" /> | 缺少此节点,或path写成swagger/** |
| Swashbuckle版本 | Swashbuckle.Core 5.6.0(适配WebApi 2.2) | 误装Swashbuckle.AspNetCore 6.5.0(.NET Core专用) |
| XML文档路径 | bin\WebApiTemplate.xml存在且可读 | bin目录下无.xml文件,或路径拼写错误(如WebApiTemplate.XML大小写不符) |
快速验证:
直接访问http://localhost:5000/swagger/v1/swagger.json,若返回JSON则Swagger后端正常,问题在UI静态资源;若返回404,则后端路由未注册。
5.3 “Dapper查询Oracle报错:ORA-00911: invalid character”
现象:
Dapper执行SELECT * FROM users WHERE id = :id时抛ORA-00911。
原因:
Oracle驱动不识别:命名参数,只认@或?位置参数。
解决方案:
在DapperRepository构造函数中,强制指定参数类型:
public DapperRepository(IDbConnectionFactory factory)
{
_factory = factory;
// 关键:告诉Dapper用Oracle风格参数
SqlMapper.AddTypeHandler(typeof(decimal), new OracleDecimalHandler());
}
// 自定义参数处理器
public class OracleDecimalHandler : SqlMapper.TypeHandler<decimal>
{
public override void SetValue(IDbDataParameter parameter, decimal value)
{
parameter.Value = value;
parameter.DbType = DbType.Decimal;
}
public override decimal Parse(object value) => Convert.ToDecimal(value);
}
并改SQL为:
var sql = "SELECT * FROM users WHERE id = :id"; // Oracle用冒号
// 或
var sql = "SELECT * FROM users WHERE id = ?"; // 通用问号
5.4 “Log4net在IIS中不写日志,本地IIS Express却正常”
现象:
VS中F5启动日志正常,但部署到IIS后logs/app.log为空。
根因分析:
IIS应用程序池默认身份为ApplicationPoolIdentity,对logs目录无写入权限。
解决步骤:
1. 在IIS管理器中,右键网站→“编辑权限”→“安全”选项卡→“编辑”→“添加”;
2. 输入IIS APPPOOL\YourAppPoolName(如IIS APPPOOL\.NET v4.5);
3. 勾选“写入”和“修改”权限;
4. 重启应用程序池。
预防措施:
在log4net.config中启用内部调试:
<log4net debug="true">
<appender name="InternalDebug" type="log4net.Appender.FileAppender">
<file value="logs/log4net-debug.log" />
<appendToFile value="true" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date [%thread] %-5level %logger - %message%newline" />
</layout>
</appender>
<root>
<level value="DEBUG" />
<appender-ref ref="InternalDebug" />
</root>
</log4net>
部署后检查logs/log4net-debug.log,可精准定位权限或路径问题。
5.5 “OAuth2登录成功,但后续接口返回401 Unauthorized”
现象:
/api/account/login返回access_token,但用该Token调/api/products仍401。
排查流程图:
graph TD
A[收到401] --> B{Token是否过期?}
B -->|是| C[检查OAuthServerProvider中ExpiresUtc设置]
B -->|否| D{Authorization头格式是否正确?}
D -->|否| E[必须为 Bearer <token>,不能少Bearer或空格]
D -->|是| F{Web.config中<system.web><authentication>是否启用?}
F -->|否| G[必须设 mode="Forms" 或 "None",WebApi用Token认证]
F -->|是| H{OAuthAuthorizationServerOptions中Provider是否赋值?}
H -->|否| I[GlobalConfiguration.Configure(WebApiConfig.Register)中必须注册]
H -->|是| J[检查CustomOAuthProvider.ValidateClientAuthentication是否调用context.Validated()]
高频错误:
- Web.config中<authentication mode="Windows" />未改为mode="None";
- OAuthAuthorizationServerOptions.Provider赋值为new CustomOAuthProvider(),但CustomOAuthProvider构造函数中未注入ILogManager,导致LogManager.Error抛空引用异常,静默失败;
- 前端发送Token时写成Authorization: token abc123(缺Bearer前缀)。
最后分享一个小技巧:在
CustomOAuthProvider的GrantResourceOwnerCredentials方法开头加一行LogManager.Info("OAuth登录开始,用户名={0}", context.UserName),若日志没出现,说明请求根本没走到OAuth流程——大概率是路由或认证模块未注册。
我在实际使用中发现,90%的401问题都出在Web.config的<authentication>配置和Authorization头格式上。建议把这两项做成部署检查清单,贴在团队Wiki首页。
简介:开箱即用的.NET WebApi基础工程模板,内置Dapper和EFCore双ORM支持,适配不同数据访问场景;Autofac实现模块化依赖注入,已预配置生命周期管理;WebApi接口层提供AccountController基础认证、OAuth2授权服务(OAuthServerProvider)、HTTP客户端封装(HttpManager)及统一日志入口(LogManager);Swagger文档自动集成并增强接口描述能力(SwaggerControllerDescProvider);所有配置文件(Web.config、log4net.config、Swagger相关设置等)均按Debug/Release环境区分,repositories.config支持数据访问策略切换;目录中保留多版本.config文件,体现真实部署所需的环境适配逻辑,适合快速搭建中小型RESTful服务或作为团队标准化开发起点。

被折叠的 条评论
为什么被折叠?



