No articles found
Try different keywords or browse our categories
Fix: IIS 403/404 After Deployment Error - Complete IIS Deployment Guide
Learn how to fix IIS 403 and 404 errors after deploying .NET applications. This comprehensive guide covers IIS configuration, permissions, and proper deployment techniques.
The ‘IIS 403/404 After Deployment’ error is a common issue when deploying .NET applications to Internet Information Services (IIS). These HTTP status errors occur when the web server cannot properly serve your application after deployment. Understanding and resolving these errors is crucial for ensuring your deployed applications function correctly in production environments.
This comprehensive guide explains what causes IIS 403/404 errors after deployment, why they happen, and provides multiple solutions to fix and prevent them in your .NET projects with clean code examples and directory structure.
What is the IIS 403/404 After Deployment Error?
IIS 403/404 errors after deployment occur when:
- The IIS application pool identity lacks proper file system permissions
- The application is not properly configured in IIS
- Required modules or handlers are missing
- Authentication settings are incorrect
- The application path is misconfigured
- Missing or incorrect web.config settings
- Insufficient permissions for the application directory
- Missing .NET runtime or hosting bundle
Common Error Messages:
403 - Forbidden: Access is denied404 - Not Found: The resource you are looking for does not exist403.14 - Forbidden: Directory browsing is disabled404.2 - Not Found: ISAPI or CGI restrictionHTTP Error 500.19 - Internal Server Error: Configuration ErrorModule 'AspNetCoreModuleV2' could not be found
Understanding the Problem
When deploying .NET applications to IIS, several configuration and permission issues can prevent the application from running properly. The 403 error indicates that the server understood the request but refuses to authorize it, while the 404 error means the server cannot find the requested resource. These errors typically stem from IIS configuration, file permissions, or missing dependencies.
Typical IIS Deployment Structure:
IIS Deployment/
├── wwwroot/
│ ├── MyApp/
│ │ ├── web.config
│ │ ├── appsettings.json
│ │ ├── MyApp.dll
│ │ ├── MyApp.runtimeconfig.json
│ │ └── wwwroot/
│ │ ├── css/
│ │ ├── js/
│ │ └── images/
│ └── logs/
├── Application Pool Configuration
├── Site Binding Configuration
└── Security Configuration
Solution 1: Proper IIS Configuration and Permissions
The most fundamental approach to prevent IIS errors is to ensure proper configuration and permissions.
❌ Without Proper Configuration:
<!-- web.config - ❌ Missing ASP.NET Core module configuration -->
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.webServer>
<!-- ❌ Missing handlers and modules -->
</system.webServer>
</configuration>
✅ With Proper Configuration:
web.config:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<location path="." inheritInChildApplications="false">
<system.webServer>
<handlers>
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
</handlers>
<aspNetCore processPath="dotnet"
arguments=".\MyApp.dll"
stdoutLogEnabled="false"
stdoutLogFile=".\logs\stdout"
hostingModel="inprocess">
<environmentVariables>
<environmentVariable name="ASPNETCORE_ENVIRONMENT" value="Production" />
<environmentVariable name="ASPNETCORE_HOSTINGSTARTUPASSEMBLIES" value="Microsoft.AspNetCore.Server.IISIntegration" />
</environmentVariables>
</aspNetCore>
</system.webServer>
</location>
</configuration>
Models/User.cs:
using System.ComponentModel.DataAnnotations;
namespace MyApp.Models
{
public class User
{
public int Id { get; set; }
[Required]
[StringLength(100)]
public string Name { get; set; } = string.Empty;
[Required]
[EmailAddress]
[StringLength(255)]
public string Email { get; set; } = string.Empty;
public bool IsActive { get; set; } = true;
public string? Role { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime? UpdatedAt { get; set; }
}
}
Controllers/HomeController.cs:
using Microsoft.AspNetCore.Mvc;
using MyApp.Models;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
namespace MyApp.Controllers
{
[ApiController]
[Route("[controller]")]
public class HomeController : ControllerBase
{
[HttpGet]
public async Task<ActionResult<Dictionary<string, object>>> Get()
{
try
{
var result = new Dictionary<string, object>
{
["status"] = "running",
["timestamp"] = DateTime.UtcNow,
["environment"] = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production",
["runtime"] = Environment.Version.ToString(),
["framework"] = System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription,
["message"] = "Application is running successfully on IIS"
};
return Ok(result);
}
catch (Exception ex)
{
return StatusCode(500, new { error = ex.Message });
}
}
[HttpGet("health")]
public async Task<ActionResult<Dictionary<string, object>>> HealthCheck()
{
try
{
// ✅ Check if the application can access its own files
var appPath = AppDomain.CurrentDomain.BaseDirectory;
var configFileExists = System.IO.File.Exists(Path.Combine(appPath, "appsettings.json"));
var healthInfo = new Dictionary<string, object>
{
["status"] = "healthy",
["timestamp"] = DateTime.UtcNow,
["configFileExists"] = configFileExists,
["applicationPath"] = appPath,
["memoryUsage"] = GC.GetTotalMemory(false)
};
return Ok(healthInfo);
}
catch (Exception ex)
{
return StatusCode(500, new { error = "Health check failed", details = ex.Message });
}
}
[HttpGet("permissions")]
public async Task<ActionResult<Dictionary<string, object>>> CheckPermissions()
{
try
{
var appPath = AppDomain.CurrentDomain.BaseDirectory;
var permissions = new Dictionary<string, object>
{
["applicationPath"] = appPath,
["canRead"] = CanAccessDirectory(appPath, FileAccess.Read),
["canWrite"] = CanAccessDirectory(appPath, FileAccess.Write),
["canExecute"] = CanAccessDirectory(appPath, FileAccess.ReadExecute),
["currentUser"] = Environment.UserName,
["currentDomain"] = Environment.UserDomainName
};
return Ok(permissions);
}
catch (Exception ex)
{
return StatusCode(500, new { error = "Permission check failed", details = ex.Message });
}
}
private bool CanAccessDirectory(string path, FileAccess access)
{
try
{
var fileInfo = new FileInfo(Path.Combine(path, "temp_access_check.tmp"));
using (var fs = new FileStream(fileInfo.FullName, FileMode.OpenOrCreate, FileAccess.Write))
{
fs.WriteByte(0);
}
System.IO.File.Delete(fileInfo.FullName);
return true;
}
catch
{
return false;
}
}
}
}
Program.cs:
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
var builder = WebApplication.CreateBuilder(args);
// ✅ Add services to the container
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// ✅ Configure the HTTP request pipeline
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
else
{
app.UseExceptionHandler("/Error");
// ✅ Use HSTS in production
app.UseHsts();
}
// ✅ Essential middleware for IIS deployment
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
// ✅ Map controllers
app.MapControllers();
app.Run();
Solution 2: Application Pool Configuration
Configure the IIS application pool correctly for .NET applications.
PowerShell Script for Application Pool Setup:
# ✅ PowerShell script to configure IIS application pool for .NET deployment
# Save as Configure-IISAppPool.ps1
param(
[string]$AppPoolName = "MyAppPool",
[string]$SiteName = "MyAppSite",
[string]$PhysicalPath = "C:\inetpub\wwwroot\MyApp",
[string]$RuntimeVersion = "v4.0", # For .NET Framework, use "No Managed Code" for .NET Core
[string]$PipelineMode = "Integrated"
)
Import-Module WebAdministration
# ✅ Create or update application pool
if (Test-Path "IIS:\AppPools\$AppPoolName") {
Write-Host "Application pool '$AppPoolName' already exists. Updating..."
Set-ItemProperty "IIS:\AppPools\$AppPoolName" -name managedRuntimeVersion -value $RuntimeVersion
Set-ItemProperty "IIS:\AppPools\$AppPoolName" -name managedPipelineMode -value $PipelineMode
} else {
Write-Host "Creating application pool '$AppPoolName'..."
New-WebAppPool -Name $AppPoolName
Set-ItemProperty "IIS:\AppPools\$AppPoolName" -name managedRuntimeVersion -value $RuntimeVersion
Set-ItemProperty "IIS:\AppPools\$AppPoolName" -name managedPipelineMode -value $PipelineMode
}
# ✅ Set identity to ApplicationPoolIdentity (recommended for security)
Set-ItemProperty "IIS:\AppPools\$AppPoolName" -name processModel.identityType -value ApplicationPoolIdentity
# ✅ Create or update website
if (Test-Path "IIS:\Sites\$SiteName") {
Write-Host "Website '$SiteName' already exists. Updating..."
Set-ItemProperty "IIS:\Sites\$SiteName" -name physicalPath -value $PhysicalPath
Set-ItemProperty "IIS:\Sites\$SiteName" -name applicationPool -value $AppPoolName
} else {
Write-Host "Creating website '$SiteName'..."
New-Website -Name $SiteName -PhysicalPath $PhysicalPath -ApplicationPool $AppPoolName
}
# ✅ Set appropriate permissions for the application directory
$acl = Get-Acl $PhysicalPath
$accessRule = New-Object System.Security.AccessControl.FileSystemAccessRule("$AppPoolName", "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow")
$acl.SetAccessRule($accessRule)
Set-Acl $PhysicalPath $acl
Write-Host "Configuration completed successfully!"
Write-Host "Application Pool: $AppPoolName"
Write-Host "Website: $SiteName"
Write-Host "Physical Path: $PhysicalPath"
IIS Configuration Helper Class:
using Microsoft.Extensions.Logging;
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
namespace MyApp.Helpers
{
public static class IISConfigurationHelper
{
private static readonly ILogger Logger;
static IISConfigurationHelper()
{
// ✅ Initialize logger (in a real app, inject via DI)
}
/// <summary>
/// Checks if the application is running under IIS
/// </summary>
/// <returns>True if running under IIS, false otherwise</returns>
public static bool IsRunningUnderIIS()
{
try
{
var serverSoftware = Environment.GetEnvironmentVariable("SERVER_SOFTWARE");
return !string.IsNullOrEmpty(serverSoftware) && serverSoftware.Contains("IIS");
}
catch
{
return false;
}
}
/// <summary>
/// Checks if ASP.NET Core Module is available
/// </summary>
/// <returns>True if module is available, false otherwise</returns>
public static bool IsAspNetCoreModuleAvailable()
{
try
{
// ✅ Check if the module is referenced in web.config
var webConfigPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "web.config");
if (File.Exists(webConfigPath))
{
var webConfigContent = File.ReadAllText(webConfigPath);
return webConfigContent.Contains("AspNetCoreModuleV2") ||
webConfigContent.Contains("AspNetCoreModule");
}
return false;
}
catch
{
return false;
}
}
/// <summary>
/// Checks if the application has necessary file permissions
/// </summary>
/// <returns>True if permissions are sufficient, false otherwise</returns>
public static bool HasNecessaryPermissions()
{
try
{
var appPath = AppDomain.CurrentDomain.BaseDirectory;
// ✅ Check if we can read the application directory
var dirInfo = new DirectoryInfo(appPath);
var files = dirInfo.GetFiles();
// ✅ Check if we can write to the logs directory
var logsPath = Path.Combine(appPath, "logs");
if (!Directory.Exists(logsPath))
{
Directory.CreateDirectory(logsPath);
}
// ✅ Try to create a temporary file
var tempFile = Path.Combine(logsPath, "permission_test.tmp");
File.WriteAllText(tempFile, "test");
File.Delete(tempFile);
return true;
}
catch
{
return false;
}
}
/// <summary>
/// Gets the current IIS application pool identity
/// </summary>
/// <returns>Application pool identity name</returns>
public static string GetApplicationPoolIdentity()
{
try
{
var identity = Environment.GetEnvironmentVariable("APP_POOL_IDENTITY");
if (!string.IsNullOrEmpty(identity))
{
return identity;
}
// ✅ For IIS in-process hosting, the identity is usually IIS AppPool\{poolname}
var poolName = Environment.GetEnvironmentVariable("APP_POOL_CONFIG");
if (!string.IsNullOrEmpty(poolName))
{
return $"IIS AppPool\\{Path.GetFileNameWithoutExtension(poolName)}";
}
return "Unknown";
}
catch
{
return "Unknown";
}
}
/// <summary>
/// Validates the deployment configuration
/// </summary>
/// <returns>Configuration validation result</returns>
public static ConfigurationValidationResult ValidateConfiguration()
{
var result = new ConfigurationValidationResult
{
IsRunningUnderIIS = IsRunningUnderIIS(),
IsAspNetCoreModuleAvailable = IsAspNetCoreModuleAvailable(),
HasNecessaryPermissions = HasNecessaryPermissions(),
ApplicationPoolIdentity = GetApplicationPoolIdentity(),
RuntimeVersion = Environment.Version.ToString(),
FrameworkDescription = RuntimeInformation.FrameworkDescription,
BaseDirectory = AppDomain.CurrentDomain.BaseDirectory
};
result.IsValid = result.IsRunningUnderIIS &&
result.IsAspNetCoreModuleAvailable &&
result.HasNecessaryPermissions;
return result;
}
}
public class ConfigurationValidationResult
{
public bool IsValid { get; set; }
public bool IsRunningUnderIIS { get; set; }
public bool IsAspNetCoreModuleAvailable { get; set; }
public bool HasNecessaryPermissions { get; set; }
public string ApplicationPoolIdentity { get; set; } = string.Empty;
public string RuntimeVersion { get; set; } = string.Empty;
public string FrameworkDescription { get; set; } = string.Empty;
public string BaseDirectory { get; set; } = string.Empty;
}
}
Solution 3: File System Permissions and Security
Configure proper file system permissions for IIS applications.
Controllers/DeploymentController.cs:
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using MyApp.Helpers;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
namespace MyApp.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class DeploymentController : ControllerBase
{
private readonly ILogger<DeploymentController> _logger;
public DeploymentController(ILogger<DeploymentController> logger)
{
_logger = logger;
}
[HttpGet("diagnostics")]
public async Task<ActionResult<Dictionary<string, object>>> GetDiagnostics()
{
try
{
var diagnostics = new Dictionary<string, object>
{
["timestamp"] = DateTime.UtcNow,
["isRunningUnderIIS"] = IISConfigurationHelper.IsRunningUnderIIS(),
["isAspNetCoreModuleAvailable"] = IISConfigurationHelper.IsAspNetCoreModuleAvailable(),
["hasNecessaryPermissions"] = IISConfigurationHelper.HasNecessaryPermissions(),
["applicationPoolIdentity"] = IISConfigurationHelper.GetApplicationPoolIdentity(),
["runtimeVersion"] = Environment.Version.ToString(),
["framework"] = System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription,
["baseDirectory"] = AppDomain.CurrentDomain.BaseDirectory,
["workingDirectory"] = Environment.CurrentDirectory,
["tempPath"] = Path.GetTempPath(),
["user"] = Environment.UserName,
["machineName"] = Environment.MachineName,
["osVersion"] = Environment.OSVersion.VersionString
};
_logger.LogInformation("Diagnostics retrieved successfully");
return Ok(diagnostics);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error retrieving diagnostics");
return StatusCode(500, new { error = ex.Message });
}
}
[HttpGet("configuration-validation")]
public async Task<ActionResult<ConfigurationValidationResult>> ValidateConfiguration()
{
try
{
var validationResult = IISConfigurationHelper.ValidateConfiguration();
_logger.LogInformation("Configuration validation completed. Valid: {IsValid}", validationResult.IsValid);
if (!validationResult.IsValid)
{
_logger.LogWarning("Configuration validation failed. Issues: IIS={IsIIS}, Module={Module}, Permissions={Permissions}",
validationResult.IsRunningUnderIIS,
validationResult.IsAspNetCoreModuleAvailable,
validationResult.HasNecessaryPermissions);
}
return Ok(validationResult);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error validating configuration");
return StatusCode(500, new { error = ex.Message });
}
}
[HttpGet("file-permissions")]
public async Task<ActionResult<Dictionary<string, object>>> CheckFilePermissions()
{
try
{
var appPath = AppDomain.CurrentDomain.BaseDirectory;
var permissions = new Dictionary<string, object>
{
["applicationPath"] = appPath,
["webConfigExists"] = File.Exists(Path.Combine(appPath, "web.config")),
["dllExists"] = File.Exists(Path.Combine(appPath, "MyApp.dll")),
["runtimeConfigExists"] = File.Exists(Path.Combine(appPath, "MyApp.runtimeconfig.json")),
["canReadWebConfig"] = CanAccessFile(Path.Combine(appPath, "web.config"), FileAccess.Read),
["canReadDll"] = CanAccessFile(Path.Combine(appPath, "MyApp.dll"), FileAccess.Read),
["canReadRuntimeConfig"] = CanAccessFile(Path.Combine(appPath, "MyApp.runtimeconfig.json"), FileAccess.Read),
["directoryPermissions"] = GetDirectoryPermissions(appPath)
};
return Ok(permissions);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error checking file permissions");
return StatusCode(500, new { error = ex.Message });
}
}
[HttpGet("create-log-file")]
public async Task<ActionResult<string>> CreateLogFile()
{
try
{
var logsPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "logs");
if (!Directory.Exists(logsPath))
{
Directory.CreateDirectory(logsPath);
}
var logFilePath = Path.Combine(logsPath, $"deployment_test_{DateTime.UtcNow:yyyyMMdd_HHmmss}.txt");
var logContent = $"Deployment test log created at {DateTime.UtcNow}\nApplication Pool: {IISConfigurationHelper.GetApplicationPoolIdentity()}\nUser: {Environment.UserName}";
System.IO.File.WriteAllText(logFilePath, logContent);
_logger.LogInformation("Log file created successfully: {LogFilePath}", logFilePath);
return Ok($"Log file created: {logFilePath}");
}
catch (Exception ex)
{
_logger.LogError(ex, "Error creating log file");
return StatusCode(500, new { error = ex.Message });
}
}
private bool CanAccessFile(string filePath, FileAccess access)
{
try
{
if (!File.Exists(filePath))
return false;
using (var fs = new FileStream(filePath, FileMode.Open, access))
{
return true;
}
}
catch
{
return false;
}
}
private Dictionary<string, bool> GetDirectoryPermissions(string directoryPath)
{
var permissions = new Dictionary<string, bool>();
try
{
var dirInfo = new DirectoryInfo(directoryPath);
// ✅ Test various access types
permissions["canList"] = true; // Directory listing is usually allowed
permissions["canRead"] = CanAccessDirectory(directoryPath, FileAccess.Read);
permissions["canWrite"] = CanAccessDirectory(directoryPath, FileAccess.Write);
permissions["canExecute"] = CanAccessDirectory(directoryPath, FileAccess.ReadExecute);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error checking directory permissions for {DirectoryPath}", directoryPath);
permissions["error"] = ex.Message;
}
return permissions;
}
private bool CanAccessDirectory(string path, FileAccess access)
{
try
{
var testFile = Path.Combine(path, $"temp_access_test_{Guid.NewGuid()}.tmp");
using (var fs = new FileStream(testFile, FileMode.CreateNew, FileAccess.Write))
{
fs.WriteByte(0);
}
// Clean up
if (File.Exists(testFile))
{
File.Delete(testFile);
}
return true;
}
catch
{
return false;
}
}
}
}
Solution 4: Web.config Optimization for IIS
Create optimized web.config for IIS deployment scenarios.
Optimized web.config:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<location path="." inheritInChildApplications="false">
<system.webServer>
<!-- ✅ Static content compression -->
<httpCompression>
<scheme name="gzip" dll="%Windir%\system32\inetsrv\gzip.dll" />
<dynamicTypes>
<add mimeType="text/*" enabled="true" />
<add mimeType="message/*" enabled="true" />
<add mimeType="application/javascript" enabled="true" />
<add mimeType="application/json" enabled="true" />
<add mimeType="*/*" enabled="false" />
</dynamicTypes>
<staticTypes>
<add mimeType="text/*" enabled="true" />
<add mimeType="message/*" enabled="true" />
<add mimeType="application/javascript" enabled="true" />
<add mimeType="application/json" enabled="true" />
<add mimeType="*/*" enabled="false" />
</staticTypes>
</httpCompression>
<urlCompression doStaticCompression="true" doDynamicCompression="true" />
<!-- ✅ Request filtering for security -->
<security>
<requestFiltering>
<fileExtensions allowUnlisted="true" >
<add fileExtension=".config" allowed="false" />
<add fileExtension=".cs" allowed="false" />
<add fileExtension=".csproj" allowed="false" />
<add fileExtension=".user" allowed="false" />
<add fileExtension=".sln" allowed="false" />
<add fileExtension=".mdb" allowed="false" />
<add fileExtension=".accdb" allowed="false" />
</fileExtensions>
<hiddenSegments>
<add segment="bin" />
<add segment="App_Code" />
<add segment="App_Data" />
<add segment="App_LocalResources" />
<add segment="App_GlobalResources" />
<add segment="App_WebReferences" />
<add segment="App_Browsers" />
<add segment="App_Themes" />
</hiddenSegments>
</requestFiltering>
</security>
<!-- ✅ Handlers for ASP.NET Core -->
<handlers>
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
</handlers>
<!-- ✅ ASP.NET Core module configuration -->
<aspNetCore processPath="dotnet"
arguments=".\MyApp.dll"
stdoutLogEnabled="true"
stdoutLogFile=".\logs\stdout"
hostingModel="inprocess">
<environmentVariables>
<environmentVariable name="ASPNETCORE_ENVIRONMENT" value="Production" />
<environmentVariable name="ASPNETCORE_HOSTINGSTARTUPASSEMBLIES" value="Microsoft.AspNetCore.Server.IISIntegration" />
<environmentVariable name="COMPlus_ReadyToRun" value="0" />
</environmentVariables>
</aspNetCore>
</system.webServer>
</location>
<!-- ✅ Runtime configuration -->
<system.web>
<compilation debug="false" targetFramework="4.7.2" />
<httpRuntime targetFramework="4.7.2" maxRequestLength="51200" executionTimeout="300" />
</system.web>
<!-- ✅ Custom error pages -->
<system.webServer>
<httpErrors errorMode="Custom">
<remove statusCode="403" subStatusCode="-1" />
<remove statusCode="404" subStatusCode="-1" />
<remove statusCode="500" subStatusCode="-1" />
<error statusCode="403" prefixLanguageFilePath="" path="/errors/403.html" responseMode="ExecuteURL" />
<error statusCode="404" prefixLanguageFilePath="" path="/errors/404.html" responseMode="ExecuteURL" />
<error statusCode="500" prefixLanguageFilePath="" path="/errors/500.html" responseMode="ExecuteURL" />
</httpErrors>
</system.webServer>
</configuration>
Solution 5: Deployment Verification and Troubleshooting
Implement deployment verification and troubleshooting capabilities.
Services/IDeploymentService.cs:
using System.Collections.Generic;
using System.Threading.Tasks;
namespace MyApp.Services
{
public interface IDeploymentService
{
Task<Dictionary<string, object>> VerifyDeploymentAsync();
Task<Dictionary<string, object>> CheckPrerequisitesAsync();
Task<Dictionary<string, object>> GetFileSystemInfoAsync();
Task<bool> RestartApplicationAsync();
Task<Dictionary<string, object>> GetProcessInfoAsync();
}
}
Services/DeploymentService.cs:
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
namespace MyApp.Services
{
public class DeploymentService : IDeploymentService
{
private readonly ILogger<DeploymentService> _logger;
public DeploymentService(ILogger<DeploymentService> logger)
{
_logger = logger;
}
public async Task<Dictionary<string, object>> VerifyDeploymentAsync()
{
try
{
var verification = new Dictionary<string, object>
{
["timestamp"] = DateTime.UtcNow,
["isWindows"] = RuntimeInformation.IsOSPlatform(OSPlatform.Windows),
["isIIS"] = IsRunningUnderIIS(),
["applicationPath"] = AppDomain.CurrentDomain.BaseDirectory,
["processPath"] = Process.GetCurrentProcess().MainModule?.FileName,
["processId"] = Process.GetCurrentProcess().Id,
["threadCount"] = Process.GetCurrentProcess().Threads.Count,
["startTime"] = Process.GetCurrentProcess().StartTime,
["machineName"] = Environment.MachineName,
["userName"] = Environment.UserName,
["domainName"] = Environment.UserDomainName,
["osVersion"] = Environment.OSVersion.VersionString,
["frameworkDescription"] = RuntimeInformation.FrameworkDescription,
["runtimeVersion"] = Environment.Version.ToString(),
["totalPhysicalMemory"] = GC.GetGCMemoryInfo().TotalAvailableMemoryBytes,
["workingSet"] = Process.GetCurrentProcess().WorkingSet64
};
// ✅ Check for required files
var requiredFiles = new[]
{
"MyApp.dll",
"MyApp.runtimeconfig.json",
"web.config",
"appsettings.json"
};
var fileStatus = new Dictionary<string, bool>();
var appPath = AppDomain.CurrentDomain.BaseDirectory;
foreach (var file in requiredFiles)
{
var fullPath = Path.Combine(appPath, file);
fileStatus[file] = File.Exists(fullPath);
}
verification["requiredFiles"] = fileStatus;
verification["allRequiredFilesPresent"] = fileStatus.Values.All(x => x);
_logger.LogInformation("Deployment verification completed");
return await Task.FromResult(verification);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error during deployment verification");
throw;
}
}
public async Task<Dictionary<string, object>> CheckPrerequisitesAsync()
{
try
{
var prerequisites = new Dictionary<string, object>
{
["dotnetInstalled"] = IsDotNetInstalled(),
["iisInstalled"] = IsIISInstalled(),
["aspNetCoreModuleInstalled"] = IsAspNetCoreModuleInstalled(),
["requiredPortsAvailable"] = AreRequiredPortsAvailable(),
["diskSpaceAvailable"] = GetDiskSpaceInfo(),
["memoryAvailable"] = GC.GetGCMemoryInfo().TotalAvailableMemoryBytes
};
_logger.LogInformation("Prerequisites check completed");
return await Task.FromResult(prerequisites);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error during prerequisites check");
throw;
}
}
public async Task<Dictionary<string, object>> GetFileSystemInfoAsync()
{
try
{
var appPath = AppDomain.CurrentDomain.BaseDirectory;
var directoryInfo = new DirectoryInfo(appPath);
var fileSystemInfo = new Dictionary<string, object>
{
["applicationPath"] = appPath,
["directoryExists"] = Directory.Exists(appPath),
["creationTime"] = directoryInfo.CreationTime,
["lastWriteTime"] = directoryInfo.LastWriteTime,
["attributes"] = directoryInfo.Attributes.ToString(),
["totalFiles"] = directoryInfo.GetFiles("*", SearchOption.AllDirectories).Length,
["totalSubdirectories"] = directoryInfo.GetDirectories("*", SearchOption.AllDirectories).Length,
["sizeInBytes"] = directoryInfo.GetFiles("*", SearchOption.AllDirectories).Sum(f => f.Length),
["freeSpace"] = new DriveInfo(appPath).AvailableFreeSpace
};
// ✅ Get file permissions
var permissions = new Dictionary<string, object>();
try
{
var testFile = Path.Combine(appPath, "permission_test.tmp");
File.WriteAllText(testFile, "test");
File.Delete(testFile);
permissions["canWrite"] = true;
}
catch
{
permissions["canWrite"] = false;
}
fileSystemInfo["permissions"] = permissions;
_logger.LogInformation("File system info retrieved");
return await Task.FromResult(fileSystemInfo);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting file system info");
throw;
}
}
public async Task<bool> RestartApplicationAsync()
{
try
{
// ✅ For IIS in-process hosting, restart by touching web.config
var webConfigPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "web.config");
if (File.Exists(webConfigPath))
{
var fileInfo = new FileInfo(webConfigPath);
File.SetLastWriteTimeUtc(webConfigPath, DateTime.UtcNow.AddSeconds(1));
_logger.LogInformation("Application restarted by updating web.config timestamp");
return true;
}
_logger.LogWarning("web.config not found, cannot restart application");
return false;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error restarting application");
return false;
}
}
public async Task<Dictionary<string, object>> GetProcessInfoAsync()
{
try
{
var process = Process.GetCurrentProcess();
var processInfo = new Dictionary<string, object>
{
["processId"] = process.Id,
["processName"] = process.ProcessName,
["startTime"] = process.StartTime,
["totalProcessorTime"] = process.TotalProcessorTime,
["workingSet"] = process.WorkingSet64,
["peakWorkingSet"] = process.PeakWorkingSet64,
["virtualMemorySize"] = process.VirtualMemorySize64,
["peakVirtualMemorySize"] = process.PeakVirtualMemorySize64,
["privateMemorySize"] = process.PrivateMemorySize64,
["handles"] = process.HandleCount,
["threads"] = process.Threads.Count,
["mainWindowTitle"] = process.MainWindowTitle,
["machineName"] = process.MachineName,
["sessionId"] = process.SessionId
};
_logger.LogInformation("Process info retrieved");
return await Task.FromResult(processInfo);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting process info");
throw;
}
}
private bool IsRunningUnderIIS()
{
try
{
var serverSoftware = Environment.GetEnvironmentVariable("SERVER_SOFTWARE");
return !string.IsNullOrEmpty(serverSoftware) && serverSoftware.Contains("IIS");
}
catch
{
return false;
}
}
private bool IsDotNetInstalled()
{
try
{
var psi = new ProcessStartInfo
{
FileName = "dotnet",
Arguments = "--version",
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true
};
using var process = Process.Start(psi);
process?.WaitForExit(2000);
return process?.ExitCode == 0;
}
catch
{
return false;
}
}
private bool IsIISInstalled()
{
try
{
// ✅ Check if IIS management tools are available
return Directory.Exists(@"C:\Windows\System32\inetsrv");
}
catch
{
return false;
}
}
private bool IsAspNetCoreModuleInstalled()
{
try
{
// ✅ Check for ASP.NET Core Module in web.config or registry
var webConfigPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "web.config");
if (File.Exists(webConfigPath))
{
var content = File.ReadAllText(webConfigPath);
return content.Contains("AspNetCoreModuleV2") || content.Contains("AspNetCoreModule");
}
return false;
}
catch
{
return false;
}
}
private bool AreRequiredPortsAvailable()
{
try
{
// ✅ Check if common ports are available (this is a simplified check)
// In a real implementation, you'd check the specific ports your app needs
return true;
}
catch
{
return false;
}
}
private Dictionary<string, object> GetDiskSpaceInfo()
{
try
{
var appPath = AppDomain.CurrentDomain.BaseDirectory;
var drive = new DriveInfo(appPath);
return new Dictionary<string, object>
{
["driveName"] = drive.Name,
["driveType"] = drive.DriveType.ToString(),
["driveFormat"] = drive.DriveFormat,
["totalSize"] = drive.TotalSize,
["totalFreeSpace"] = drive.TotalFreeSpace,
["availableFreeSpace"] = drive.AvailableFreeSpace,
["isReady"] = drive.IsReady
};
}
catch (Exception ex)
{
return new Dictionary<string, object> { ["error"] = ex.Message };
}
}
}
}
Program.cs with Deployment Service:
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using MyApp.Services;
var builder = WebApplication.CreateBuilder(args);
// ✅ Add services to the container
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// ✅ Add deployment service
builder.Services.AddScoped<IDeploymentService, DeploymentService>();
var app = builder.Build();
// ✅ Configure the HTTP request pipeline
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.MapControllers();
app.Run();
Working Code Examples
Complete IIS Deployment Ready Application:
// Program.cs
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using MyApp.Services;
var builder = WebApplication.CreateBuilder(args);
// ✅ Add services to the container
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// ✅ Add deployment service
builder.Services.AddScoped<IDeploymentService, DeploymentService>();
var app = builder.Build();
// ✅ Configure the HTTP request pipeline
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.MapControllers();
// ✅ Log startup information
using (var scope = app.Services.CreateScope())
{
var deploymentService = scope.ServiceProvider.GetRequiredService<IDeploymentService>();
try
{
var verification = await deploymentService.VerifyDeploymentAsync();
var logger = scope.ServiceProvider.GetRequiredService<ILogger<Program>>();
logger.LogInformation("Application deployed successfully. Verification: {@Verification}", verification);
}
catch (Exception ex)
{
var logger = scope.ServiceProvider.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "Error during deployment verification");
}
}
app.Run();
Unit Test Example:
// MyApp.Tests/UnitTests.cs
using Microsoft.Extensions.DependencyInjection;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MyApp.Services;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace MyApp.Tests
{
[TestClass]
public class IISDeploymentTests
{
private ServiceProvider _serviceProvider;
private DeploymentService _deploymentService;
[TestInitialize]
public void Setup()
{
var services = new ServiceCollection();
services.AddScoped<IDeploymentService, DeploymentService>();
_serviceProvider = services.BuildServiceProvider();
_deploymentService = _serviceProvider.GetRequiredService<DeploymentService>();
}
[TestMethod]
public async Task DeploymentService_VerifyDeployment_ReturnsValidInfo()
{
// ✅ Act
var result = await _deploymentService.VerifyDeploymentAsync();
// ✅ Assert
Assert.IsNotNull(result);
Assert.IsTrue(result.ContainsKey("timestamp"));
Assert.IsTrue(result.ContainsKey("applicationPath"));
Assert.IsTrue(result.ContainsKey("processId"));
}
[TestMethod]
public async Task DeploymentService_CheckPrerequisites_ReturnsValidInfo()
{
// ✅ Act
var result = await _deploymentService.CheckPrerequisitesAsync();
// ✅ Assert
Assert.IsNotNull(result);
Assert.IsTrue(result.ContainsKey("dotnetInstalled"));
Assert.IsTrue(result.ContainsKey("iisInstalled"));
}
[TestMethod]
public async Task DeploymentService_GetFileSystemInfo_ReturnsValidInfo()
{
// ✅ Act
var result = await _deploymentService.GetFileSystemInfoAsync();
// ✅ Assert
Assert.IsNotNull(result);
Assert.IsTrue(result.ContainsKey("applicationPath"));
Assert.IsTrue(result.ContainsKey("directoryExists"));
}
[TestMethod]
public async Task DeploymentService_GetProcessInfo_ReturnsValidInfo()
{
// ✅ Act
var result = await _deploymentService.GetProcessInfoAsync();
// ✅ Assert
Assert.IsNotNull(result);
Assert.IsTrue(result.ContainsKey("processId"));
Assert.IsTrue(result.ContainsKey("workingSet"));
}
[TestMethod]
public async Task DeploymentService_RestartApplication_ReturnsBool()
{
// ✅ Act
var result = await _deploymentService.RestartApplicationAsync();
// ✅ Assert
Assert.IsInstanceOfType(result, typeof(bool));
}
[TestCleanup]
public void Cleanup()
{
_serviceProvider?.Dispose();
}
}
}
Best Practices for IIS Deployment
1. Always Use Application Pool Identity
<!-- ✅ Use ApplicationPoolIdentity for security -->
<system.applicationHost>
<applicationPools>
<add name="MyAppPool">
<processModel identityType="ApplicationPoolIdentity" />
</add>
</applicationPools>
</system.applicationHost>
2. Configure Proper File Permissions
# ✅ PowerShell to set proper permissions
$sitePath = "C:\inetpub\wwwroot\MyApp"
$appPoolName = "IIS AppPool\MyAppPool"
$acl = Get-Acl $sitePath
$accessRule = New-Object System.Security.AccessControl.FileSystemAccessRule($appPoolName, "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow")
$acl.SetAccessRule($accessRule)
Set-Acl $sitePath $acl
3. Use In-Process Hosting for .NET Core
<!-- ✅ Use in-process hosting for better performance -->
<aspNetCore processPath="dotnet"
arguments=".\MyApp.dll"
stdoutLogEnabled="false"
stdoutLogFile=".\logs\stdout"
hostingModel="inprocess">
</aspNetCore>
4. Enable Detailed Error Logging
<!-- ✅ Enable logging for troubleshooting -->
<aspNetCore processPath="dotnet"
arguments=".\MyApp.dll"
stdoutLogEnabled="true"
stdoutLogFile=".\logs\stdout">
</aspNetCore>
5. Configure Custom Error Pages
<!-- ✅ Configure custom error pages for better UX -->
<system.webServer>
<httpErrors errorMode="Custom">
<error statusCode="403" path="/errors/403.html" responseMode="ExecuteURL" />
<error statusCode="404" path="/errors/404.html" responseMode="ExecuteURL" />
</httpErrors>
</system.webServer>
6. Use Environment Variables
<!-- ✅ Use environment variables for configuration -->
<aspNetCore processPath="dotnet" arguments=".\MyApp.dll">
<environmentVariables>
<environmentVariable name="ASPNETCORE_ENVIRONMENT" value="Production" />
</environmentVariables>
</aspNetCore>
Debugging Steps
Step 1: Check IIS Logs
# ✅ Check IIS logs for error details
# Default location: C:\inetpub\logs\LogFiles\
# Or custom location specified in IIS Manager
Step 2: Check Application Event Logs
# ✅ Check Windows Event Viewer for application errors
Get-WinEvent -FilterHashtable @{LogName='Application'; Level=1,2,3} | Where-Object {$_.ProviderName -eq 'IIS-APPHOSTSVC'}
Step 3: Verify Application Pool Settings
# ✅ Check application pool configuration
Get-ItemProperty "IIS:\AppPools\MyAppPool"
Step 4: Test File Permissions
# ✅ Test if the application pool identity can access files
Test-Path "C:\inetpub\wwwroot\MyApp\web.config"
Step 5: Check .NET Installation
# ✅ Verify .NET runtime is installed
dotnet --info
Step 6: Verify ASP.NET Core Module
# ✅ Check if ASP.NET Core Module is installed
Get-ChildItem "C:\Windows\System32\inetsrv\" | Where-Object {$_.Name -like "*aspnetcore*"}
Common Mistakes to Avoid
1. Not Installing ASP.NET Core Hosting Bundle
# ❌ Forgetting to install the hosting bundle on the server
# Download from: https://dotnet.microsoft.com/download/dotnet-core
2. Incorrect Application Pool Identity
<!-- ❌ Using NetworkService or LocalSystem unnecessarily -->
<processModel identityType="NetworkService" /> <!-- ❌ Security risk -->
3. Missing web.config
<!-- ❌ Deploying without proper web.config for IIS -->
<!-- Always include ASP.NET Core module configuration -->
4. Wrong File Permissions
# ❌ Not granting proper permissions to application pool identity
# Always grant minimum required permissions
5. Not Setting Environment Variables
<!-- ❌ Not setting ASPNETCORE_ENVIRONMENT -->
<!-- This can cause configuration issues -->
Performance Considerations
1. Use In-Process Hosting
<!-- ✅ In-process hosting provides better performance -->
<aspNetCore hostingModel="inprocess">
2. Optimize Compression
<!-- ✅ Enable compression for better performance -->
<httpCompression doStaticCompression="true" doDynamicCompression="true">
3. Configure Appropriate Timeouts
<!-- ✅ Set appropriate request timeouts -->
<httpRuntime executionTimeout="300" maxRequestLength="51200" />
4. Monitor Resource Usage
// ✅ Monitor memory and CPU usage in your application
var memory = GC.GetGCMemoryInfo().TotalAvailableMemoryBytes;
var cpu = Process.GetCurrentProcess().TotalProcessorTime;
Security Considerations
1. Use Least Privilege
# ✅ Grant only necessary permissions to application pool identity
# Never use high-privilege accounts unnecessarily
2. Secure Web.config
<!-- ✅ Restrict access to sensitive files -->
<security>
<requestFiltering>
<fileExtensions>
<add fileExtension=".config" allowed="false" />
</fileExtensions>
</requestFiltering>
</security>
3. Enable HTTPS
<!-- ✅ Force HTTPS in production -->
<system.webServer>
<rewrite>
<rules>
<rule name="Force HTTPS" enabled="true">
<match url="(.*)" />
<conditions>
<add input="{HTTPS}" pattern="^OFF$" />
</conditions>
<action type="Redirect" url="https://{HTTP_HOST}/{R:1}" redirectType="Permanent" />
</rule>
</rules>
</rewrite>
</system.webServer>
4. Regular Security Updates
# ✅ Keep IIS and .NET runtime updated
# Apply security patches regularly
Testing Deployment Scenarios
1. Test File Access
[TestMethod]
public async Task DeploymentService_VerifyFileAccess_Succeeds()
{
var service = new DeploymentService(mockLogger);
var result = await service.GetFileSystemInfoAsync();
Assert.IsTrue((bool)result["directoryExists"]);
}
2. Test Prerequisites
[TestMethod]
public async Task DeploymentService_CheckPrerequisites_Succeeds()
{
var service = new DeploymentService(mockLogger);
var result = await service.CheckPrerequisitesAsync();
Assert.IsTrue((bool)result["dotnetInstalled"]);
}
3. Test Process Information
[TestMethod]
public async Task DeploymentService_GetProcessInfo_Succeeds()
{
var service = new DeploymentService(mockLogger);
var result = await service.GetProcessInfoAsync();
Assert.IsTrue(result.ContainsKey("processId"));
}
4. Test Application Restart
[TestMethod]
public async Task DeploymentService_RestartApplication_Succeeds()
{
var service = new DeploymentService(mockLogger);
var result = await service.RestartApplicationAsync();
// Result may vary based on environment, but should not throw
Assert.IsInstanceOfType(result, typeof(bool));
}
Alternative Solutions
1. Use IIS Express for Development
<!-- ✅ Use IIS Express locally, full IIS for production -->
<!-- Different configurations for different environments -->
2. Consider Azure App Service
# ✅ For cloud deployments, consider Azure App Service
# Handles many IIS configuration issues automatically
3. Use Docker Containers
# ✅ Containerize your application to avoid IIS-specific issues
FROM mcr.microsoft.com/dotnet/aspnet:6.0
COPY . /app
WORKDIR /app
ENTRYPOINT ["dotnet", "MyApp.dll"]
Migration Checklist
- Install .NET Core Hosting Bundle on IIS server
- Create application pool with appropriate .NET version
- Set application pool identity to ApplicationPoolIdentity
- Create website/application in IIS Manager
- Set proper file system permissions for application pool identity
- Verify web.config contains ASP.NET Core module configuration
- Test application with browser and verify 200 status
- Check IIS logs for any errors
- Configure custom error pages if needed
- Set up monitoring and alerting for the application
Conclusion
The ‘IIS 403/404 After Deployment’ error is a common but preventable issue that occurs when .NET applications are not properly configured for IIS hosting. By following the solutions provided in this guide—implementing proper IIS configuration, setting appropriate permissions, configuring application pools correctly, and following best practices—you can effectively prevent and resolve these errors in your .NET applications.
The key is to understand IIS deployment requirements, implement proper configuration patterns, use modern .NET features correctly, and maintain clean, well-organized code. With proper IIS deployment configuration, your .NET applications will run reliably in production environments and avoid common deployment errors.
Remember to test your changes thoroughly, follow IIS best practices for deployment, implement proper error handling, and regularly review your deployment procedures to ensure your applications maintain the best possible architecture and avoid common IIS deployment errors like 403 and 404 errors.
Related Articles
Fix: Connection String Not Working in .NET - Complete Configuration Guide
Learn how to fix connection string errors in .NET applications. This comprehensive guide covers connection string configuration, troubleshooting, and proper database connectivity techniques.
Fix: Entity Framework Migration Not Working Error - Complete EF Core Guide
Learn how to fix Entity Framework migration errors in .NET applications. This comprehensive guide covers migration troubleshooting, database synchronization, and proper EF Core configuration techniques.
Fix: CS0246: The type or namespace name could not be found error
Learn how to fix the 'CS0246: The type or namespace name could not be found' error in C# applications. This comprehensive guide covers using statements, assembly references, and proper namespace management.