一、 IIS不是文件夹,理解托管模型是关键

很多朋友第一次把.NET Core应用往IIS上放的时候,容易犯一个想当然的错误:觉得IIS就是个高级的文件服务器,直接把发布好的文件往网站目录里一扔就完事了。结果一访问,很可能就给你弹个“500.19内部服务器错误”,或者干脆显示一片空白。这背后的根本原因,是没搞清楚.NET Core和传统.NET Framework在IIS里“住”的方式完全不同。

传统.NET Framework应用,可以看作是IIS的“亲儿子”,IIS直接就能理解并执行你的代码。而.NET Core应用,对于IIS来说,更像是一个需要特殊翻译的“客人”。IIS本身不直接运行.NET Core的代码,它只负责接收HTTP请求,然后像个接线员一样,把请求转交给一个真正的“处理器”——那就是 ASP.NET Core模块,也叫ANCM。这个模块才是能听懂.NET Core语言、并启动你应用的“翻译官”。

所以,部署的第一步,不是传文件,而是确保服务器上已经安装了这个“翻译官”。你需要去微软官网下载并安装对应版本的 .NET Core Hosting Bundle。这个东西打包了.NET Core运行时和最重要的ANCM模块。安装完后,记得重启一下IIS服务器(运行iisreset命令或者重启机器),让新安装的模块生效。

技术栈:ASP.NET Core 6.0 + IIS

// Program.cs - 检查应用是否正常运行
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// 一个简单的健康检查端点,用于验证应用是否在IIS后面成功启动
app.MapGet("/health", () => "ASP.NET Core 6.0 App is running behind IIS!");

// 模拟一个常见的API接口
app.MapGet("/api/hello", (string? name) => 
{
    // 如果未提供名字,使用默认值
    name ??= "Visitor";
    return new { Message = $"Hello, {name}! Welcome from IIS." };
});

app.Run();

安装好Hosting Bundle后,在IIS管理器中,你会在网站的“模块”配置里看到 AspNetCoreModuleV2 的身影,这就说明“翻译官”就位了。

二、 权限问题:应用池身份那点事儿

解决了“翻译官”问题,下一个常遇到的拦路虎就是权限。错误日志里经常出现“访问被拒绝”、“无法写入目录”这类提示。这通常是因为你的应用程序池身份没有足够的权限去操作它需要的文件夹。

IIS中,每个网站都运行在一个特定的“应用程序池”下,这个池有一个身份标识。默认情况下,这个身份是 ApplicationPoolIdentity,它是一个虚拟账户,权限相对受限。如果你的应用需要写日志到某个目录、或者读写网站目录外的文件,这个默认账户可能就“力不从心”了。

解决思路有两种:一是提升这个账户的权限,二是更换一个更有权力的账户。

1. 提升默认账户权限: 找到你的网站物理路径对应的文件夹,右键“属性” -> “安全” -> “编辑” -> “添加”。在对象名称里输入 IIS AppPool\你的应用程序池名称(例如 IIS AppPool\MyDotNetCoreAppPool),然后赋予它“修改”或“完全控制”的权限。这种方法比较精细,只针对特定文件夹授权。

2. 更换应用程序池身份: 在IIS管理器中,找到你的应用程序池,点击“高级设置”。找到“身份标识”一项,将其从 ApplicationPoolIdentity 改为 LocalSystem 或你自定义的一个具有合适权限的本地/域账户。LocalSystem 权限很高,在测试环境可以临时使用,生产环境出于安全考虑,建议使用专门创建的最小权限账户。

这里有一个常见的场景示例:你的应用需要将日志写入 C:\MyAppLogs 目录。

技术栈:ASP.NET Core 6.0 + Serilog

// Program.cs - 配置需要文件写入权限的日志组件
using Serilog;

var builder = WebApplication.CreateBuilder(args);

// 配置Serilog,将日志同时输出到控制台和一个文件
Log.Logger = new LoggerConfiguration()
    .WriteTo.Console() // 输出到控制台,便于调试
    .WriteTo.File("C:/MyAppLogs/myapp-.txt", // 注意:此路径需要应用程序池身份有写入权限
                  rollingInterval: RollingInterval.Day, // 按天滚动日志文件
                  retainedFileCountLimit: 7) // 保留最近7天的日志
    .CreateLogger();

builder.Host.UseSerilog(); // 在ASP.NET Core通用主机中使用Serilog

var app = builder.Build();
app.MapGet("/", () => 
{
    // 记录一条信息日志
    Log.Information("有人访问了首页。");
    return "Hello World with Logging!";
});

app.Run();

如果运行此应用的程序池账户对 C:\MyAppLogs 没有写入权限,访问网站时就会出错。按照上述方法配置权限后,问题即可解决。

