A lightweight NuGet package designed to enhance web API development in ASP.NET Core. It provides various utilities and features to streamline common tasks and improve the efficiency of API development.
NuGet link: SakurWebApiUtilities
- Rate Limiting: Implement request rate limiting using
ClientStatistics
to track and enforce limits on client requests. - Authentication Setup: Simplifies configuration of authentication schemes with customizable parameters for domains, audiences, and roles.
- Scheduled Tasks: Allows for the creation and management of recurring tasks that can be scheduled at specific intervals or times.
- Request Body Validation: Automatically validates incoming request bodies with detailed error reporting for missing fields.
- Error Handling: Provides
ApiException
andApiResponse
classes for structured error management and consistent API responses. - Connection String Utilities: Simplifies the creation of connection strings for database connections with custom SSL configurations.
- Password Management: Offers methods for hashing, validating, and generating random passwords.
- Extension Methods: Includes various helpful extension methods for common tasks, such as string manipulation and object property transfer.
This package includes support for request rate limiting, using ClientStatistics
to track request history for each client.
Rate limiting is enforced by tracking the timestamps of each client’s requests within a time window, capped by MaxRequests
.
The ClientStatistics
class stores request timestamps in a queue-like structure that tracks only the last MaxRequests
timestamps. This queue is then checked against the defined TimeWindow
to determine if a client has exceeded the allowed rate.
The middleware uses IDistributedCache
to store ClientStatistics
across distributed instances, allowing rate limits to be enforced consistently across multiple servers. You will need to configure a distributed cache in Program.cs
:
In Program.cs
or wherever your Web API is built, add:
builder.Services.AddDistributedMemoryCache(); // to add the distributed memory cache
app.UseRateLimiting(); // to add the rate limiting middleware
Now, use the [Limit] attribute to specify limits for controller methods:
[Limit(MaxRequests = 20, TimeWindow = 10)]
- MaxRequests: Maximum number of requests allowed within the
TimeWindow
period. - TimeWindow: The time window (in seconds) during which
MaxRequests
is allowed.
This package simplifies authentication configuration in your Web API.
In Program.cs
, configure the authentication with the following:
builder.Services.SetupAuth(authDomain, authAudience, roles, authenticationScheme);
Will setup the authentication for the service collection
- Parameters:
authDomain
: The authentication domain (issuer).authAudience
: The audience identifier for the token.roles
: Required roles for accessing the API.authenticationScheme
: The authentication scheme to use (default is"Bearer"
).
Will return the service collection for easy chaining.
Example usage:
builder.Services.SetupAuth(
authDomain: "https://my-auth-domain.com",
authAudience: "my-api-audience",
roles: new[] { "Admin", "User" },
authenticationScheme: "Bearer" // not really needed since this is the default anyway
);
WebApiUtilities provides utilities for setting up scheduled tasks that can run at specific times or intervals. This is useful for automating recurring operations, such as data cleanup, sending periodic notifications, or syncing external data.
- Data Synchronization: Run a daily task to sync data from an external source (e.g., fetching the latest inventory data every night at 2 AM).
- Log Cleanup: Schedule an hourly task to remove outdated logs or temporary files.
- Automated Notifications: Trigger a weekly summary email or alert to users.
With WebApiUtilities, you can set tasks to run at fixed intervals or specific times of day, allowing for a wide range of scheduling needs.
-
IntervalTask: Runs repeatedly at a fixed interval.
- Interval: How frequently the task should run.
- InitialStartDelay: Sets a delay before the task’s first execution. This can help stagger multiple tasks that run at the same interval (e.g., once per minute), avoiding the scenario where many tasks run simultaneously, causing spikes in load. By spreading out task start times, you achieve a more even distribution of workload over time.
-
TimeOfDayTask: Runs once a day at a specific time.
- ScheduledTime: The time of day to execute the task (e.g., 04:00 for 4 AM).
- Define tasks by inheriting from
IntervalTask
orTimeOfDayTask
, and implement theExecuteAsync
method.
Here is an example of a TimeOfDayTask
that updates a list of sanctioned names at 4 AM:
public class UpdateSanctionsTask : TimeOfDayTask
{
public override TimeSpan ScheduledTime => new TimeSpan(4, 0, 0); // 4 AM
public override async Task ExecuteAsync(CancellationToken cancellationToken)
{
await SanctionsManager.Instance.UpdateSanctionedNamesIfNeededAsync();
}
}
- Register tasks in the DI container (probably in your Main function):
builder.Services.AddScheduledTasks(
typeof(BroadcastTask),
typeof(UpdateSanctionsTask));
This setup will handle instantiation and scheduling for registered tasks using the ScheduledTaskManager
.
The RequestBody
class serves as a base class for defining the request body of your web API requests. It provides a structured way to validate incoming data and ensure that all required fields are present before processing the request.
-
Automatic Validation: By inheriting from
RequestBody
, you can automatically validate whether the required properties are provided in the request body. This is done using the custom[Required]
attribute, allowing for easy and consistent validation across your API. -
Missing Properties Reporting: The class includes functionality to generate messages detailing which required fields are missing. This is useful for debugging and providing informative error responses to clients.
-
Flexible Validation Options: You can control whether empty strings are considered valid using the
allowEmptyStrings
parameter in the validation methods. This allows for more flexibility based on your API’s requirements.
To use the RequestBody
class, create a derived class that defines your specific request body structure. Here's an example:
public class MyRequest : RequestBody
{
[Required]
public string Name { get; set; }
[Required]
public int Age { get; set; }
public override bool Valid => ValidateByRequiredAttributes();
// Additional properties and methods can be added as needed.
}
In this example, the MyRequest
class specifies that both Name
and Age
are required fields. The Valid
property leverages the automatic validation mechanism to check the presence of these fields.
-
GetInvalidBodyMessage: Call this method to obtain a detailed string message indicating which fields are missing from the request body.
-
GetMissingProperties: This method returns a list of property names that are required but not provided.
-
ValidateByRequiredAttributes: Use this method to perform a quick validation check against the properties marked with the
[Required]
attribute.
The Valid property can be overridden to provide custom validation logic if needed but then the automatically generated error messages might not be accurate since they are based on the [Required]
attribute.
If the required attribute is not used for any property a custom validation logic should be implemented in the Valid
property. The GetInvalidBodyMessage
and GetMissingProperties
methods can still be used to generate error messages and will then assume all properties that don't have the JsonIgnore attribute are required.
The ApiException
class represents errors that occur within the API. This class is crucial for handling exceptions gracefully and returning meaningful error messages to the client. Key features include:
-
Error Details:
- The class contains properties for storing an error message (
ErrorMessage
), the associated HTTP status code (StatusCode
), and an optional error object (ErrorObject
) that can contain additional context or data related to the error.
- The class contains properties for storing an error message (
-
Constructors for Flexibility:
- There are multiple constructors allowing for different ways to initialize an
ApiException
:- Message and Status Code: The simplest constructor requires only a message and a status code.
- Error Object: Another constructor allows for an error object to be passed, providing further detail about the exception.
- Combined Constructor: A more comprehensive constructor accepts both an error object and a message, enabling detailed error reporting.
- There are multiple constructors allowing for different ways to initialize an
-
HTTP Status Code Utility:
- One of the most useful aspects of the
ApiException
class is the ability to include an HTTP status code. This allows exceptions to be thrown deep within the application logic and subsequently handled at the controller level. By capturing the correct status code, the API can return appropriate responses to clients based on the context of the error.
- One of the most useful aspects of the
-
Readable Output:
- The
ToString()
method is overridden to provide a human-readable representation of the exception, combining the error message with the stack trace. This is useful for logging and debugging purposes.
- The
The ApiResponse
class is designed to encapsulate the response returned by the API. It provides constructors to create responses with various types of content, allowing for flexible API design. Below are the key functionalities:
-
Status Code Handling:
- The
ApiResponse
constructor accepts anHttpStatusCode
parameter, enabling the specification of the HTTP status code for the response. The default is set toHttpStatusCode.OK
, ensuring that responses are valid even without explicit status codes.
- The
-
Response Body Customization:
- Multiple constructors allow for the inclusion of different response body formats:
- Message-Only Response: One constructor allows for just a message to be included in the response body.
- Object Response: Another constructor allows for an arbitrary object to be sent as the response body, providing the flexibility to return complex data structures.
- Multiple constructors allow for the inclusion of different response body formats:
-
Exception Handling:
- The constructor that accepts an
ApiException
creates a well-structured response based on the exception's data. This includes:- An optional error message.
- An optional error object containing additional data related to the exception.
- The appropriate HTTP status code derived from the exception.
- The constructor that accepts an
A static class designed to facilitate the creation of connection strings from a given URL. It includes a method to generate a connection string that accounts for various parameters like SSL mode and trust settings. It uses a nested ConnectionStringBuilder
class to construct the connection string.
- GetConnectionStringFromUrl: Creates a connection string from a provided URL, with options for SSL mode and trust settings.
An enumeration that defines the types of SSL modes that can be used for database connections:
- Disable: SSL is disabled.
- Prefer: SSL is preferred, but not required.
- Require: SSL is required.
A static class that provides functionality to retrieve environment variables. It includes caching to improve performance and throws exceptions if the specified variable is not found or does not meet minimum length requirements.
- GetEnvironmentVariable: Retrieves an environment variable, validating its existence and length, with an option to bypass the cache.
A static class that provides extension methods for working with Npgsql database connections. It allows for the retrieval of objects and collections from the database using asynchronous methods.
- GetAsync: Retrieves a list of objects from the database based on a query and parameters.
- GetSingleOrDefaultAsync: Retrieves a single object or the default value if not found.
The GetAsync<T>
method in the NpgsqlExtensionMethods
class is designed for retrieving complex objects from a PostgreSQL database asynchronously. It allows for mapping complex relationships between entities through manual parameter lookups, enabling developers to fetch and construct related objects efficiently.
It is a bit complicated to understand the use case and why it is needed but here’s a detailed breakdown of the an example, focusing on how GetAsync
is utilized to fetch Transaction
objects along with their related Account
:
First, imagine a Transaction
class and its properties, here is a simplified version:
public class Transaction
{
public int Id { get; set; }
public decimal Value { get; set; }
// AffectedAccount is a reference to another table's record (Account)
public Account? AffectedAccount { get; set; }
}
The AffectedAccount
property is of type Account?
, indicating that it references another entity that can be null if no related account exists. This relationship is key for understanding how GetAsync
works with manual parameter lookups to populate this property.
The database table for Transaction
might look like this:
CREATE TABLE transaction (
id SERIAL PRIMARY KEY,
value NUMERIC NOT NULL,
affected_account INT REFERENCES account(id)
);
This is a common scenario in database design where entities have relationships with other entities, and you need to fetch related objects to construct a complete view of your data. Sometimes, these relationships are complex and require multiple queries or joins to retrieve all the necessary information. Sometimes the "sub-entities" can be stored in a cache in memory to avoid these joins or repeated database calls, it is useful to be able to combine the data from the cache with the data from the database.
Now, here is an example of how GetAsync
is used to to fetch transactions and populate the AffectedAccount
property with the corresponding Account
object:
public async Task<List<Transaction>> GetAll()
{
const string query = "SELECT * FROM transaction";
using (NpgsqlConnection connection = await GetConnectionAsync())
return await connection.GetAsync<Transaction>(query, null, new Dictionary<string, Func<object?, Task<object?>>>()
{
{
nameof(Transaction.AffectedAccount), async (x) =>
{
if(x == null) return null;
return await AccountRepository.Instance.GetByIdAsync((int)x);
}
}
});
}
- The SQL query retrieves all records from the
transaction
table where. - The
GetAsync<Transaction>
method is called to execute the SQL query. The third parameter is a dictionary that defines manual parameter lookups. - The dictionary passed to
GetAsync
specifies how to handle theAffectedAccount
property ofTransaction
.- The key is
nameof(Transaction.AffectedAccount)
, which tells the method to look up this property. - The value is a function that takes an object (expected to be the ID of the
AffectedAccount
because that's what's stored in the database) and returns the correspondingAccount
asynchronously:
async (x) => { if (x == null) return null; return await AccountRepository.Instance.GetByIdAsync((int)x); }
- If
x
is not null, it callsGetByIdAsync
from theAccountRepository
to fetch theAccount
object by its ID.
- The key is
AccountRepository.GetByIdAsync() implementation
It doesn't really matter for the explanation because how it is implemented doesn't really concern the method that fetches the transactions but here is an example of how `GetByIdAsync` might be implemented in the `AccountRepository`:public async Task<Account?> GetByIdAsync(int id)
{
if (accountsCache == null)
await GetAllAsync();
if (!accountsCache.ContainsKey(id))
return null;
return accountsCache[id];
}
- The
GetAsync
method executes the SQL query and starts mapping the results toTransaction
objects. - For each transaction, if the
AffectedAccount
field in the database has a value, the provided function is invoked, allowing it to retrieve the fullAccount
object asynchronously. - This pattern allows you to maintain the relationships between your entities effectively without needing to manually join tables in your SQL queries or handling nested SQL commands.
-
Complex Object Relationships: It simplifies the retrieval of complex entities and their relationships by allowing manual lookups for properties that reference other tables.
-
Asynchronous Operation: The use of asynchronous methods (
await
) ensures that database operations do not block the execution thread, enhancing application performance. -
Separation of Concerns: The logic for retrieving related accounts is encapsulated in the
AccountRepository
, adhering to principles of clean architecture and separation of concerns. -
Dynamic Fetching: This method provides a dynamic way to fetch related objects based on the specific requirements defined in the SQL query, making it flexible for various use cases.
-
Caching Opportunities: The
GetByIdAsync
method includes caching logic, which can improve performance by reducing repeated database calls for the same account data.
The use of GetAsync
with manual parameter lookup in the context of the Transaction
and Account
demonstrates a powerful approach for handling complex data retrieval in a clean and efficient manner. It abstracts the intricacies of joining and fetching related data while allowing for asynchronous execution and better resource management.
A static class containing utilities for password management, including hashing and validation.
- ValidPassword: Validates a plaintext password against a stored hash.
- CreatePasswordHash: Creates a hash and salt from a plaintext password.
- CreateRandomPassword: Generates a random password of a specified length.
-
FirstCharToLowerCase
- Description: Converts the first character of a string to lowercase.
- Usage:
string result = myString.FirstCharToLowerCase();
-
GetSignedHash
- Description: Creates a signed hash of a message using a specified secret.
- Usage:
string hash = myMessage.GetSignedHash(mySecret);
-
SplitByUpperCase
- Description: Splits a string into an array of substrings based on uppercase letters.
- Usage:
string[] parts = myString.SplitByUpperCase();
-
JoinWith
- Description: Joins an array of strings with a specified character.
- Usage:
string result = myArray.JoinWith(", ");
-
GetContentHash
- Description: Creates an MD5 hash from the content of a string.
- Usage:
string hash = myString.GetContentHash();
-
ApplyParameters
- Description:
The
ApplyParameters
method is designed to replace placeholders in a given text with corresponding property values from provided parameter objects. The placeholders are formatted as{{parameterName}}
, whereparameterName
corresponds to the property names of the objects passed as parameters. - How It Works:
- The method first checks if the
parameters
array is null or if thetext
does not contain any placeholders. If so, it returns the original text. - It iterates over each parameter object and retrieves its properties using reflection.
- For each property, it checks if its value is non-null and then replaces any occurrences of the corresponding placeholder in the text with the property's string value.
- This method is useful for dynamically generating strings, such as templates, where specific values need to be injected based on object properties.
- The method first checks if the
- Usage Example:
var user = new { Name = "Alice", Age = 30 }; string messageTemplate = "Hello, {{Name}}! You are {{Age}} years old."; string result = messageTemplate.ApplyParameters(user); // result: "Hello, Alice! You are 30 years old."
- Description:
The
-
TransferPropertiesTo
- Description:
The
TransferPropertiesTo
method facilitates the copying of property values from one object (source) to another (target) based on matching property names and types. This is particularly useful when you have two objects that share some properties, and you want to update the target object with values from the source object. - How It Works:
- The method checks if the target object is null and throws an
ArgumentNullException
if it is. - It retrieves the properties of both the source and target objects using reflection.
- It iterates through each property of the source object, searching for a property in the target object that has the same name and type.
- If a matching property is found, it checks if the target property can be written to (i.e., it has a setter). If so, it copies the value from the source property to the target property.
- This method helps ensure that only compatible properties are copied, preventing type mismatches and enhancing code safety.
- The method checks if the target object is null and throws an
- Usage Example:
public class Source { public string Name { get; set; } public int Age { get; set; } } public class Target { public string Name { get; set; } public int Age { get; set; } public string Address { get; set; } } var source = new Source { Name = "Bob", Age = 25 }; var target = new Target(); target = source.TransferPropertiesTo(target); // target now has Name = "Bob" and Age = 25. Address remains null.
- Description:
The
-
TakeRandom (List)
- Description: Selects a random element from a list.
- Usage:
var randomElement = myList.TakeRandom();
-
TakeRandom (T[])
- Description: Selects a random element from an array.
- Usage:
var randomElement = myArray.TakeRandom();
-
ToSqlIdParameterList
- Description: Converts a list of integers into a string format suitable for SQL queries, e.g.,
(1, 2, 3)
. - Usage:
string sqlList = myIds.ToSqlIdParameterList();
- Description: Converts a list of integers into a string format suitable for SQL queries, e.g.,