Configuration files are the unsung source of many production outages. A misplaced comma in appsettings.json or a forgotten connection string in Web.config can bring down an entire application. This guide focuses on the most frequent and damaging configuration mistakes in ASP.NET projects, explaining why they happen and how to fix them for good. We write for developers who have felt the sting of a staging environment that behaves differently from production, or who have accidentally committed secrets to source control. By the end, you will have a clear mental model for structuring configuration that is safe, maintainable, and environment-aware.
Why Configuration Mistakes Persist in ASP.NET Apps
Configuration errors are not just about typos. They often stem from a mismatch between how developers think about configuration and how the runtime actually loads it. In classic ASP.NET, Web.config is an XML file with a strict schema. A single unclosed tag or an incorrect section group can cause a yellow screen of death. In .NET Core and later, appsettings.json uses JSON, which is more forgiving but introduces its own traps: silent fallback defaults, case sensitivity, and unexpected binding behavior.
Another layer of complexity comes from environment-specific settings. Many teams start with a single appsettings.json, then add appsettings.Development.json, appsettings.Staging.json, and appsettings.Production.json. The layering rules are not always intuitive, and developers often assume that production values override everything when in fact the order of precedence is: environment variables, command-line arguments, then JSON files in reverse order of specificity. This can lead to settings that are ignored or overwritten without warning.
Security is another major pain point. Connection strings, API keys, and other secrets often end up in plain text inside configuration files that get checked into source control. Even when teams use environment variables, they may forget to set them in all environments, causing the app to fall back to a default that is insecure or broken. The result is a fragile system that works on one machine and fails on another.
Finally, there is the human factor: configuration files are rarely reviewed with the same rigor as code. A pull request for a new feature might get thorough scrutiny, but a change to a connection string in Web.config often slips through without a second look. This guide aims to change that by giving you a checklist of common pitfalls and the reasoning behind each fix.
Common Misconceptions About Configuration Layering
Many developers believe that appsettings.Production.json always wins over appsettings.json. In reality, the default configuration builder in .NET loads appsettings.json first, then appsettings.{Environment}.json, and finally environment variables. Because later sources override earlier ones, environment variables take highest precedence. This means that if you have a setting in appsettings.Production.json and also in an environment variable, the environment variable wins. Teams that rely solely on JSON files for production settings may be surprised when a server-level environment variable overrides their carefully tuned values.
The Cost of Configuration Errors
While a single misconfiguration might seem minor, its impact can be severe. A database connection pointing to the wrong server can cause data corruption or downtime. An incorrect CORS origin can block legitimate API calls or open the door to cross-origin attacks. In one composite scenario, a team spent three days debugging a staging environment that kept crashing because the appsettings.Staging.json file had a trailing comma after the last property, which the JSON parser rejected. The fix was a single character, but the cost in developer time and delayed releases was significant. Understanding these patterns helps you avoid similar traps.
Core Principles for Robust Configuration
Before diving into specific mistakes, it helps to establish a few guiding principles. First, treat configuration as code: it should be version-controlled, reviewed, and tested. Second, separate secrets from non-sensitive settings. Third, use environment-specific files only for values that actually change between environments. Fourth, prefer structured configuration objects over flat key-value pairs for clarity and type safety.
In ASP.NET Core, the Options pattern is the recommended way to access configuration. Instead of reading Configuration["Database:ConnectionString"] throughout your code, you bind a section to a strongly typed class and inject IOptions<DatabaseOptions>. This approach catches binding errors at startup rather than at runtime, and it makes your configuration contract explicit. For example, if your JSON has "Database": { "ConnectionString": "...", "Timeout": 30 }, you can define a DatabaseOptions class with matching properties. If the property names do not match exactly (case-insensitive by default), the binding will silently skip them, which is a common source of bugs. We recommend validating options at startup using ValidateOnStart or a custom validation attribute.
Another core principle is to avoid hardcoding environment names. Instead of checking env.IsDevelopment() in multiple places, centralize environment-specific logic in configuration files or startup filters. This makes it easier to add a new environment later without hunting through code.
Why the Options Pattern Matters
Without the Options pattern, you might scatter configuration reads across your codebase, making it hard to track where each value comes from. When a setting changes, you have to update every reference. With the Options pattern, you change the configuration source or the binding class once, and the rest of the application picks up the change automatically (if you use IOptionsSnapshot or IOptionsMonitor for reloadable settings). This pattern also enables unit testing: you can inject mock options without touching the file system.
Configuration Validation at Startup
One of the best practices we have adopted is to validate configuration as early as possible. In ASP.NET Core, you can call services.AddOptions<DatabaseOptions>().Bind(Configuration.GetSection("Database")).ValidateDataAnnotations().ValidateOnStart(). This throws a descriptive exception at application startup if a required field is missing or a value is out of range. Without this, a misconfigured setting might go unnoticed until a user triggers the code path that uses it, resulting in a confusing runtime error.
Common Mistakes in Web.config and How to Fix Them
Even though .NET Core has moved toward JSON, many ASP.NET applications—especially those running on .NET Framework—still rely heavily on Web.config. Here are the most frequent mistakes we see.
Mistake 1: Connection Strings with Hardcoded Credentials
It is still common to find connection strings like Server=myServer;Database=myDb;User Id=sa;Password=PlainTextPassword; inside Web.config. This is a security risk if the file is ever exposed, and it makes rotating credentials painful. The fix is to use integrated security where possible, or to encrypt the <connectionStrings> section using aspnet_regiis -pef. For on-premises deployments, consider using a protected configuration provider. For cloud deployments, use Azure Key Vault or environment variables instead.
Mistake 2: Incorrect Section Ordering
Web.config sections must appear in a specific order defined by the schema. Placing <system.web> after <system.webServer> can cause a configuration error. The safest approach is to let Visual Studio manage the order, or to use a schema-aware editor. If you are editing manually, always insert new sections in the correct position relative to existing ones.
Mistake 3: Missing or Misconfigured Custom Errors
In production, you want <customErrors mode="RemoteOnly" defaultRedirect="Error.aspx" /> to avoid showing stack traces to end users. A common mistake is leaving the mode as Off or forgetting to set a default redirect. This exposes sensitive information and creates a poor user experience. Always test that custom errors work correctly by simulating an error in a staging environment.
Mistake 4: Overusing AppSettings in Web.config
The <appSettings> section is convenient, but it encourages a flat key-value structure that becomes unwieldy as the application grows. Keys like EmailFromAddress, EmailSmtpServer, EmailPort are better grouped into a custom section or, if you can migrate, into a JSON file. For new development on .NET Framework, consider using a custom configuration section that maps to a strongly typed class.
Common Mistakes in appsettings.json and How to Fix Them
With .NET Core and later, appsettings.json is the primary configuration file. Here are the pitfalls we see most often.
Mistake 5: Trailing Commas in JSON
JSON does not allow trailing commas, but many editors and copy-paste operations introduce them. A single trailing comma after the last property in an object or the last element in an array will cause the JSON parser to throw an exception at startup. The fix is to use a linter or an editor that highlights JSON errors. Also, consider enabling config.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) so that startup fails fast if the file is invalid.
Mistake 6: Case Sensitivity in Property Names
By default, the JSON configuration provider is case-insensitive, but the binding to a POCO class is case-sensitive by convention (it matches exact casing if you use Bind without options). A common mistake is to have "database" in JSON but bind to a class with property Database. While this works because of case-insensitivity, it can lead to confusion when you later add environment variables that are case-sensitive on Linux. We recommend using consistent camelCase in JSON and PascalCase in C# classes, and relying on the default binder which handles the mapping.
Mistake 7: Relying on Default Values That Are Never Overridden
It is tempting to define default values in appsettings.json and only override them in environment-specific files. However, if you forget to add the override in production, the default value is used, which might be inappropriate (e.g., a development database connection string). The fix is to make the default values safe for production, or to leave them empty and validate that they are provided. Using ValidateOnStart with a required attribute ensures that no setting is accidentally left at its default.
Mistake 8: Not Using Environment Variables for Secrets
Even with appsettings.json, many teams still put API keys and passwords directly in the file. The better approach is to use environment variables or a secret manager like Azure Key Vault. In ASP.NET Core, you can add environment variables as a configuration source with config.AddEnvironmentVariables(). By convention, environment variables with double underscore separators (e.g., Database__ConnectionString) map to hierarchical keys. This keeps secrets out of source control and makes it easy to set different values per environment.
Edge Cases and Exceptions in Configuration Loading
Even when you follow best practices, configuration loading can behave unexpectedly in certain scenarios. Understanding these edge cases helps you debug faster.
Edge Case 1: Reload on Change with Large Files
ASP.NET Core supports reloading configuration when a JSON file changes, but this can cause performance issues if the file is large or changes frequently. The file watcher uses FileSystemWatcher, which may raise multiple events for a single save operation. This can trigger multiple reloads and potentially cause race conditions if your application is not designed to handle them. The workaround is to debounce the reload or to avoid reloadOnChange: true for files that change often. For most applications, the default behavior works fine, but be aware of this limitation in high-throughput scenarios.
Edge Case 2: XML Configuration in .NET Core via System.Configuration
Some legacy libraries still expect to read from Web.config using System.Configuration.ConfigurationManager. In .NET Core, this API is available as a NuGet package (System.Configuration.ConfigurationManager), but it only reads from the app's .config file (e.g., MyApp.dll.config), not from appsettings.json. This can lead to a situation where part of the application uses the new configuration system and another part uses the old one, causing inconsistencies. The fix is to migrate all configuration to the new system or to use a bridge that reads from appsettings.json and populates the AppSettings collection.
Edge Case 3: Configuration in Docker Containers
When running ASP.NET Core in Docker, environment variables are the primary way to inject configuration. However, Docker environment variables are set at container start and cannot be changed without restarting the container. If you rely on reloadOnChange for JSON files, those files are baked into the image and cannot be changed at runtime. The best practice is to use environment variables for values that change per environment, and keep static defaults in the image. Also, be aware that Docker Compose allows setting environment variables in the docker-compose.yml file, which is a convenient way to manage per-service configuration.
Limits of Configuration Approaches and When to Reconsider
No configuration strategy is perfect. Here are the limits of the approaches we have discussed and when you might need something different.
When the Options Pattern Becomes Overkill
The Options pattern adds boilerplate: you need a class, registration code, and validation. For small applications with only a handful of settings, it might be simpler to read values directly from IConfiguration. However, as the application grows, the benefits of type safety and testability outweigh the initial overhead. A good rule of thumb is to use the Options pattern for any setting that is used in more than one place or that has validation requirements.
When Environment Variables Are Not Enough
Environment variables are great for secrets and per-environment values, but they are flat strings. If you need to pass complex nested objects, environment variables become unwieldy. In that case, consider using a configuration service like Azure App Configuration or Consul, which can store hierarchical data and push updates to your application in real time. These tools also support feature flags, which are difficult to manage with plain configuration files.
When XML Configuration Still Makes Sense
For applications that run on .NET Framework and rely on IIS, Web.config is unavoidable for certain settings like IIS modules, handlers, and URL rewrite rules. Trying to move those to JSON would require custom middleware and is usually not worth the effort. The key is to keep the XML configuration minimal and move application-level settings to a separate JSON file or database.
When to Use a Database for Configuration
If your application needs to change configuration at runtime without a restart (e.g., feature toggles, A/B test parameters), a database or a distributed cache is a better fit than file-based configuration. ASP.NET Core supports custom configuration providers, so you can write one that reads from SQL Server or Redis. This gives you the ability to update settings centrally and have all instances pick up the change within seconds.
Practical Next Steps for Cleaner Configuration
Based on the mistakes and principles we have covered, here are five specific actions you can take starting today.
- Audit your current configuration files. Search for hardcoded secrets, trailing commas, and missing environment-specific files. Use a JSON linter to validate all appsettings files.
- Implement the Options pattern for at least one section. Pick the section that causes the most runtime errors (often database or external service URLs) and create a strongly typed class with validation.
- Move all secrets to environment variables or a secret store. Remove any connection string or API key from source control. Use
dotnet user-secretsfor development and environment variables for production. - Add startup validation. Call
ValidateOnStartfor all options that are critical to the application's core functionality. This will catch misconfigurations before any request is served. - Set up a configuration review checklist for pull requests. Include items like: are there any new secrets? Are environment-specific files updated? Is the JSON valid? Are there any hardcoded values that should be configurable?
Configuration is not glamorous, but getting it right saves hours of debugging and prevents security incidents. By treating configuration with the same care as your application code, you build a more resilient system that behaves predictably across all environments.
Comments (0)
Please sign in to post a comment.
Don't have an account? Create one
No comments yet. Be the first to comment!