三、 配置打架:web.config、appsettings.json与环境变量

部署到IIS后,你的应用会同时受到多个“指挥官”的影响:IIS的 web.config、项目自带的 appsettings.json、以及服务器上的环境变量。它们之间有个优先级,搞不清楚就容易配置失效。

优先级从高到低通常是:命令行参数 > 环境变量 > appsettings.{Environment}.json > appsettings.json > web.config

web.config 在.NET Core IIS部署中主要就干两件核心的事:

  1. 告诉IIS用哪个“翻译官”(ANCM)来处理请求。
  2. 为“翻译官”传递启动参数,比如指定你的.NET Core应用DLL路径。

一个典型的、干净的 web.config 文件长这样:

技术栈:ASP.NET Core 6.0

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <location path="." inheritInChildApplications="false">
    <!-- system.webServer 节点是IIS的配置核心 -->
    <system.webServer>
      <!-- handlers 节点定义了如何处理请求 -->
      <handlers>
        <!-- 这个add指令是关键:所有请求(path="*" verb="*")都交给AspNetCoreModuleV2处理 -->
        <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
      </handlers>
      <!-- aspNetCore 节点是给ANCM模块的指令 -->
      <aspNetCore processPath="dotnet" 
                  arguments=".\MyDotNetCoreApp.dll" 
                  stdoutLogEnabled="false" 
                  stdoutLogFile=".\logs\stdout" 
                  hostingModel="inprocess" />
                  <!-- 
                    processPath: 启动的进程,这里是dotnet命令。
                    arguments: 传递给dotnet命令的参数,即你的应用主DLL。
                    stdoutLogEnabled: 是否启用标准输出日志,设为true对排错极有帮助。
                    stdoutLogFile: 输出日志的目录。需要确保目录存在且有写入权限。
                    hostingModel: 托管模式,inprocess(进程内)性能更好,outofprocess(进程外)更隔离。
                  -->
    </system.webServer>
  </location>
</configuration>

注意,你应用本身的配置(如数据库连接字符串),强烈建议放在 appsettings.jsonappsettings.Production.json 中,而不是 web.config 里。因为 web.config 可能会被IIS或部署工具在特定情况下覆盖或修改。

例如,数据库连接字符串:

技术栈:ASP.NET Core 6.0 + Entity Framework Core

// appsettings.Production.json - 生产环境专用配置
{
  "ConnectionStrings": {
    "DefaultConnection": "Server=PROD_SERVER;Database=MyAppDb;User Id=appuser;Password=StrongPassword123;TrustServerCertificate=True;"
    // 生产环境的真实服务器和凭据
  },
  "Logging": {
    "LogLevel": {
      "Default": "Warning" // 生产环境通常提高日志级别,减少噪音
    }
  }
}
// 在Program.cs或Startup.cs中读取配置
var builder = WebApplication.CreateBuilder(args);

// 从配置系统中获取连接字符串
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");

// 将DbContext注册到依赖注入容器
builder.Services.AddDbContext<MyAppDbContext>(options =>
    options.UseSqlServer(connectionString)); // 这里明确使用SQL Server

var app = builder.Build();
// ... 其余配置

四、 端口与绑定的玄机:避免冲突与正确访问

