Add dotnet-ef JSON config defaults and validation features#37966
Add dotnet-ef JSON config defaults and validation features#37966IMZihad21 wants to merge 2 commits intodotnet:mainfrom
Conversation
Add .config/dotnet-ef.json discovery from current directory up to repo root and apply config values for project, startup project, framework, configuration, and context when CLI options are not explicitly provided. Introduce resource-backed validation and error handling for invalid JSON, unsupported/unknown properties, invalid value types, and unreadable files. Add dotnet-ef tests for config discovery/precedence, path resolution, argument propagation, and failure scenarios. Impact: users can set consistent dotnet-ef defaults in source-controlled config with existing CLI precedence preserved, reducing repetitive command arguments without breaking current behavior.
@dotnet-policy-service agree |
There was a problem hiding this comment.
Pull request overview
This PR adds support for loading default dotnet ef options from a repo-local JSON config file (.config/dotnet-ef.json), applying those defaults when corresponding CLI options aren’t provided, while keeping explicit CLI arguments authoritative.
Changes:
- Introduces config discovery/loading + validation via
DotNetEfConfigLoader(walk-up discovery, path resolution, strict property validation). - Applies config defaults in
dotnet-efexecution flow (project/startup project/framework/configuration) and conditionally forwards a default--contextfor select commands. - Adds resource-backed user-facing errors and new unit tests covering discovery/precedence/path behavior and invalid/unreadable config handling.
Reviewed changes
Copilot reviewed 4 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| test/dotnet-ef.Tests/DotNetEfConfigTest.cs | Adds unit tests for config discovery, precedence, path resolution, and invalid/unreadable config scenarios. |
| src/dotnet-ef/RootCommand.cs | Loads config defaults and conditionally appends --context to forwarded args; adds helpers for context applicability/option detection. |
| src/dotnet-ef/Properties/Resources.resx | Adds new localized strings for config read/validation errors. |
| src/dotnet-ef/Properties/Resources.Designer.cs | Adds strongly-typed accessors for the new config error strings. |
| src/dotnet-ef/DotNetEfConfigLoader.cs | Implements config discovery, JSON parsing, validation, and relative path resolution. |
Files not reviewed (1)
- src/dotnet-ef/Properties/Resources.Designer.cs: Language not supported
src/dotnet-ef/RootCommand.cs
Outdated
| var skipOptimization = remainingArguments.Count > 2 | ||
| && remainingArguments[0] == "dbcontext" | ||
| && remainingArguments[1] == "optimize" | ||
| && !remainingArguments.Any(a => a == "--no-scaffold"); | ||
| startupProject.Build(skipOptimization ? ["/p:EFScaffoldModelStage=none", "/p:EFPrecompileQueriesStage=none"] : null); |
There was a problem hiding this comment.
skipOptimization is now computed from remainingArguments, which may include a config-injected --context. That changes behavior for dbcontext optimize when the user didn't pass any extra args: previously _args.Count > 2 would be false, but with config defaults it can become true and pass /p:EFScaffoldModelStage=none and /p:EFPrecompileQueriesStage=none unexpectedly. Consider basing this check on the original user args (or removing the Count > 2 gate if it's not needed) so config defaults don't affect build behavior.
src/dotnet-ef/RootCommand.cs
Outdated
| => args.Any( | ||
| argument => names.Any( | ||
| name => string.Equals(argument, name, StringComparison.Ordinal) | ||
| || argument.StartsWith(name + "=", StringComparison.Ordinal))); |
There was a problem hiding this comment.
ContainsOption only treats name= as an inline option form, but this CLI parser also supports name:value (it splits on both ':' and '='). If a user passes --context:ExplicitContext (or -c:ExplicitContext), ResolveContext won't detect it and may append a second --context from config. Update the detection to treat both name= and name: as inline forms (and add/adjust a test case to cover this).
| || argument.StartsWith(name + "=", StringComparison.Ordinal))); | |
| || argument.StartsWith(name + "=", StringComparison.Ordinal) | |
| || argument.StartsWith(name + ":", StringComparison.Ordinal))); |
| var value = ValidateValue(fullPath, property); | ||
|
|
||
| switch (property.Name) | ||
| { | ||
| case "project": | ||
| project = ResolvePath(configDirectory, value); | ||
| break; | ||
| case "startupProject": | ||
| startupProject = ResolvePath(configDirectory, value); | ||
| break; | ||
| case "context": | ||
| context = value; | ||
| break; | ||
| case "framework": | ||
| framework = value; | ||
| break; | ||
| case "configuration": | ||
| configuration = value; |
There was a problem hiding this comment.
ValidateValue is called before switching on the property name, so forbidden/unknown properties with non-string values (e.g. { "connection": 1 }) will currently throw the "must be a non-empty JSON string" error instead of the more accurate "property isn't supported" / "remove unsupported property" message. Consider switching on property.Name first and only validating the value for supported properties, so error reporting remains accurate regardless of the JSON value kind.
| var value = ValidateValue(fullPath, property); | |
| switch (property.Name) | |
| { | |
| case "project": | |
| project = ResolvePath(configDirectory, value); | |
| break; | |
| case "startupProject": | |
| startupProject = ResolvePath(configDirectory, value); | |
| break; | |
| case "context": | |
| context = value; | |
| break; | |
| case "framework": | |
| framework = value; | |
| break; | |
| case "configuration": | |
| configuration = value; | |
| switch (property.Name) | |
| { | |
| case "project": | |
| project = ResolvePath(configDirectory, ValidateValue(fullPath, property)); | |
| break; | |
| case "startupProject": | |
| startupProject = ResolvePath(configDirectory, ValidateValue(fullPath, property)); | |
| break; | |
| case "context": | |
| context = ValidateValue(fullPath, property); | |
| break; | |
| case "framework": | |
| framework = ValidateValue(fullPath, property); | |
| break; | |
| case "configuration": | |
| configuration = ValidateValue(fullPath, property); |
Keep dbcontext optimize skip behavior based on original user arguments so config-injected defaults do not change build behavior. Recognize both '=' and ':' inline context option forms when detecting explicit CLI input, and validate config values only after dispatching on supported property names so forbidden or unknown properties report the correct error category. Add tests covering explicit inline context options, optimize skip behavior, and forbidden or unknown non-string config properties to lock in the reviewed behavior.
|
Addressed all review points in the latest update.
Please re-check the updated changes. |
Summary
This PR adds support for loading default
dotnet efoptions from.config/dotnet-ef.json.Config is discovered by walking up from the current working directory. When a matching CLI option is not provided, values from config are used.
Supported config properties:
projectstartupProjectframeworkconfigurationcontextCLI options continue to take precedence.
Changes
src/dotnet-ef/DotNetEfConfigLoader.cs.src/dotnet-ef/RootCommand.csto apply config defaults for root options and context forwarding behavior.src/dotnet-ef/Properties/Resources.resxsrc/dotnet-ef/Properties/Resources.Designer.cstest/dotnet-ef.Tests/DotNetEfConfigTest.csfor:Config snapshot
{ "project": "src/App.Infrastructure", "startupProject": "src/App.Api", "framework": "net9.0", "configuration": "Debug", "context": "AppDbContext" }Validation
.\restore.cmd. .\activate.ps1dotnet test test/dotnet-ef.Tests/dotnet-ef.Tests.csprojResult: passed locally.
Impact
dotnet efarguments in repo workflows.Review updates
dbcontext optimizeso config-injected--contextno longer changes skip-optimization behavior.=and:forms, such as--context:Fooand-c:Foo.