using System.Text.Json.Nodes;
namespace NpgsqlRestClient;
///
/// Centralized configuration defaults for NpgsqlRestClient.
/// All default values are defined here to ensure consistency between
/// runtime behavior and --config output.
///
public static class ConfigDefaults
{
// Helper methods to create JsonArray from primitives (AOT-compatible)
// Using JsonNode cast to avoid generic Add which requires runtime code generation
private static JsonArray CreateIntArray(params int[] values)
{
var arr = new JsonArray();
foreach (var v in values) arr.Add((JsonNode)v);
return arr;
}
private static JsonArray CreateStringArray(params string[] values)
{
var arr = new JsonArray();
foreach (var v in values) arr.Add((JsonNode)v);
return arr;
}
// Builds the default RateLimiter Partition Sources array via explicit JsonNode casts so the
// JsonArray collection initializer's generic Add doesn't trigger IL3050 under PublishAot.
private static JsonArray CreatePartitionSourcesArray()
{
var arr = new JsonArray();
arr.Add((JsonNode?)new JsonObject { ["Type"] = "Claim", ["Name"] = "name_identifier" });
arr.Add((JsonNode?)new JsonObject { ["Type"] = "IpAddress" });
arr.Add((JsonNode?)new JsonObject { ["Type"] = "Static", ["Value"] = "anonymous" });
return arr;
}
// IP-only partition sources for the default login_throttle policy (one bucket per client IP).
private static JsonArray CreateIpPartitionSourcesArray()
{
var arr = new JsonArray();
arr.Add((JsonNode?)new JsonObject { ["Type"] = "IpAddress" });
return arr;
}
///
/// Returns a JsonObject containing all default configuration values.
///
public static JsonObject GetDefaults()
{
return new JsonObject
{
["ApplicationName"] = null,
["EnvironmentName"] = "Production",
["Urls"] = "http://localhost:8080",
["StartupMessage"] = "Started in {time}, listening on {urls}, version {version}",
["Config"] = new JsonObject
{
["AddEnvironmentVariables"] = false,
["ParseEnvironmentVariables"] = true,
["EnvFile"] = null,
["ValidateConfigKeys"] = "Warning"
},
["ConnectionStrings"] = new JsonObject
{
["Default"] = "Host=localhost;Port=5432;Database=postgres;Username=postgres;Password=postgres"
},
["ConnectionSettings"] = new JsonObject
{
["SetApplicationNameInConnection"] = true,
["UseJsonApplicationName"] = false,
["TestConnectionStrings"] = true,
["RetryOptions"] = new JsonObject
{
["Enabled"] = true,
["RetrySequenceSeconds"] = CreateIntArray(1, 3, 6, 12),
["ErrorCodes"] = CreateStringArray("08000", "08003", "08006", "08001", "08004", "55P03", "55006", "53300", "57P03", "40001")
},
["MetadataQueryConnectionName"] = null,
["MetadataQuerySchema"] = null,
["MultiHostConnectionTargets"] = new JsonObject
{
["Default"] = "Any",
["ByConnectionName"] = new JsonObject()
}
},
["Ssl"] = new JsonObject
{
["Enabled"] = false,
["UseHttpsRedirection"] = true,
["UseHsts"] = true
},
["DataProtection"] = new JsonObject
{
["Enabled"] = false,
["CustomApplicationName"] = null,
["DefaultKeyLifetimeDays"] = 90,
["Storage"] = "Default",
["FileSystemPath"] = "./data-protection-keys",
["GetAllElementsCommand"] = "select get_data_protection_keys()",
["StoreElementCommand"] = "call store_data_protection_keys($1,$2)",
["EncryptionAlgorithm"] = null,
["ValidationAlgorithm"] = null,
["KeyEncryption"] = "None",
["CertificatePath"] = null,
["CertificatePassword"] = null,
["DpapiLocalMachine"] = false
},
["Kestrel"] = new JsonObject
{
["Endpoints"] = new JsonObject(),
["Certificates"] = new JsonObject(),
["Limits"] = new JsonObject
{
["MaxConcurrentConnections"] = null,
["MaxConcurrentUpgradedConnections"] = null,
["MaxRequestBodySize"] = null,
["MaxRequestBufferSize"] = null,
["MaxRequestHeaderCount"] = null,
["MaxRequestHeadersTotalSize"] = null,
["MaxRequestLineSize"] = null,
["MaxResponseBufferSize"] = null,
["KeepAliveTimeout"] = null,
["RequestHeadersTimeout"] = null,
["Http2"] = new JsonObject
{
["MaxStreamsPerConnection"] = null,
["HeaderTableSize"] = null,
["MaxFrameSize"] = null,
["MaxRequestHeaderFieldSize"] = null,
["InitialConnectionWindowSize"] = null,
["InitialStreamWindowSize"] = null,
["MaxReadFrameSize"] = null,
["KeepAlivePingDelay"] = null,
["KeepAlivePingTimeout"] = null,
["KeepAlivePingPolicy"] = null
},
["Http3"] = new JsonObject
{
["MaxRequestHeaderFieldSize"] = null
}
},
["DisableStringReuse"] = false,
["AllowAlternateSchemes"] = false,
["AllowSynchronousIO"] = false,
["AllowResponseHeaderCompression"] = true,
["AddServerHeader"] = true,
["AllowHostHeaderOverride"] = false
},
["ThreadPool"] = new JsonObject
{
["MinWorkerThreads"] = null,
["MinCompletionPortThreads"] = null,
["MaxWorkerThreads"] = null,
["MaxCompletionPortThreads"] = null
},
["Auth"] = GetAuthDefaults(),
["Log"] = GetLogDefaults(),
["ResponseCompression"] = GetResponseCompressionDefaults(),
["Antiforgery"] = GetAntiforgeryDefaults(),
["StaticFiles"] = GetStaticFilesDefaults(),
["Cors"] = GetCorsDefaults(),
["SecurityHeaders"] = GetSecurityHeadersDefaults(),
["ForwardedHeaders"] = GetForwardedHeadersDefaults(),
["HealthChecks"] = GetHealthChecksDefaults(),
["Stats"] = GetStatsDefaults(),
["CommandRetryOptions"] = GetCommandRetryOptionsDefaults(),
["CacheOptions"] = GetCacheOptionsDefaults(),
["ValidationOptions"] = GetValidationOptionsDefaults(),
["RateLimiterOptions"] = GetRateLimiterOptionsDefaults(),
["ErrorHandlingOptions"] = GetErrorHandlingOptionsDefaults(),
["NpgsqlRest"] = GetNpgsqlRestDefaults()
};
}
private static JsonObject GetAuthDefaults()
{
return new JsonObject
{
["CookieAuth"] = false,
["CookieAuthScheme"] = null,
["CookieValid"] = "14 days",
["CookieName"] = null,
["CookiePath"] = null,
["CookieDomain"] = null,
["CookieMultiSessions"] = true,
["CookieHttpOnly"] = true,
["CookieSameSite"] = null,
["CookieSecure"] = null,
["BearerTokenAuth"] = false,
["BearerTokenAuthScheme"] = null,
["BearerTokenExpire"] = "1 hour",
["BearerTokenRefreshPath"] = "/api/token/refresh",
["JwtAuth"] = false,
["JwtAuthScheme"] = null,
["JwtSecret"] = null,
["JwtIssuer"] = null,
["JwtAudience"] = null,
["JwtExpire"] = "60 minutes",
["JwtRefreshExpire"] = "7 days",
["JwtValidateIssuer"] = false,
["JwtValidateAudience"] = false,
["JwtValidateLifetime"] = true,
["JwtValidateIssuerSigningKey"] = true,
["JwtClockSkew"] = "5 minutes",
["JwtRefreshPath"] = "/api/jwt/refresh",
["Schemes"] = new JsonObject
{
["short_session"] = new JsonObject
{
["Type"] = "Cookies",
["Enabled"] = false,
["CookieValid"] = "1 hour",
["CookieMultiSessions"] = false
},
["api_token"] = new JsonObject
{
["Type"] = "BearerToken",
["Enabled"] = false,
["BearerTokenExpire"] = "30 minutes",
["BearerTokenRefreshPath"] = "/api/api-token/refresh"
},
["admin_jwt"] = new JsonObject
{
["Type"] = "Jwt",
["Enabled"] = false,
["JwtSecret"] = null,
["JwtIssuer"] = null,
["JwtAudience"] = null,
["JwtExpire"] = "5 minutes",
["JwtRefreshExpire"] = "1 hour",
["JwtRefreshPath"] = "/api/admin-jwt/refresh"
}
},
["External"] = new JsonObject
{
["Enabled"] = false,
["BrowserSessionStatusKey"] = "__external_status",
["BrowserSessionMessageKey"] = "__external_message",
["SigninUrl"] = "/signin-{0}",
["SignInHtmlTemplate"] = "Talking To {0}Loading...{1}",
["RedirectUrl"] = null,
["ReturnToPath"] = "/",
["ReturnToPathQueryStringKey"] = "return_to",
["LoginCommand"] = "select * from external_login($1,$2,$3,$4,$5)",
["ClientAnalyticsData"] = "{timestamp:new Date().toISOString(),timezone:Intl.DateTimeFormat().resolvedOptions().timeZone,screen:{width:window.screen.width,height:window.screen.height,colorDepth:window.screen.colorDepth,pixelRatio:window.devicePixelRatio,orientation:screen.orientation.type},browser:{userAgent:navigator.userAgent,language:navigator.language,languages:navigator.languages,cookiesEnabled:navigator.cookieEnabled,doNotTrack:navigator.doNotTrack,onLine:navigator.onLine,platform:navigator.platform,vendor:navigator.vendor},memory:{deviceMemory:navigator.deviceMemory,hardwareConcurrency:navigator.hardwareConcurrency},window:{innerWidth:window.innerWidth,innerHeight:window.innerHeight,outerWidth:window.outerWidth,outerHeight:window.outerHeight},location:{href:window.location.href,hostname:window.location.hostname,pathname:window.location.pathname,protocol:window.location.protocol,referrer:document.referrer},performance:{navigation:{type:performance.navigation?.type,redirectCount:performance.navigation?.redirectCount},timing:performance.timing?{loadEventEnd:performance.timing.loadEventEnd,loadEventStart:performance.timing.loadEventStart,domComplete:performance.timing.domComplete,domInteractive:performance.timing.domInteractive,domContentLoadedEventEnd:performance.timing.domContentLoadedEventEnd}:null}}",
["ClientAnalyticsIpKey"] = "ip",
["Google"] = new JsonObject
{
["Enabled"] = false,
["ClientId"] = "",
["ClientSecret"] = "",
["AuthUrl"] = "https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id={0}&redirect_uri={1}&scope=openid profile email&state={2}",
["TokenUrl"] = "https://oauth2.googleapis.com/token",
["InfoUrl"] = "https://www.googleapis.com/oauth2/v3/userinfo",
["EmailUrl"] = null
},
["LinkedIn"] = new JsonObject
{
["Enabled"] = false,
["ClientId"] = "",
["ClientSecret"] = "",
["AuthUrl"] = "https://www.linkedin.com/oauth/v2/authorization?response_type=code&client_id={0}&redirect_uri={1}&state={2}&scope=r_liteprofile%20r_emailaddress",
["TokenUrl"] = "https://www.linkedin.com/oauth/v2/accessToken",
["InfoUrl"] = "https://api.linkedin.com/v2/me",
["EmailUrl"] = "https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements//(handle~))"
},
["GitHub"] = new JsonObject
{
["Enabled"] = false,
["ClientId"] = "",
["ClientSecret"] = "",
["AuthUrl"] = "https://github.com/login/oauth/authorize?client_id={0}&redirect_uri={1}&state={2}&allow_signup=false",
["TokenUrl"] = "https://github.com/login/oauth/access_token",
["InfoUrl"] = "https://api.github.com/user",
["EmailUrl"] = null
},
["Microsoft"] = new JsonObject
{
["Enabled"] = false,
["ClientId"] = "",
["ClientSecret"] = "",
["AuthUrl"] = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?response_type=code&client_id={0}&redirect_uri={1}&scope=openid%20profile%20email&state={2}",
["TokenUrl"] = "https://login.microsoftonline.com/common/oauth2/v2.0/token",
["InfoUrl"] = "https://graph.microsoft.com/oidc/userinfo",
["EmailUrl"] = null
},
["Facebook"] = new JsonObject
{
["Enabled"] = false,
["ClientId"] = "",
["ClientSecret"] = "",
["AuthUrl"] = "https://www.facebook.com/v20.0/dialog/oauth?response_type=code&client_id={0}&redirect_uri={1}&scope=public_profile%20email&state={2}",
["TokenUrl"] = "https://graph.facebook.com/v20.0/oauth/access_token",
["InfoUrl"] = "https://graph.facebook.com/me?fields=id,name,email",
["EmailUrl"] = null
}
},
["PasskeyAuth"] = new JsonObject
{
["Enabled"] = false,
["EnableRegister"] = false,
["RateLimiterPolicy"] = null,
["ConnectionName"] = null,
["CommandRetryStrategy"] = "default",
["RelyingPartyId"] = null,
["RelyingPartyName"] = null,
["RelyingPartyOrigins"] = new JsonArray(),
["AddPasskeyOptionsPath"] = "/api/passkey/add/options",
["AddPasskeyPath"] = "/api/passkey/add",
["RegistrationOptionsPath"] = "/api/passkey/register/options",
["RegistrationPath"] = "/api/passkey/register",
["LoginOptionsPath"] = "/api/passkey/login/options",
["LoginPath"] = "/api/passkey/login",
["ChallengeTimeoutMinutes"] = 5,
["UserVerificationRequirement"] = "required",
["ResidentKeyRequirement"] = "required",
["AttestationConveyance"] = "none",
["ChallengeAddExistingUserCommand"] = "select * from passkey_challenge_add_existing($1,$2)",
["ChallengeRegistrationCommand"] = "select * from passkey_challenge_registration($1)",
["ChallengeAuthenticationCommand"] = "select * from passkey_challenge_authentication($1,$2)",
["VerifyChallengeCommand"] = "select * from passkey_verify_challenge($1,$2)",
["ValidateSignCount"] = true,
["AuthenticateDataCommand"] = "select * from passkey_authenticate_data($1)",
["CompleteAddExistingUserCommand"] = "select * from passkey_complete_add_existing($1,$2,$3,$4,$5,$6,$7,$8)",
["CompleteRegistrationCommand"] = "select * from passkey_complete_registration($1,$2,$3,$4,$5,$6,$7,$8)",
["CompleteAuthenticateCommand"] = "select * from passkey_complete_authenticate($1,$2,$3,$4)",
["ClientAnalyticsIpKey"] = "ip",
["StatusColumnName"] = "status",
["MessageColumnName"] = "message",
["ChallengeColumnName"] = "challenge",
["ChallengeIdColumnName"] = "challenge_id",
["UserNameColumnName"] = "user_name",
["UserDisplayNameColumnName"] = "user_display_name",
["UserHandleColumnName"] = "user_handle",
["ExcludeCredentialsColumnName"] = "exclude_credentials",
["AllowCredentialsColumnName"] = "allow_credentials",
["PublicKeyColumnName"] = "public_key",
["PublicKeyAlgorithmColumnName"] = "public_key_algorithm",
["SignCountColumnName"] = "sign_count"
}
};
}
private static JsonObject GetLogDefaults()
{
return new JsonObject
{
["MinimalLevels"] = new JsonObject
{
["NpgsqlRest"] = "Information",
["NpgsqlRestClient"] = "Information",
["System"] = "Warning",
["Microsoft"] = "Warning"
},
["ToConsole"] = true,
["ConsoleMinimumLevel"] = "Verbose",
["ToFile"] = false,
["FilePath"] = "logs/log.txt",
["FileSizeLimitBytes"] = 30000000,
["FileMinimumLevel"] = "Verbose",
["RetainedFileCountLimit"] = 30,
["RollOnFileSizeLimit"] = true,
["ToPostgres"] = false,
["PostgresCommand"] = "call log($1,$2,$3,$4,$5)",
["PostgresMinimumLevel"] = "Verbose",
["ToOpenTelemetry"] = false,
["OTLPEndpoint"] = "http://localhost:4317",
["OTLPProtocol"] = "Grpc",
["OTLResourceAttributes"] = new JsonObject
{
["service.name"] = "{application}",
["service.version"] = "1.0",
["service.environment"] = "{environment}"
},
["OTLPHeaders"] = new JsonObject(),
["OTLPMinimumLevel"] = "Verbose",
["OutputTemplate"] = "[{Timestamp:HH:mm:ss.fff} {Level:u3}] {Message:lj} [{SourceContext}]{NewLine}{Exception}"
};
}
private static JsonObject GetResponseCompressionDefaults()
{
return new JsonObject
{
["Enabled"] = false,
["EnableForHttps"] = false,
["UseBrotli"] = true,
["UseGzipFallback"] = true,
["CompressionLevel"] = "Optimal",
["IncludeMimeTypes"] = CreateStringArray(
"text/plain", "text/css", "application/javascript", "text/javascript", "text/html",
"application/xml", "text/xml", "application/json", "text/json",
"image/svg+xml", "font/woff", "font/woff2", "application/font-woff", "application/font-woff2"
),
["ExcludeMimeTypes"] = new JsonArray()
};
}
private static JsonObject GetAntiforgeryDefaults()
{
return new JsonObject
{
["Enabled"] = false,
["CookieName"] = null,
["FormFieldName"] = "__RequestVerificationToken",
["HeaderName"] = "RequestVerificationToken",
["SuppressReadingTokenFromFormBody"] = false,
["SuppressXFrameOptionsHeader"] = false
};
}
private static JsonObject GetStaticFilesDefaults()
{
return new JsonObject
{
["Enabled"] = false,
["RootPath"] = "wwwroot",
["AuthorizePaths"] = new JsonArray(),
["UnauthorizedRedirectPath"] = "/",
["UnauthorizedReturnToQueryParameter"] = "return_to",
["ParseContentOptions"] = new JsonObject
{
["Enabled"] = false,
["AvailableClaims"] = new JsonArray(),
["AvailableEnvVars"] = new JsonArray(),
["CacheParsedFile"] = true,
["Headers"] = CreateStringArray("Cache-Control: no-store, no-cache, must-revalidate", "Pragma: no-cache", "Expires: 0"),
["FilePaths"] = CreateStringArray("*.html"),
["AntiforgeryFieldName"] = "antiForgeryFieldName",
["AntiforgeryToken"] = "antiForgeryToken"
}
};
}
private static JsonObject GetCorsDefaults()
{
return new JsonObject
{
["Enabled"] = false,
["AllowedOrigins"] = new JsonArray(),
["AllowedMethods"] = CreateStringArray("*"),
["AllowedHeaders"] = CreateStringArray("*"),
["AllowCredentials"] = false,
["PreflightMaxAgeSeconds"] = 600
};
}
private static JsonObject GetSecurityHeadersDefaults()
{
return new JsonObject
{
["Enabled"] = false,
["XContentTypeOptions"] = "nosniff",
["XFrameOptions"] = "DENY",
["ReferrerPolicy"] = "strict-origin-when-cross-origin",
["ContentSecurityPolicy"] = null,
["PermissionsPolicy"] = null,
["CrossOriginOpenerPolicy"] = null,
["CrossOriginEmbedderPolicy"] = null,
["CrossOriginResourcePolicy"] = null
};
}
private static JsonObject GetForwardedHeadersDefaults()
{
return new JsonObject
{
["Enabled"] = false,
["ForwardLimit"] = 1,
["KnownProxies"] = new JsonArray(),
["KnownNetworks"] = new JsonArray(),
["AllowedHosts"] = new JsonArray()
};
}
private static JsonObject GetHealthChecksDefaults()
{
return new JsonObject
{
["Enabled"] = false,
["CacheDuration"] = "5 seconds",
["Path"] = "/health",
["ReadyPath"] = "/health/ready",
["LivePath"] = "/health/live",
["IncludeDatabaseCheck"] = true,
["DatabaseCheckName"] = "postgresql",
["RequireAuthorization"] = false,
["RateLimiterPolicy"] = null
};
}
private static JsonObject GetStatsDefaults()
{
return new JsonObject
{
["Enabled"] = false,
["CacheDuration"] = "5 seconds",
["RateLimiterPolicy"] = null,
["ConnectionName"] = null,
["RequireAuthorization"] = false,
["AuthorizedRoles"] = new JsonArray(),
["OutputFormat"] = "html",
["SchemaSimilarTo"] = null,
["RoutinesStatsPath"] = "/stats/routines",
["TablesStatsPath"] = "/stats/tables",
["IndexesStatsPath"] = "/stats/indexes",
["ActivityPath"] = "/stats/activity"
};
}
private static JsonObject GetCommandRetryOptionsDefaults()
{
return new JsonObject
{
["Enabled"] = true,
["DefaultStrategy"] = "default",
["Strategies"] = new JsonObject
{
["default"] = new JsonObject
{
["RetrySequenceSeconds"] = CreateIntArray(0, 1, 2, 5, 10),
["ErrorCodes"] = CreateStringArray(
"40001", "40P01", "08000", "08003", "08006", "08001", "08004", "08007", "08P01",
"53000", "53100", "53200", "53300", "53400", "57P01", "57P02", "57P03",
"58000", "58030", "55P03", "55006", "55000"
)
}
}
};
}
private static JsonObject GetCacheOptionsDefaults()
{
return new JsonObject
{
["Enabled"] = false,
["Type"] = "Memory",
["MemoryCachePruneIntervalSeconds"] = 60,
["RedisConfiguration"] = "localhost:6379,abortConnect=false,ssl=false,connectTimeout=10000,syncTimeout=5000,connectRetry=3",
["MaxCacheableRows"] = 1000,
["UseHashedCacheKeys"] = false,
["HashKeyThreshold"] = 256,
["InvalidateCacheSuffix"] = null,
["HybridCacheUseRedisBackend"] = false,
["HybridCacheMaximumKeyLength"] = 1024,
["HybridCacheMaximumPayloadBytes"] = 1048576,
["HybridCacheDefaultExpiration"] = null,
["HybridCacheLocalCacheExpiration"] = null,
["Profiles"] = new JsonObject
{
["fast_memory"] = new JsonObject
{
["Enabled"] = false,
["Type"] = "Memory",
["Expiration"] = "30 seconds",
["Parameters"] = new JsonArray("user_id")
},
["shared_redis"] = new JsonObject
{
["Enabled"] = false,
["Type"] = "Redis",
["Expiration"] = "1 hour"
},
["date_range_hybrid"] = new JsonObject
{
["Enabled"] = false,
["Type"] = "Hybrid",
["Expiration"] = "5 minutes",
["Parameters"] = new JsonArray("from", "to"),
["When"] = new JsonArray(
new JsonObject
{
["Parameter"] = "to",
["Value"] = null,
["Then"] = "skip"
}
)
}
}
};
}
private static JsonObject GetValidationOptionsDefaults()
{
return new JsonObject
{
["Enabled"] = true,
["Rules"] = new JsonObject
{
["not_null"] = new JsonObject
{
["Type"] = "NotNull",
["Pattern"] = null,
["MinLength"] = null,
["MaxLength"] = null,
["Message"] = "Parameter '{0}' cannot be null",
["StatusCode"] = 400
},
["not_empty"] = new JsonObject
{
["Type"] = "NotEmpty",
["Pattern"] = null,
["MinLength"] = null,
["MaxLength"] = null,
["Message"] = "Parameter '{0}' cannot be empty",
["StatusCode"] = 400
},
["required"] = new JsonObject
{
["Type"] = "Required",
["Pattern"] = null,
["MinLength"] = null,
["MaxLength"] = null,
["Message"] = "Parameter '{0}' is required",
["StatusCode"] = 400
},
["email"] = new JsonObject
{
["Type"] = "Regex",
["Pattern"] = "^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$",
["MinLength"] = null,
["MaxLength"] = null,
["Message"] = "Parameter '{0}' must be a valid email address",
["StatusCode"] = 400
}
}
};
}
private static JsonObject GetRateLimiterOptionsDefaults()
{
var policies = new JsonObject
{
["fixed"] = new JsonObject
{
["Type"] = "FixedWindow",
["Enabled"] = false,
["PermitLimit"] = 100,
["WindowSeconds"] = 60,
["QueueLimit"] = 10,
["AutoReplenishment"] = true
},
["sliding"] = new JsonObject
{
["Type"] = "SlidingWindow",
["Enabled"] = false,
["PermitLimit"] = 100,
["WindowSeconds"] = 60,
["SegmentsPerWindow"] = 6,
["QueueLimit"] = 10,
["AutoReplenishment"] = true
},
["bucket"] = new JsonObject
{
["Type"] = "TokenBucket",
["Enabled"] = false,
["TokenLimit"] = 100,
["TokensPerPeriod"] = 10,
["ReplenishmentPeriodSeconds"] = 10,
["QueueLimit"] = 10,
["AutoReplenishment"] = true
},
["concurrency"] = new JsonObject
{
["Type"] = "Concurrency",
["Enabled"] = false,
["PermitLimit"] = 10,
["QueueLimit"] = 5,
["OldestFirst"] = true
},
["per_user"] = new JsonObject
{
["Type"] = "FixedWindow",
["Enabled"] = false,
["PermitLimit"] = 100,
["WindowSeconds"] = 60,
["QueueLimit"] = 10,
["AutoReplenishment"] = true,
["Partition"] = new JsonObject
{
["Sources"] = CreatePartitionSourcesArray(),
["BypassAuthenticated"] = false
}
},
["login_throttle"] = new JsonObject
{
["Type"] = "FixedWindow",
["Enabled"] = false,
["PermitLimit"] = 10,
["WindowSeconds"] = 60,
["QueueLimit"] = 0,
["AutoReplenishment"] = true,
["StatusMessage"] = "Too many login attempts. Please wait a minute and try again.",
["Partition"] = new JsonObject
{
["Sources"] = CreateIpPartitionSourcesArray(),
["BypassAuthenticated"] = false
}
}
};
return new JsonObject
{
["Enabled"] = false,
["StatusCode"] = 429,
["StatusMessage"] = "Too many requests. Please try again later.",
["DefaultPolicy"] = null,
["Policies"] = policies
};
}
private static JsonObject GetErrorHandlingOptionsDefaults()
{
var errorCodePolicies = new JsonArray();
errorCodePolicies.Add((JsonNode)new JsonObject
{
["Name"] = "Default",
["ErrorCodes"] = new JsonObject
{
["42501"] = new JsonObject { ["StatusCode"] = 403, ["Title"] = "Insufficient Privilege", ["Details"] = null, ["Type"] = null },
["57014"] = new JsonObject { ["StatusCode"] = 205, ["Title"] = "Cancelled", ["Details"] = null, ["Type"] = null },
["P0001"] = new JsonObject { ["StatusCode"] = 400, ["Title"] = null, ["Details"] = null, ["Type"] = null },
["P0004"] = new JsonObject { ["StatusCode"] = 400, ["Title"] = null, ["Details"] = null, ["Type"] = null }
}
});
return new JsonObject
{
["RemoveTypeUrl"] = false,
["RemoveTraceId"] = true,
["DefaultErrorCodePolicy"] = "Default",
["TimeoutErrorMapping"] = new JsonObject
{
["StatusCode"] = 504,
["Title"] = "Command execution timed out",
["Details"] = null,
["Type"] = null
},
["ErrorCodePolicies"] = errorCodePolicies
};
}
private static JsonObject GetNpgsqlRestDefaults()
{
return new JsonObject
{
["ConnectionName"] = null,
["UseMultipleConnections"] = false,
["CommandTimeout"] = null,
["SchemaSimilarTo"] = null,
["SchemaNotSimilarTo"] = null,
["IncludeSchemas"] = null,
["ExcludeSchemas"] = null,
["NameSimilarTo"] = null,
["NameNotSimilarTo"] = null,
["IncludeNames"] = null,
["ExcludeNames"] = null,
["CommentsMode"] = "OnlyAnnotated",
["UrlPathPrefix"] = "/api",
["KebabCaseUrls"] = true,
["CamelCaseNames"] = true,
["RequiresAuthorization"] = true,
["LogConnectionNoticeEvents"] = true,
["LogConnectionNoticeEventsMode"] = "FirstStackFrameAndMessage",
["LogCommands"] = false,
["LogCommandParameters"] = false,
["DebugLogEndpointCreateEvents"] = true,
["DebugLogCommentAnnotationEvents"] = true,
["DefaultHttpMethod"] = null,
["DefaultRequestParamType"] = null,
["QueryStringNullHandling"] = "Ignore",
["TextResponseNullHandling"] = "EmptyString",
["RequestHeadersMode"] = "Parameter",
["RequestHeadersContextKey"] = "request.headers",
["RequestHeadersParameterName"] = "_headers",
["WrapInTransaction"] = false,
["JsonTimestampsAreUtc"] = true,
["BeforeRoutineCommands"] = new JsonArray(
new JsonObject
{
["Enabled"] = false,
["Sql"] = "select set_config('search_path', $1, true)",
["Parameters"] = new JsonArray(
new JsonObject
{
["Source"] = "Claim",
["Name"] = "tenant_id"
}
)
}
),
["InstanceIdRequestHeaderName"] = null,
["CustomRequestHeaders"] = new JsonObject(),
["AvailableEnvVars"] = new JsonArray(),
["ExecutionIdHeaderName"] = "X-NpgsqlRest-ID",
["DefaultServerSentEventsEventNoticeLevel"] = "INFO",
["ServerSentEventsResponseHeaders"] = new JsonObject(),
["WarnUnboundServerSentEventsNotices"] = true,
["RoutineOptions"] = new JsonObject
{
["Enabled"] = true,
["CustomTypeParameterSeparator"] = null,
["IncludeLanguages"] = null,
["ExcludeLanguages"] = null,
["NestedJsonForCompositeTypes"] = false,
["ResolveNestedCompositeTypes"] = true
},
["UploadOptions"] = GetUploadOptionsDefaults(),
["TableFormatOptions"] = GetTableFormatOptionsDefaults(),
["AuthenticationOptions"] = GetAuthenticationOptionsDefaults(),
["HttpFileOptions"] = GetHttpFileOptionsDefaults(),
["OpenApiOptions"] = GetOpenApiOptionsDefaults(),
["McpOptions"] = GetMcpOptionsDefaults(),
["ClientCodeGen"] = GetClientCodeGenDefaults(),
["HttpClientOptions"] = GetHttpClientOptionsDefaults(),
["ProxyOptions"] = GetProxyOptionsDefaults(),
["SqlFileSource"] = GetSqlFileSourceDefaults()
};
}
private static JsonObject GetUploadOptionsDefaults()
{
return new JsonObject
{
["Enabled"] = false,
["LogUploadEvent"] = true,
["LogUploadParameters"] = false,
["DefaultUploadHandler"] = "large_object",
["UseDefaultUploadMetadataParameter"] = false,
["DefaultUploadMetadataParameterName"] = "_upload_metadata",
["UseDefaultUploadMetadataContextKey"] = false,
["DefaultUploadMetadataContextKey"] = "request.upload_metadata",
["UploadHandlers"] = new JsonObject
{
["StopAfterFirstSuccess"] = false,
["IncludedMimeTypePatterns"] = null,
["ExcludedMimeTypePatterns"] = null,
["BufferSize"] = 8192,
["TextTestBufferSize"] = 4096,
["TextNonPrintableThreshold"] = 5,
["AllowedImageTypes"] = "jpeg, png, gif, bmp, tiff, webp",
["RowCommandUserClaimsKey"] = "claims",
["LargeObjectEnabled"] = true,
["LargeObjectKey"] = "large_object",
["LargeObjectCheckText"] = false,
["LargeObjectCheckImage"] = false,
["FileSystemEnabled"] = true,
["FileSystemKey"] = "file_system",
["FileSystemPath"] = "/tmp/uploads",
["FileSystemUseUniqueFileName"] = true,
["FileSystemCreatePathIfNotExists"] = true,
["FileSystemCheckText"] = false,
["FileSystemCheckImage"] = false,
["CsvUploadEnabled"] = true,
["CsvUploadKey"] = "csv",
["CsvUploadCheckFileStatus"] = true,
["CsvUploadDelimiterChars"] = ",",
["CsvUploadHasFieldsEnclosedInQuotes"] = true,
["CsvUploadSetWhiteSpaceToNull"] = true,
["CsvUploadRowCommand"] = "call process_csv_row($1,$2,$3,$4)",
["ExcelUploadEnabled"] = true,
["ExcelKey"] = "excel",
["ExcelSheetName"] = null,
["ExcelAllSheets"] = false,
["ExcelTimeFormat"] = "HH:mm:ss",
["ExcelDateFormat"] = "yyyy-MM-dd",
["ExcelDateTimeFormat"] = "yyyy-MM-dd HH:mm:ss",
["ExcelRowDataAsJson"] = false,
["ExcelUploadRowCommand"] = "call process_excel_row($1,$2,$3,$4)"
}
};
}
private static JsonObject GetAuthenticationOptionsDefaults()
{
return new JsonObject
{
["DefaultAuthenticationType"] = null,
["StatusColumnName"] = "status",
["SchemeColumnName"] = "scheme",
["BodyColumnName"] = "body",
["ResponseTypeColumnName"] = "application/json",
["HashColumnName"] = "hash",
["PasswordParameterNameContains"] = "pass",
["DefaultUserIdClaimType"] = "user_id",
["DefaultNameClaimType"] = "user_name",
["DefaultRoleClaimType"] = "user_roles",
["DefaultDisplayNameClaimType"] = "display_name",
["SerializeAuthEndpointsResponse"] = false,
["ObfuscateAuthParameterLogValues"] = true,
["PasswordVerificationFailedCommand"] = null,
["PasswordVerificationSucceededCommand"] = null,
["UseUserContext"] = false,
["ContextKeyClaimsMapping"] = new JsonObject
{
["request.user_id"] = "user_id",
["request.user_name"] = "user_name",
["request.user_roles"] = "user_roles"
},
["ClaimsJsonContextKey"] = null,
["IpAddressContextKey"] = "request.ip_address",
["UseUserParameters"] = false,
["ParameterNameClaimsMapping"] = new JsonObject
{
["_user_id"] = "user_id",
["_user_name"] = "user_name",
["_user_roles"] = "user_roles"
},
["ClaimsJsonParameterName"] = "_user_claims",
["IpAddressParameterName"] = "_ip_address",
["LoginPath"] = null,
["LogoutPath"] = null,
["BasicAuth"] = new JsonObject
{
["Enabled"] = false,
["Realm"] = null,
["Users"] = new JsonObject(),
["SslRequirement"] = "Required",
["UseDefaultPasswordHasher"] = true,
["ChallengeCommand"] = null
}
};
}
private static JsonObject GetHttpFileOptionsDefaults()
{
return new JsonObject
{
["Enabled"] = false,
["Option"] = "File",
["Name"] = null,
["NamePattern"] = "{0}_{1}",
["CommentHeader"] = "Simple",
["CommentHeaderIncludeComments"] = true,
["FileMode"] = "Schema",
["FileOverwrite"] = true
};
}
private static JsonObject GetOpenApiOptionsDefaults()
{
return new JsonObject
{
["Enabled"] = false,
["FileName"] = "npgsqlrest_openapi.json",
["UrlPath"] = "/openapi.json",
["FileOverwrite"] = true,
["DocumentTitle"] = null,
["DocumentVersion"] = "1.0.0",
["DocumentDescription"] = null,
["AddCurrentServer"] = true,
["Servers"] = new JsonArray(),
["SecuritySchemes"] = new JsonArray(),
["IncludeSchemas"] = new JsonArray(),
["ExcludeSchemas"] = new JsonArray(),
["NameSimilarTo"] = null,
["NameNotSimilarTo"] = null,
["RequiresAuthorizationOnly"] = false
};
}
private static JsonObject GetMcpOptionsDefaults()
{
return new JsonObject
{
["Enabled"] = false,
["UrlPath"] = "/mcp",
["ServerName"] = null,
["ServerVersion"] = "1.0.0",
["Instructions"] = null,
["ToolDescriptionSuffix"] = null,
["RateLimiterPolicy"] = null,
["AllowedOrigins"] = new JsonArray(),
["Authorization"] = new JsonObject
{
["RequireAuthorization"] = false,
["AuthorizationServers"] = new JsonArray(),
["ScopesSupported"] = new JsonArray(),
["Audience"] = null,
["ProtectedResourceMetadataPath"] = null,
["FilterToolsByRole"] = false
}
};
}
private static JsonObject GetClientCodeGenDefaults()
{
return new JsonObject
{
["Enabled"] = false,
["FilePath"] = null,
["FileOverwrite"] = true,
["IncludeHost"] = true,
["CustomHost"] = null,
["CommentHeader"] = "Simple",
["CommentHeaderIncludeComments"] = true,
["BySchema"] = true,
["IncludeStatusCode"] = true,
["CreateSeparateTypeFile"] = true,
["ExportTypes"] = false,
["ImportBaseUrlFrom"] = null,
["ImportParseQueryFrom"] = null,
["IncludeParseUrlParam"] = false,
["IncludeParseRequestParam"] = false,
["HeaderLines"] = CreateStringArray("// autogenerated at {0}", ""),
["SkipRoutineNames"] = new JsonArray(),
["SkipFunctionNames"] = new JsonArray(),
["SkipPaths"] = new JsonArray(),
["SkipSchemas"] = new JsonArray(),
["DefaultJsonType"] = "any",
["UseRoutineNameInsteadOfEndpoint"] = false,
["ExportUrls"] = false,
["SkipTypes"] = false,
["UniqueModels"] = false,
["XsrfTokenHeaderName"] = null,
["ExportEventSources"] = true,
["CustomImports"] = new JsonArray(),
["CustomHeaders"] = new JsonObject(),
["IncludeSchemaInNames"] = true,
["ErrorExpression"] = "await response.json()",
["ErrorType"] = "{status: number; title: string; detail?: string | null} | undefined"
};
}
private static JsonObject GetHttpClientOptionsDefaults()
{
return new JsonObject
{
["Enabled"] = false,
["ResponseStatusCodeField"] = "status_code",
["ResponseBodyField"] = "body",
["ResponseHeadersField"] = "headers",
["ResponseContentTypeField"] = "content_type",
["ResponseSuccessField"] = "success",
["ResponseErrorMessageField"] = "error_message"
};
}
private static JsonObject GetProxyOptionsDefaults()
{
return new JsonObject
{
["Enabled"] = false,
["Host"] = null,
["DefaultTimeout"] = "00:00:30",
["ForwardHeaders"] = true,
["ExcludeHeaders"] = CreateStringArray("Host", "Content-Length", "Transfer-Encoding"),
["ForwardResponseHeaders"] = true,
["ExcludeResponseHeaders"] = CreateStringArray("Transfer-Encoding", "Content-Length"),
["ResponseStatusCodeParameter"] = "_proxy_status_code",
["ResponseBodyParameter"] = "_proxy_body",
["ResponseHeadersParameter"] = "_proxy_headers",
["ResponseContentTypeParameter"] = "_proxy_content_type",
["ResponseSuccessParameter"] = "_proxy_success",
["ResponseErrorMessageParameter"] = "_proxy_error_message",
["ForwardUploadContent"] = false
};
}
private static JsonObject GetSqlFileSourceDefaults()
{
return new JsonObject
{
["Enabled"] = false,
["FilePattern"] = "",
["CommentsMode"] = "OnlyAnnotated",
["CommentScope"] = "All",
["ErrorMode"] = "Exit",
["ResultPrefix"] = "result",
["UnnamedSingleColumnSet"] = true,
["NestedJsonForCompositeTypes"] = false,
["SkipNonQueryCommands"] = true,
["LogCommandText"] = false
};
}
private static JsonObject GetTableFormatOptionsDefaults()
{
return new JsonObject
{
["Enabled"] = false,
["HtmlEnabled"] = true,
["HtmlKey"] = "html",
["HtmlHeader"] =
"",
["HtmlFooter"] = null,
["ExcelEnabled"] = true,
["ExcelKey"] = "excel",
["ExcelSheetName"] = null,
["ExcelDateTimeFormat"] = null,
["ExcelNumericFormat"] = null
};
}
///
/// Deep merges the actual configuration values over the defaults.
/// Values from actual config override defaults at the leaf level.
///
public static JsonNode? MergeWithDefaults(JsonNode? defaults, JsonNode? actual)
{
if (actual is null)
{
// For container types (objects/arrays), fall back to defaults
// For primitive defaults, null means "explicitly null" - preserve it
if (defaults is JsonObject || defaults is JsonArray)
{
return defaults.DeepClone();
}
return null;
}
if (defaults is null)
{
return actual.DeepClone();
}
if (defaults is JsonObject defaultObj && actual is JsonObject actualObj)
{
var result = new JsonObject();
// Add all keys from defaults, using case-insensitive matching against actual
foreach (var kvp in defaultObj)
{
if (ContainsKeyIgnoreCase(actualObj, kvp.Key, out var matchedKey))
{
// Recursively merge (use default's key casing)
result[kvp.Key] = MergeWithDefaults(kvp.Value, actualObj[matchedKey!]);
}
else
{
// Use default
result[kvp.Key] = kvp.Value?.DeepClone();
}
}
// Add any keys from actual that aren't in defaults (case-insensitive)
foreach (var kvp in actualObj)
{
if (!ContainsKeyIgnoreCase(defaultObj, kvp.Key, out _))
{
result[kvp.Key] = kvp.Value?.DeepClone();
}
}
return result;
}
if (defaults is JsonArray defaultArr)
{
if (actual is JsonArray actualArray)
{
// Merge array elements using corresponding default elements as schema
return MergeArrayElements(defaultArr, actualArray);
}
// Config system converts empty arrays to "" or null - keep the default (empty array)
return defaults.DeepClone();
}
// For primitive values, coerce actual to match default's type
return CoerceToDefaultType(defaults, actual);
}
private static JsonArray MergeArrayElements(JsonArray defaultArr, JsonArray actualArr)
{
if (actualArr.Count == 0)
{
return new JsonArray();
}
var result = new JsonArray();
for (int i = 0; i < actualArr.Count; i++)
{
var actualItem = actualArr[i];
// Use the corresponding default element as schema, or the first if fewer defaults
JsonNode? defaultItem = i < defaultArr.Count ? defaultArr[i] : (defaultArr.Count > 0 ? defaultArr[0] : null);
if (actualItem is JsonObject actualItemObj && defaultItem is JsonObject)
{
// Merge object element using default schema (preserves key ordering)
result.Add(MergeWithDefaults(defaultItem, actualItemObj));
}
else if (defaultItem is not null)
{
// Primitive array element - coerce to match default's type
result.Add(CoerceToDefaultType(defaultItem, actualItem));
}
else
{
result.Add(actualItem?.DeepClone());
}
}
return result;
}
private static JsonNode? CoerceToDefaultType(JsonNode? defaults, JsonNode? actual)
{
if (actual is null)
{
return actual;
}
// If default is a string value but actual is numeric, convert actual to string
if (defaults is JsonValue defaultJv && actual is JsonValue actualJv)
{
if (defaultJv.TryGetValue(out _))
{
// Default says string - ensure actual is also string
if (actualJv.TryGetValue(out _))
{
return actual.DeepClone();
}
// Actual is numeric but default is string - convert to string
if (actualJv.TryGetValue(out var decVal))
{
return JsonValue.Create(decVal.ToString());
}
if (actualJv.TryGetValue(out var longVal))
{
return JsonValue.Create(longVal.ToString());
}
}
}
return actual.DeepClone();
}
///
/// Recursively validates actual config keys against the defaults schema.
/// Returns a list of full key paths for unrecognized keys.
///
public static List FindUnknownConfigKeys(JsonNode? defaults, JsonNode? actual, string path = "")
{
var warnings = new List();
if (actual is null || defaults is null)
{
return warnings;
}
// If defaults is a JsonArray, IConfiguration serializes arrays as objects with numeric keys.
// Skip index-key validation but validate properties within each element.
if (defaults is JsonArray defaultArray && actual is JsonObject actualArrayObj)
{
JsonObject? elementSchema = defaultArray.Count > 0 ? defaultArray[0] as JsonObject : null;
if (elementSchema is not null)
{
foreach (var kvp in actualArrayObj)
{
if (kvp.Value is JsonObject elementObj)
{
var elementPath = string.IsNullOrEmpty(path) ? kvp.Key : $"{path}:{kvp.Key}";
warnings.AddRange(FindUnknownConfigKeys(elementSchema, elementObj, elementPath));
}
}
}
return warnings;
}
if (defaults is JsonObject defaultObj && actual is JsonObject actualObj)
{
foreach (var kvp in actualObj)
{
var fullPath = string.IsNullOrEmpty(path) ? kvp.Key : $"{path}:{kvp.Key}";
// Name-keyed open dictionaries: the named children are arbitrary (user-chosen) but each
// child has a well-defined inner shape. Pick that shape and validate against it. Without
// these intercepts, the validator falls back to looking up the user-chosen name in
// defaults and either flags it as unknown (if the name has no example match) or matches
// it against an example's partial key set (if the name *does* collide with an example,
// which falsely rejects valid per-type override keys).
//
// Each intercept short-circuits the rest of the loop body so neither the example-lookup
// path nor the IsOpenDictionarySection fallback fires.
if (string.Equals(path, "Auth:Schemes", StringComparison.Ordinal) && kvp.Value is JsonObject schemeObj)
{
var schemeSchema = GetAuthSchemeSchemaByType(schemeObj);
if (schemeSchema is not null)
{
warnings.AddRange(FindUnknownConfigKeys(schemeSchema, schemeObj, fullPath));
}
// No/invalid Type: skip validation. RegisterAuthSchemes throws a clear error at
// startup for missing/invalid Type; no point double-reporting it here.
continue;
}
if (string.Equals(path, "RateLimiterOptions:Policies", StringComparison.Ordinal) && kvp.Value is JsonObject policyObj)
{
var policySchema = GetRateLimiterPolicySchemaByType(policyObj);
if (policySchema is not null)
{
warnings.AddRange(FindUnknownConfigKeys(policySchema, policyObj, fullPath));
}
// BuildRateLimiter silently skips policies with no/invalid Type — match that here.
continue;
}
if (string.Equals(path, "CacheOptions:Profiles", StringComparison.Ordinal) && kvp.Value is JsonObject profileObj)
{
warnings.AddRange(FindUnknownConfigKeys(GetCacheProfileSchema(), profileObj, fullPath));
continue;
}
if (string.Equals(path, "ValidationOptions:Rules", StringComparison.Ordinal) && kvp.Value is JsonObject ruleObj)
{
warnings.AddRange(FindUnknownConfigKeys(GetValidationRuleSchema(), ruleObj, fullPath));
continue;
}
if (ContainsKeyIgnoreCase(defaultObj, kvp.Key, out var matchedKey))
{
var defaultChild = defaultObj[matchedKey!];
// Empty JsonObject in defaults means open-ended/dictionary section - skip subtree
if (defaultChild is JsonObject emptyObj && emptyObj.Count == 0)
{
continue;
}
warnings.AddRange(FindUnknownConfigKeys(defaultChild, kvp.Value, fullPath));
}
else
{
// Key not in defaults - check if parent is an open dictionary section
if (IsOpenDictionarySection(path))
{
continue;
}
warnings.Add(fullPath);
}
}
}
return warnings;
}
///
/// Returns the per-type schema for a named entry under Auth:Schemes, keyed off the scheme's
/// Type field. Returns null when Type is missing/invalid so the caller can skip validation
/// (RegisterAuthSchemes will throw a clearer error at startup).
///
private static JsonObject? GetAuthSchemeSchemaByType(JsonObject scheme)
{
string? type = null;
foreach (var kvp in scheme)
{
if (string.Equals(kvp.Key, "Type", StringComparison.OrdinalIgnoreCase))
{
if (kvp.Value is JsonValue val && val.TryGetValue(out var s))
{
type = s;
}
break;
}
}
if (string.IsNullOrWhiteSpace(type))
{
return null;
}
if (string.Equals(type, "Cookies", StringComparison.OrdinalIgnoreCase))
{
return new JsonObject
{
["Type"] = "Cookies",
["Enabled"] = true,
["CookieValid"] = "14 days",
["CookieName"] = null,
["CookiePath"] = null,
["CookieDomain"] = null,
["CookieMultiSessions"] = true,
["CookieHttpOnly"] = true,
["CookieSameSite"] = null,
["CookieSecure"] = null
};
}
if (string.Equals(type, "BearerToken", StringComparison.OrdinalIgnoreCase))
{
return new JsonObject
{
["Type"] = "BearerToken",
["Enabled"] = true,
["BearerTokenExpire"] = "1 hour",
["BearerTokenRefreshPath"] = null
};
}
if (string.Equals(type, "Jwt", StringComparison.OrdinalIgnoreCase))
{
return new JsonObject
{
["Type"] = "Jwt",
["Enabled"] = true,
["JwtSecret"] = null,
["JwtIssuer"] = null,
["JwtAudience"] = null,
["JwtExpire"] = "60 minutes",
["JwtRefreshExpire"] = "7 days",
["JwtClockSkew"] = "5 minutes",
["JwtValidateIssuer"] = false,
["JwtValidateAudience"] = false,
["JwtValidateLifetime"] = true,
["JwtValidateIssuerSigningKey"] = true,
["JwtRefreshPath"] = null
};
}
return null;
}
private static string? ReadStringField(JsonObject obj, string key)
{
foreach (var kvp in obj)
{
if (string.Equals(kvp.Key, key, StringComparison.OrdinalIgnoreCase))
{
if (kvp.Value is JsonValue val && val.TryGetValue(out var s))
{
return s;
}
return null;
}
}
return null;
}
private static JsonObject PartitionSchema() => new()
{
["Sources"] = new JsonArray(new JsonObject
{
["Type"] = null,
["Name"] = null,
["Value"] = null
}),
["BypassAuthenticated"] = false
};
///
/// Returns the per-type schema for a named entry under RateLimiterOptions:Policies, keyed
/// off the policy's Type field (FixedWindow / SlidingWindow / TokenBucket / Concurrency).
/// Returns null when Type is missing/invalid — BuildRateLimiter silently skips such policies, so
/// the validator does the same to avoid double-reporting.
///
private static JsonObject? GetRateLimiterPolicySchemaByType(JsonObject policy)
{
var type = ReadStringField(policy, "Type");
if (string.IsNullOrWhiteSpace(type))
{
return null;
}
if (string.Equals(type, "FixedWindow", StringComparison.OrdinalIgnoreCase))
{
return new JsonObject
{
["Type"] = "FixedWindow",
["Enabled"] = true,
["PermitLimit"] = 100,
["WindowSeconds"] = 60,
["QueueLimit"] = 10,
["AutoReplenishment"] = true,
["StatusCode"] = null,
["StatusMessage"] = null,
["Partition"] = PartitionSchema()
};
}
if (string.Equals(type, "SlidingWindow", StringComparison.OrdinalIgnoreCase))
{
return new JsonObject
{
["Type"] = "SlidingWindow",
["Enabled"] = true,
["PermitLimit"] = 100,
["WindowSeconds"] = 60,
["SegmentsPerWindow"] = 6,
["QueueLimit"] = 10,
["AutoReplenishment"] = true,
["StatusCode"] = null,
["StatusMessage"] = null,
["Partition"] = PartitionSchema()
};
}
if (string.Equals(type, "TokenBucket", StringComparison.OrdinalIgnoreCase))
{
return new JsonObject
{
["Type"] = "TokenBucket",
["Enabled"] = true,
["TokenLimit"] = 100,
["TokensPerPeriod"] = 10,
["ReplenishmentPeriodSeconds"] = 10,
["QueueLimit"] = 10,
["AutoReplenishment"] = true,
["StatusCode"] = null,
["StatusMessage"] = null,
["Partition"] = PartitionSchema()
};
}
if (string.Equals(type, "Concurrency", StringComparison.OrdinalIgnoreCase))
{
return new JsonObject
{
["Type"] = "Concurrency",
["Enabled"] = true,
["PermitLimit"] = 10,
["QueueLimit"] = 5,
["OldestFirst"] = true,
["StatusCode"] = null,
["StatusMessage"] = null,
["Partition"] = PartitionSchema()
};
}
return null;
}
///
/// Returns the schema for any entry under CacheOptions:Profiles. All profiles share the same
/// key set regardless of Type (Memory / Redis / Hybrid); only the backend selection varies.
///
private static JsonObject GetCacheProfileSchema() => new()
{
["Enabled"] = false,
["Type"] = null,
["Expiration"] = null,
["Parameters"] = new JsonArray(),
["When"] = new JsonArray(new JsonObject
{
["Parameter"] = null,
["Value"] = null,
["Then"] = null
})
};
///
/// Returns the schema for any entry under ValidationOptions:Rules. All rules share the same
/// key set regardless of Type (NotNull / NotEmpty / Required / Regex / MinLength / MaxLength).
///
private static JsonObject GetValidationRuleSchema() => new()
{
["Type"] = null,
["Pattern"] = null,
["MinLength"] = null,
["MaxLength"] = null,
["Message"] = null,
["StatusCode"] = 400
};
private static bool ContainsKeyIgnoreCase(JsonObject obj, string key, out string? matchedKey)
{
foreach (var kvp in obj)
{
if (string.Equals(kvp.Key, key, StringComparison.OrdinalIgnoreCase))
{
matchedKey = kvp.Key;
return true;
}
}
matchedKey = null;
return false;
}
///
/// Sections where users add arbitrary keys beyond the defaults.
///
private static bool IsOpenDictionarySection(string path)
{
// NOTE: Auth:Schemes, RateLimiterOptions:Policies, CacheOptions:Profiles, and
// ValidationOptions:Rules are intentionally NOT in this list. They are name-keyed open
// dictionaries too, but each is intercepted earlier in FindUnknownConfigKeys with a
// type/shape-discriminated schema so user-chosen names get the same per-entry validation that
// matching example names get. Adding them here would silently disable that.
if (path is
"ConnectionStrings" or
"Log:MinimalLevels" or
"Log:OTLResourceAttributes" or
"CommandRetryOptions:Strategies" or
"NpgsqlRest:AuthenticationOptions:ContextKeyClaimsMapping" or
"NpgsqlRest:AuthenticationOptions:ParameterNameClaimsMapping" or
"NpgsqlRest:ClientCodeGen:CustomHeaders")
{
return true;
}
// ErrorHandlingOptions:ErrorCodePolicies:N:ErrorCodes - user-defined postgres error codes
if (path.StartsWith("ErrorHandlingOptions:ErrorCodePolicies:", StringComparison.Ordinal) &&
path.EndsWith(":ErrorCodes", StringComparison.Ordinal))
{
return true;
}
return false;
}
}