在IIS上,访问端口和URL绑定是由IIS网站绑定设置的,而不是你代码里 UseUrlslaunchSettings.json 中指定的那个。很多开发者在本机用Kestrel跑得好好的(比如 http://localhost:5000),部署到IIS后却不知道怎么访问了。

在IIS管理器中,选中你的网站,点击右侧“绑定”。通常你会看到一个绑定,类型是http,IP地址为“全部未分配”,端口是80(HTTP)或443(HTTPS)。这意味着你的网站通过服务器的IP地址和80端口对外提供服务。

所以,你访问的地址应该是 http://你的服务器IP或域名,而不是 http://你的服务器IP:5000。那个5000端口是Kestrel开发服务器自己用的,在IIS集成模式下,Kestrel不再直接对外,而是通过ANCM与IIS通信,IIS才是对外的门户。

应用场景: 假设你有一个内部使用的仪表盘应用,部署在IP为 192.168.1.100 的服务器上。你在IIS中创建了网站 MyDashboard,物理路径指向发布文件夹,绑定为 http,端口 8080(因为80端口可能被其他站点占用)。

那么,你的同事在浏览器中访问的地址就是 http://192.168.1.100:8080。你的应用代码里不需要也不应该去指定这个8080端口。

技术栈:ASP.NET Core 6.0

// Program.cs - 注意:部署到IIS时,不要硬编码UseUrls
var builder = WebApplication.CreateBuilder(args);

// 错误的做法:在IIS部署时,这行代码通常无效,因为IIS会覆盖监听地址。
// app.Urls.Add("http://*:8080"); 

// 正确的做法:让IIS和ANCM来决定如何托管。
var app = builder.Build();

app.MapGet("/", () => 
{
    // 可以获取当前请求的原始信息
    var request = app.Services.GetService<IHttpContextAccessor>()?.HttpContext?.Request;
    var host = request?.Host.ToString() ?? "Unknown Host";
    var scheme = request?.Scheme ?? "Unknown Scheme";
    return $"应用正在通过 {scheme}://{host} 提供服务。";
});

app.Run();

五、 日志:照亮排错黑暗的明灯

当问题发生时,最宝贵的就是日志。在IIS部署场景下,日志来源有好几个,你得知道去哪找。

1. IIS失败请求跟踪: 适合诊断HTTP层面的错误(如404, 500)。在IIS中为网站启用“失败请求跟踪”,可以记录下错误请求的完整管道事件,非常强大。

2. Windows事件查看器: 很多系统级和模块级的错误会记录在这里。打开“事件查看器”,依次展开“Windows 日志” -> “应用程序”。查找来源为 IIS AspNetCore ModuleIIS-Express 的错误事件,里面往往有详细的错误信息。

3. ASP.NET Core模块标准输出日志: 这是最常用、最直接的排错工具。还记得前面 web.config 里的 stdoutLogEnabledstdoutLogFile 吗?将 stdoutLogEnabled 设为 true,并确保 stdoutLogFile 指向的目录(如 .\logs\)存在且应用程序池有写入权限。重启网站后,应用的启动日志、控制台输出都会被记录到该目录下的文件中。这里能看到应用启动失败的根本原因,比如哪个依赖库没找到、哪个配置文件解析出错。

4. 应用自身日志: 就是你用ILogger接口写的那些日志,它们输出到哪里取决于你的日志配置(如上面的Serilog例子)。

让我们模拟一个启动时常见的错误:缺少特定的配置项。

技术栈:ASP.NET Core 6.0

// Program.cs - 一个因配置缺失导致启动失败的例子
var builder = WebApplication.CreateBuilder(args);

// 假设我们的应用强依赖一个叫“CriticalApiKey”的配置
var criticalApiKey = builder.Configuration["CriticalApiKey"];
if (string.IsNullOrEmpty(criticalApiKey))
{
    // 如果配置缺失,直接抛出异常,导致应用启动失败
    // 这个错误信息会出现在标准输出日志和Windows事件日志中
    throw new InvalidOperationException("致命错误:配置项 'CriticalApiKey' 未设置。请在appsettings.json或环境变量中配置。");
}

builder.Services.AddSingleton(new ApiService(criticalApiKey));
var app = builder.Build();
app.MapGet("/", () => "应用启动成功!");
app.Run();

如果部署时忘记配置 CriticalApiKey,应用会在启动瞬间崩溃。查看 logs\stdout 目录下最新的日志文件,你就能清晰地看到 InvalidOperationException: 致命错误:配置项 'CriticalApiKey' 未设置。... 这条信息,从而快速定位问题。

技术优缺点:

  • 优点: IIS提供了成熟的管理界面、强大的安全特性(如Windows身份验证、请求过滤)、以及与Windows生态的深度集成。ANCM模块使得部署过程相对标准化。
  • 缺点: 配置复杂度较高,权限和模块依赖问题容易成为绊脚石。性能上,虽然进程内模式已极大提升,但相较于独立运行的Kestrel,仍多了一层转发开销。此外,它被绑定在Windows服务器上。

注意事项:

  1. 始终使用 dotnet publish 命令发布应用,并将输出文件夹(如 publish)的内容作为网站物理路径,而不是整个项目文件夹。
  2. 生产环境务必使用进程内托管模式(hostingModel="inprocess"),以获得最佳性能。
  3. 妥善管理配置文件,敏感信息(如密码、密钥)应使用环境变量、Azure Key Vault等方式,避免直接写在 appsettings.json 中并提交到代码库。
  4. 部署后,第一时间检查 stdout 日志,确认应用启动无任何错误或警告。

文章总结: 将.NET Core应用部署到IIS,就像为一位新居民安排入住一个成熟的社区。理解IIS的“托管模型”是拿到钥匙的第一步,确保“翻译官”(ANCM)到位。接着要处理好“居民”的“权限”问题,让应用池身份能顺畅工作。然后要理清“社区规则”(web.config)和“住户守则”(appsettings.json)之间的关系,避免指令冲突。明白“访问门户”(端口绑定)由IIS统一管理,而非应用自身。最后,当出现任何问题时,学会打开“社区监控日志”(标准输出、事件查看器)来洞察根源。遵循这些步骤和思路,大部分IIS部署难题都能迎刃而解,让你的.NET Core应用在Windows服务器上稳定运行。