Sometimes data is created only to be logged and never used again. Creating intermediate lists or arrays in those cases increases memory usage and adds unnecessary complexity.
A summary string can often be built directly from the source data:
var summary =
string.Join(", ",
source.Select(x => x.Name));
logger.LogInformation("Items=[{Items}]", summary);
If the source might be null, a safe fallback avoids runtime errors:
var safeSource = source ?? Enumerable.Empty<Item>();
var summary =
string.Join(", ",
safeSource.Select(x => x.Name));
logger.LogInformation("Items=[{Items}]", summary);
This approach keeps the code lean while still producing clear, useful log output.
The choice between arrays and lists communicates intent and affects performance and safety.
When data is only needed for display or logging, an array represents a fixed snapshot and avoids accidental changes:
string[] names =
source.Select(x => x.Name).ToArray();
logger.LogInformation(
"Names=[{Names}]",
string.Join(", ", names));
Lists are useful when the collection must be modified or extended later:
var names =
source.Select(x => x.Name).ToList();
names.Add("NewItem");
Choosing the right type makes the code easier to understand and prevents unintended side effects.
When collections are logged directly, many logging systems only display the type name instead of the actual values. This makes it hard to understand what data was processed.
Converting the collection into a readable string solves this problem:
string[] values = { "One", "Two", "Three" };
var text = string.Join(", ", values);
logger.LogInformation("Values=[{Values}]", text);
To give additional context, the number of elements can also be logged:
logger.LogInformation(
"Count={Count}, Values=[{Values}]",
values.Length,
string.Join(", ", values));
Readable output improves troubleshooting and reduces the need to reproduce issues locally.
Complex objects and dictionaries often contain far more data than a log entry really needs. Logging them directly can flood the log with noise or produce unreadable output.
A better approach is to extract only the most relevant values and create a compact summary:
var summary = dataMap.Select(x => new
{
Key = x.Key,
Count = x.Value.Count,
Status = x.Value.Status
});
logger.LogInformation("DataSummary={Summary}", summary);
This keeps the log focused on what matters: identifiers, counts, and simple status values. The result is easier to scan, easier to search, and more useful during debugging or monitoring.
When data is loaded from a list or a database, there is always a chance that nothing is found. If the code still tries to access properties on a missing object, the log statement itself can crash the application.
A simple null check makes the behavior explicit and keeps the log stable:
var item = items.FirstOrDefault(x => x.Id == id);
if (item == null)
{
logger.LogWarning("No item found for Id={Id}", id);
}
else
{
logger.LogInformation(
"Id={Id}, Type={Type}",
item.Id,
item.Type);
}
This version clearly separates the “not found” case from the normal case and produces meaningful log messages for both situations.
When a more compact style is preferred, null operators can be used instead:
logger.LogInformation(
"Id={Id}, Type={Type}",
item?.Id ?? "<unknown>",
item?.Type ?? "<unknown>");
Both approaches prevent runtime errors and ensure that logging remains reliable even when data is incomplete.
Good logging is one of the most underrated tools in software development.
When done right, logs explain what your application is doing — even when things go wrong.
Logging is not just about writing messages to a file or console.
It’s about choosing what to log and how to log it safely and clearly.
Common pitfalls include:
Accessing properties on objects that may be null
Logging complex data structures without readable output
Producing logs that are too verbose or too vague
Creating unnecessary data just for logging purposes
This collection focuses on practical, everyday logging patterns:
Writing null-safe log statements
Turning collections into human-readable output
Logging only the information that matters
Choosing simple and efficient data structures for log data
Each example is intentionally small and generic, so the ideas can be reused in any .NET project.
Value
These patterns help you create logs that are stable, readable, and genuinely useful — especially when debugging production issues.
Testing HttpClient setup is a task many teams underestimate until something breaks in production. Modern .NET applications rely heavily on HttpClientFactory to add features such as retries, logging, authentication, or caching. These behaviors are implemented through message handlers that form a pipeline around every outgoing request.
If one handler is missing or misordered, the entire behavior changes—sometimes silently. A retry handler that never runs or a logging handler that is skipped can lead to confusing and costly issues. That’s why verifying the correct handlers are attached during application startup is essential.
However, developers quickly discover that it is not straightforward to test this. The built-in HttpClient does not expose its handler chain publicly, and typical unit-testing approaches cannot reveal what the factory actually constructs.
This Snipp explains the entire picture:
• the problem developers face when trying to validate HttpClient pipelines
• the cause, which is rooted in .NET’s internal design
• the resolution, with a practical reflection-based method to inspect handlers exactly as the runtime creates them
Following these Snipps, you will be able to reliably confirm that your handlers—such as retry and logging—are attached and working as intended.
To test HttpClient handlers effectively, you need to inspect the internal handler chain that .NET builds at runtime. Since this chain is stored in a private field, reflection is the only reliable method to access it. The approach is safe, does not modify production code, and gives you full visibility into the pipeline.
The process begins by resolving your service from the DI container. If your service stores the HttpClient in a protected field, you can access it using reflection:
var field = typeof(MyClient)
.GetField("_httpClient", BindingFlags.Instance | BindingFlags.NonPublic);
var httpClient = (HttpClient)field.GetValue(serviceInstance);
Next, retrieve the private _handler field from HttpMessageInvoker:
var handlerField = typeof(HttpMessageInvoker)
.GetField("_handler", BindingFlags.Instance | BindingFlags.NonPublic);
var current = handlerField.GetValue(httpClient);
Finally, walk through the entire handler chain:
var handlers = new List<DelegatingHandler>();
while (current is DelegatingHandler delegating)
{
handlers.Add(delegating);
current = delegating.InnerHandler;
}
With this list, you can assert the presence of your custom handlers:
Assert.Contains(handlers, h => h is HttpRetryHandler);
Assert.Contains(handlers, h => h is HttpLogHandler);
This gives your test real confidence that the HttpClient pipeline is constructed correctly—exactly as it will run in production.
Issue
Libraries often expose many raw exceptions, depending on how internal HTTP or retry logic is implemented. This forces library consumers to guess which exceptions to catch and creates unstable behavior.
Cause
Exception strategy is not treated as part of the library’s public contract. Internal exceptions leak out, and any change in handlers or retry logic changes what callers experience.
Resolution
Define a clear exception boundary:
Internally
Catch relevant exceptions (HttpRequestException, timeout exceptions, retry exceptions).
Log them
Use the unified logging method.
Expose only a custom exception
Throw a single exception type, such as ServiceClientException, at the public boundary.
Code Example
catch (Exception ex)
{
LogServiceException(ex);
throw new ServiceClientException("Service request failed.", ex);
}
This approach creates a predictable public API, hides implementation details, and ensures your library remains stable even as the internal HTTP pipeline evolves.
The main reason regular tests cannot inspect HttpClient handlers is simple: the pipeline is private. The HttpClient instance created by IHttpClientFactory stores its entire message-handler chain inside a non-public field named _handler on its base class HttpMessageInvoker.
This means:
So while Visual Studio’s debugger can show the handler sequence, your code cannot. This is why common testing approaches fail: they operate at the service level, not the internal pipeline level.
A service class typically stores a protected or private HttpClient instance:
protected readonly HttpClient _httpClient;
Even if your test resolves this service, the handler pipeline remains invisible.
To validate the runtime configuration—exactly as it will behave in production—you must inspect the pipeline directly. Since .NET does not expose it, the only practical method is to use reflection. The next Snipp explains how to implement this in a clean and repeatable way.
Issue
HTTP calls fail for many reasons: timeouts, throttling, network issues, or retry exhaustion. Logging only one exception type results in missing or inconsistent diagnostic information.
Cause
Most implementations log only HttpRequestException, ignoring other relevant exceptions like retry errors or cancellation events. Over time, this makes troubleshooting difficult and logs incomplete.
Resolution
Use a single unified logging method that handles all relevant exception types. Apply specific messages for each category while keeping the logic in one place.
private void LogServiceException(Exception ex)
{
switch (ex)
{
case HttpRequestException httpEx:
LogHttpRequestException(httpEx);
break;
case RetryException retryEx:
_logger.LogError("Retry exhausted. Last status: {Status}. Exception: {Ex}",
retryEx.StatusCode, retryEx);
break;
case TaskCanceledException:
_logger.LogError("Request timed out. Exception: {Ex}", ex);
break;
case OperationCanceledException:
_logger.LogError("Operation was cancelled. Exception: {Ex}", ex);
break;
default:
_logger.LogError("Unexpected error occurred. Exception: {Ex}", ex);
break;
}
}
private void LogHttpRequestException(HttpRequestException ex)
{
if (ex.StatusCode == HttpStatusCode.NotFound)
_logger.LogError("Resource not found. Exception: {Ex}", ex);
else if (ex.StatusCode == HttpStatusCode.TooManyRequests)
_logger.LogError("Request throttled. Exception: {Ex}", ex);
else
_logger.LogError("HTTP request failed ({Status}). Exception: {Ex}",
ex.StatusCode, ex);
}
Centralizing logic ensures consistent, clear, and maintainable logging across all error paths.
When configuring HttpClient using AddHttpClient(), developers often attach important features using message handlers. These handlers form a step-by-step pipeline that processes outgoing requests. Examples include retry logic, request logging, or authentication.
The problem appears when you want to test that the correct handlers are attached. It is common to write integration tests that resolve your service from the DI container, call methods, and inspect behavior. But this does not confirm whether the handler chain is correct.
A handler can silently fail to attach due to a typo, incorrect registration, or a missing service. You may have code like this:
But you cannot verify from your test that the constructed pipeline includes these handlers. Even worse, Visual Studio can display the handler chain in the debugger, but this ability is not accessible through public APIs.
Without a direct way to look inside the pipeline, teams cannot automatically verify one of the most important parts of their application’s networking stack. The next Snipp explains why this limitation exists.
Creating reliable HTTP client services is a challenge for many .NET developers. Network timeouts, throttling, retries, and unexpected exceptions often lead to inconsistent logging, unclear error messages, and unstable public APIs. This Snipp gives an overview of how to design a clean, predictable, and well-structured error-handling strategy for your HTTP-based services.
Readers will learn why custom exceptions matter, how to log different failure types correctly, and how to build a stable exception boundary that hides internal details from users of a library. Each child Snipp focuses on one topic and includes practical examples. Together, they offer a clear blueprint for building services that are easier to debug, test, and maintain.
The overall goal is simple: Create a .NET service that logs clearly, behaves consistently, and protects callers from internal complexity.
In .NET applications, it’s common to have multiple classes that share the same interface or base class. Instead of registering each class manually in the Dependency Injection (DI) container, you can register them all automatically by scanning the assembly.
Here’s a simple example:
var serviceTypes = typeof(IServiceBase)
.Assembly
.GetTypes()
.Where(t =>
typeof(IServiceBase).IsAssignableFrom(t) &&
!t.IsAbstract &&
!t.IsInterface);
foreach (var type in serviceTypes)
{
services.AddSingleton(typeof(IServiceBase), type);
}
// If you also need a concrete type directly
services.AddSingleton<SpecialService>();
// Example: register a factory or manager
services.AddSingleton<IServiceFactory, ServiceFactory>();
This pattern ensures:
IServiceBase are available through IEnumerable<IServiceBase>.When registering services, you must decide how long they should live in your application:
General advice:
Choosing the right lifetime prevents resource leaks, avoids threading issues, and makes your application more reliable.
When building background services in .NET, it’s helpful to include structured logging for better traceability and diagnostics. One common pattern is using logging scopes to include context like the service or task name in each log entry.
Instead of manually providing this context everywhere, you can simplify the process by automatically extracting it based on the class and namespace — making the code cleaner and easier to maintain.
Replace this verbose pattern:
_logger.BeginScope(LoggingScopeHelper.CreateScope("FeatureName", "WorkerName"));
With a simple, reusable version:
_logger.BeginWorkerScope();
public static class LoggerExtensions
{
public static IDisposable BeginWorkerScope(this ILogger logger)
{
var scopeData = LoggingScopeHelper.CreateScope();
return logger.BeginScope(scopeData);
}
}
Combining base URLs with relative paths in .NET can lead to errors if not handled carefully. This guide shows a safe and reusable way to do it using two methods: one that works with Uri objects and another that accepts strings for convenience.
Here's the core method that safely combines a Uri with a relative path:
public static string SafeCombineUrl(Uri? baseUri, string relativePath)
{
try
{
if (string.IsNullOrWhiteSpace(relativePath))
{
return baseUri?.ToString() ?? string.Empty;
}
if (baseUri == null)
{
return relativePath;
}
return new Uri(baseUri, relativePath).ToString();
}
catch (Exception ex)
{
return $"[InvalidUrl={ex.Message}]";
}
}
To make it easier to use with strings, add this helper method:
public static string SafeCombineUrl(string baseUri, string relativePath)
{
try
{
Uri? baseUriObj = null;
if (!string.IsNullOrWhiteSpace(baseUri))
{
baseUriObj = new Uri(baseUri, UriKind.Absolute);
}
return SafeCombineUrl(baseUriObj, relativePath);
}
catch (Exception ex)
{
return $"[InvalidBaseUri={ex.Message}]";
}
}
relativePath is empty, it returns the base URL.When working with JsonElement in C#, calling methods like TryGetProperty on a default or uninitialized JsonElement can cause runtime exceptions. This usually happens when the JsonElement has not been properly assigned a value.
To avoid this issue, always check whether the JsonElement is valid before accessing its properties. A safe way to do this is by checking its ValueKind.
Here’s a safer extension method that returns a string property only if it exists and the element is valid:
public static string? GetStringProperty(this JsonElement element, string propertyName)
{
if (element.ValueKind == JsonValueKind.Undefined)
return null;
return element.TryGetProperty(propertyName, out var prop) && prop.ValueKind == JsonValueKind.String
? prop.GetString()
: null;
}
This ensures that your code won’t throw an InvalidOperationException when the JsonElement is default.
Use this method when reading from JSON documents where property existence isn’t guaranteed.
When writing unit tests for a custom DelegatingHandler, you might try calling:
var response = await handler.SendAsync(request, cancellationToken);
But this will cause a compiler error. Why? Because SendAsync in DelegatingHandler is protected, meaning you can't call it directly from your test project.
Use HttpMessageInvoker, which is designed to work with any HttpMessageHandler (including DelegatingHandler). It provides a public SendAsync method, so you can easily test your handler:
var handler = new YourCustomHandler
{
InnerHandler = new DummyHandler() // Replace with mock/stub as needed
};
var invoker = new HttpMessageInvoker(handler);
var response = await invoker.SendAsync(request, cancellationToken);
This allows you to simulate HTTP requests through your custom handler, without using HttpClient.
DelegatingHandler is a subclass of HttpMessageHandler.HttpMessageInvoker takes any HttpMessageHandler and exposes a public way to send HTTP requests.protected SendAsync.Use a mock InnerHandler to control the behavior of the response. This helps you test how your DelegatingHandler reacts to different scenarios.
When writing unit tests in .NET using Assert.Contains, it's easy to accidentally check for exact matches when you only want to check if a string is part of another.
Here’s a common mistake:
Assert.Contains("TestCookie=TestValue", cookieStrings);
This fails if cookieStrings contains items like "TestCookie=TestValue; Path=/; HttpOnly" — because "TestCookie=TestValue" is not an exact match to any item.
Use the lambda version of Assert.Contains to check for a substring match:
Assert.Contains(cookieStrings, c => c.Contains("TestCookie=TestValue"));
This makes sure that at least one string in the collection includes "TestCookie=TestValue" somewhere inside it.
In a test where you're adding a cookie to an HttpClient, you might write:
Assert.Contains(
httpClient.DefaultRequestHeaders.GetValues("Cookie"),
c => c.Contains("TestCookie=TestValue")
);
Assert.Contains(item, collection) checks for exact matches.Assert.Contains(collection, predicate) to check for substring matches.Summary:
Masking replaces the middle part of a string with repeated mask characters (like *), preserving the string’s total length and showing only a visible prefix and suffix. If the string is too short, it masks the entire string. The mask character can be customized.
Key points:
*).Example Implementation:
public static string Mask(string input, int prefixLength = 4, int suffixLength = 4, char maskCharacter = '*')
{
if (string.IsNullOrWhiteSpace(input)) return new string(maskCharacter, 3);
if (prefixLength < 0 || suffixLength < 0)
throw new ArgumentOutOfRangeException("Prefix and suffix lengths cannot be negative.");
int inputLength = input.Length;
if (prefixLength + suffixLength >= inputLength)
{
return new string(maskCharacter, inputLength);
}
string prefix = input.Substring(0, prefixLength);
string suffix = input.Substring(inputLength - suffixLength);
string maskedPart = new string(maskCharacter, inputLength - prefixLength - suffixLength);
return prefix + maskedPart + suffix;
}
Use Case:
Ideal when you need to display partial information while maintaining the same length, such as masking credit card numbers or tokens in a UI.
Redaction hides sensitive parts of a string by keeping only a visible prefix and suffix and inserting a customizable placeholder (such as "...") in the middle. If the string is too short, it returns just the placeholder to avoid revealing data.
Key points:
"...", "###", or emojis).Example Implementation:
public static string Redact(string token, int prefixLength = 4, int suffixLength = 4, string redactionString = "...")
{
if (string.IsNullOrWhiteSpace(token)) return "[Token is null or empty]";
if (prefixLength < 0 || suffixLength < 0) return "[Invalid prefix or suffix length]";
if (string.IsNullOrEmpty(redactionString)) redactionString = "...";
int tokenLength = token.Length;
int minLengthForFullRedaction = prefixLength + suffixLength + redactionString.Length;
if (tokenLength >= minLengthForFullRedaction)
{
string prefix = token.Substring(0, prefixLength);
string suffix = token.Substring(tokenLength - suffixLength);
return $"{prefix}{redactionString}{suffix}";
}
int minLengthForPrefixOnly = prefixLength + redactionString.Length;
if (tokenLength >= minLengthForPrefixOnly)
{
string prefix = token.Substring(0, prefixLength);
return $"{prefix}{redactionString}";
}
return redactionString;
}
Use Case:
Useful for logs or UI where a brief summary of sensitive data is needed without showing the entire value.
When handling tokens in .NET applications, it's essential to avoid logging them in full due to the potential exposure of sensitive information. A best practice is to redact tokens before logging by showing only a prefix and/or suffix.
Here’s a robust approach:
Redact tokens safely: Display only the first few and last few characters of the token, separated by ellipses (...). If the token is too short to show both, consider showing only the prefix followed by ..., or return a standardized warning.
Implement a helper method: Encapsulate redaction logic in a shared utility to ensure consistent and secure usage throughout the codebase.
public static string RedactToken(string token, int prefixLength = 6, int suffixLength = 4)
{
if (string.IsNullOrEmpty(token))
return "[null or empty token]";
int minLengthForFullRedaction = prefixLength + suffixLength;
if (token.Length >= minLengthForFullRedaction)
{
var prefix = token.Substring(0, prefixLength);
var suffix = token.Substring(token.Length - suffixLength);
return $"{prefix}...{suffix}";
}
int minLengthForPrefixOnly = prefixLength + 3; // For "..."
if (token.Length >= minLengthForPrefixOnly)
{
var prefix = token.Substring(0, prefixLength);
return $"{prefix}...";
}
return "[token too short to redact securely]";
}
Optional hashing for debugging: If correlation is needed without revealing the token, hash it using a secure algorithm (e.g., SHA256) and log only the hash.
By centralizing redaction in a reusable helper and applying consistent rules, applications can balance debugging needs with security best practices.
C#'s async/await pattern simplifies asynchronous programming, but integrating it into console applications poses a challenge. The traditional static void Main() method can't be marked as async, leading to compiler errors when attempting to use await directly.
Workaround Strategies:
Separate Async Method: Encapsulate asynchronous operations within a separate method marked as async. Then, invoke this method from Main() using .GetAwaiter().GetResult() to execute it synchronously. This approach ensures exceptions are unwrapped properly, avoiding the AggregateException that occurs with .Result or .Wait().
Async Main (C# 7.1 and Later): Starting with C# 7.1, you can define the entry point as static async Task Main(), allowing the use of await directly within Main(). This modernizes the approach and simplifies asynchronous code execution in console applications.
For a detailed explanation and code examples see Async/await in a console application.
When handling sensitive information like passwords, API keys, or personal data, it’s important to protect this data when displaying or logging it. Two common techniques for this are redaction and masking.
"...". This gives a clear but limited preview of the data.*), keeping the original length intact and showing only limited characters at the start and end.Choosing between redaction and masking depends on your needs: whether you want to reduce visible length for compact display (redaction), or maintain length for format consistency while hiding data (masking).
Logging is an essential part of application development for debugging, monitoring, and understanding the flow of execution, especially in complex systems. When logging in a C# method with parameters that need validation, it's crucial to follow best practices to ensure clear and useful log messages. Below is a sample demonstrating how to log and validate parameters in a C# method:
public bool ValidateAndProcessData(string data)
{
// Log the start of the method
_logger.LogInformation("ValidateAndProcessData method started");
// Validate input data
if (string.IsNullOrEmpty(data))
{
_logger.LogError("Input data is null or empty");
throw new ArgumentException("Input data cannot be null or empty", nameof(data));
}
try
{
// Process data
_logger.LogInformation("Processing data: {data}", data);
// Simulating processing time
System.Threading.Thread.Sleep(1000);
_logger.LogInformation("Data processed successfully");
return true;
}
catch (Exception ex)
{
// Log any exceptions that occur during processing
_logger.LogError(ex, "Error processing data: {data}", data);
throw; // Re-throw the exception for higher-level handling
}
finally
{
// Log the end of the method
_logger.LogInformation("ValidateAndProcessData method completed");
}
}
By following this sample, you ensure that your method logs relevant information about parameter validation and method execution, making it easier to debug and monitor your application's behavior.
In a console apps, there is often a need to obtain user input from the console while ensuring that the input is not empty or only whitespace characters.
In this sample, we define a method GetUserInput that takes an optional message parameter. It continuously prompts the user until a non-empty, non-whitespace input is provided.
static string GetUserInput(string message = "Please enter some input:")
{
string input;
do
{
Console.WriteLine(message);
input = Console.ReadLine()?.Trim();
} while (string.IsNullOrWhiteSpace(input));
return input;
}
Explanation:
message parameter allows customizing input prompt message.Console.ReadLine()?.Trim() reads user input and trims leading/trailing whitespace.?. operator is used for null-conditional access, ensuring that Console.ReadLine() doesn't throw a null reference exception if the input is null.do-while loop ensures user input is not empty or whitespace.The provided source code defines a simple DependencyProvider class responsible for managing dependencies using .NET Core's built-in dependency injection system. The class includes a method to retrieve required services based on their interfaces.
public static class DependencyProvider
{
private static IServiceProvider Provider()
{
var services = new ServicesCollection();
services.AddSingleton<IClient, Client>();
return services.BuildServiceProvider();
}
public static T GetRequiredService<T>() where T : notnull
{
var provider = Provider();
return provider.GetRequiredService<T>();
}
}
To use this DependencyProvider, you can retrieve instances of required services as shown below:
readonly IClient _client = DependencyProvider.GetRequiredService<IClient>();
To add IConfiguration to the service collection, you can use the IServiceCollection interface. First, create a ServiceCollection instance, then use the AddSingleton method to add IConfiguration, passing in your configuration object _config.
IConfigurationBuilder builder = new ConfigurationBuilder()
.SetBasePath(AppDomain.CurrentDomain.BaseDirectory)
.AddJsonFile("appsettings.json");
IConfiguration configuration = builder.Build();
IServiceCollection services = new ServiceCollection();
services.AddSingleton<IConfiguration>(configuration);
This setup is particularly useful for scenarios like unit testing background services, as discussed in this Stack Overflow thread: Unit testing a .NET Core background service.
This code snippet demonstrates configuring and retrieving custom settings from appsettings.json. It defines a ClientSettings class to manage client-specific configurations such as name and URL. The appsettings.json file is structured to hold these settings under a "Clients" section. The code includes validation checks to ensure that the required settings are provided and the URL is valid.
// Configuration in appsettings.json
{
"Clients": {
"Client": {
"Name": "Acme Corporation",
"Url": "https://acme.example.com"
}
}
}
// Settings class
internal class ClientSettings
{
public const string ConfigSection = "Clients.Client";
public string ClientName { get; set; } = "DefaultClientName";
public string ClientUrl { get; set; } = string.Empty;
public static ClientSettings Load(IConfiguration configuration)
{
ClientSettings settings = configuration.GetSection(ConfigSection).Get<ClientSettings>() ?? throw new ConfigurationErrorsException($"'{ConfigSection}' section not found. Add configuration to appsettings.json");
if (string.IsNullOrWhiteSpace(settings.ClientName)) throw new ConfigurationErrorsException("ClientName is null or empty");
if (string.IsNullOrWhiteSpace(settings.ClientUrl)) throw new ConfigurationErrorsException("ClientUrl is null or empty");
if (!(Uri.TryCreate(settings.ClientUrl, UriKind.Absolute, out Uri? uri) && (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps)))
{
throw new ConfigurationErrorsException("ClientUrl is not a valid URL");
}
return settings;
}
}
// Using setting
public Client(IConfiguration configuration)
{
_clientSettings = ClientSettings.Load(configuration);
}
Links and Explanation:
Makolyte - C# How to Read Custom Configuration from appsettings.json: This link provides a detailed guide on reading custom configurations from appsettings.json in C#.
Stack Overflow - Configuration.GetSection easily gets primitive string values but not complex values: This Stack Overflow thread discusses issues related to retrieving complex values using Configuration.GetSection in .NET applications and provides insights into resolving such problems.